Repository navigation
Stop vendored Gradle liveness attesting a version the lockfile overrides - #2004
Draft
Mikola Lysenko (mikolalysenko) wants to merge 2 commits into
Draft
Mikola Lysenko (mikolalysenko) wants to merge 2 commits into
Mikola Lysenko (mikolalysenko) wants to merge 2 commits into
Conversation
Vendored Gradle keeps the original GAV and leaves lock files unchanged, but neither its planner nor its VEX liveness (`wired`, which re-plans) read any lock file. A `gradle.lockfile` pinning the patched GA at another version makes Gradle resolve that version (or fail lock validation), yet vendored liveness still attested the patch. The planner now reads every lock file of every project of the build with the shared `gradle::locks` reader (per-project, buildscript, settings and legacy `gradle/dependency-locks/` files, via `ScriptGraph::lockfile_paths`) and degrades with `gradle_lock_overrides_vendored` when any of them locks the patched GA at a version other than the vendored one, or is not UTF-8. A custom `lockFile` location is degraded with `gradle_custom_lock_file`. Since `wired` requires a re-plan with no degraded warning, both withhold VEX attestation; vendoring itself still applies. Mapping from hosted: hosted pins a suffixed version (below-base lock is invalid, above-base unattested); vendored serves only its own version, so any other locked version is unattested. Hosted rejects every settings classpath lock because its script runs too late; vendored wires that classpath with an in-block exclusiveContent entry, so a settings lock at the vendored version is served. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Review findings on the vendored Gradle lock check: - A settings-classpath lock is accepted only when that build's settings script holds the in-block exclusiveContent entry for the GAV in every block that needs one. Liveness (`wired`) now re-plans with `Committed::Only`, so it counts only the committed entry, never one a re-plan would write; vendoring counts the entry it writes. Otherwise `gradle_settings_lock_unrouted`, as hosted rejects it. - A lock at another version of the GA that the vendored index also serves is not an override, so two vendored versions of one GA no longer degrade each other. - A listed lock file that cannot be read as UTF-8 text (I/O error, refused symlink, non-UTF-8) fails closed with its own reason, `gradle_lock_unreadable`, instead of being skipped. - A custom `lockFile` set as a literal `file(...)` (or `"$projectDir/..."`) in a project's own build script, inside the checkout, is read and checked like any lock (missing fails closed); only an unfollowable one is `gradle_custom_lock_file`. - One lock inventory: `ScriptGraph::read_locks` (and `custom_lock_files`), used by hosted discovery and the vendored check; `is_settings_lock` moves to `gradle::locks`. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
Vendored Gradle keeps the original GAV, routes it to the committed repository with
exclusiveContent, and leaves lock files unchanged. Neither the vendored planner nor its VEX liveness (vendor::jvm::gradle::wired/wired_checked, which re-plan and require no degraded warning) read any lock file. Ifgradle.lockfile(or another lock of the build) pins the patched GA at a different version, Gradle resolves that locked version (or fails lock validation), yet vendored liveness still attested the patch. Hosted discovery already checks every lock (vex/discover/gradle.rs).Fix
The planner in
vendor/jvm/gradle.rsnow runscheck_locksaftercheck_graph. It reads the build's lock inventory, which is now shared with hosted discovery:ScriptGraph::read_locks, coveringgradle.lockfile,buildscript-gradle.lockfile,settings-gradle.lockfileand the legacygradle/dependency-locks/*.lockfileof every project of every build. It also reads every customlockFilethe graph can follow (ScriptGraph::custom_lock_files).Each problem below adds a
vendor_jvm_degradedwarning.wiredrequires a re-plan with no degraded warning, so each one also withholds VEX attestation. The patch is still applied.gradle_lock_overrides_vendoredgradle_settings_lock_unroutedsettings-gradle.lockfileentry exists, but that build's settings script does not hold the in-blockexclusiveContententry for the GAV in every block that needs one (buildscript, andpluginManagementwhen there are settingsplugins{}).gradle_lock_unreadablegradle_custom_lock_filelockFilelocation can't be followed statically. Only a literalfile('…')or"$projectDir/…"in a project's own build script, outsideallprojects/subprojects/configure/project(…)blocks and inside the checkout, can be followed.How hosted's rules map to vendored (also in the doc comment). Hosted pins a suffixed version: a lock below the base is invalid, and a lock above it is
LockAboveBase(unattested). Vendored keeps the original GAV, so a lock entry is served only at a vendored version. Any other locked version, above or below, means the build doesn't consume the patch.Hosted rejects every settings-classpath lock, because its script runs after that classpath resolves. Vendored instead routes that classpath with in-block entries, so it accepts a settings lock only when the settings script actually holds them:
Committed::OrPlanned).Committed::Only); one a re-plan would write doesn't count.Anything else is rejected, as hosted rejects it.
Docs: the reason codes are added to the vendored table in
CLI_CONTRACT.md, with a paragraph indocs/design/maven-vendoring.md.Review findings (
/code-review high)settings_lock_needs_the_committed_in_block_entry. A settings lock is accepted with the committed entry. If the entry is stripped, liveness fails even though a re-plan would write it again. A settings script with no in-block entry is rejected (also covered inlock_at_another_version_withholds_liveness).two_vendored_versions_of_a_ga_do_not_override_each_other.tools/test classpath included) does resolve the GA, just at that version, so that part of the build doesn't consume the patch. Hosted, which is ground truth, also withholds the whole build for one above-base lock. What can't be decided statically is whether Gradle reads a lock at all (whether that project enablesdependencyLocking), so every lock of the build counts. The message now names the locking configurations.readcan't return (I/O error, refused symlink outside the checkout, non-UTF-8) now fails closed. Test:unreadable_lock_files_fail_closed.gradle_lock_unreadablewith an accurate message, documented inCLI_CONTRACT.md.lockFiledegraded every patch: confirmed, fixed. A followable custom lock is read and parsed like any lock. A missing one fails closed, since it was set explicitly. Only an unfollowable one isgradle_custom_lock_file. Tests:custom_lock_file_is_followed_when_static, plus the path-resolution cases ingradle::graph::tests::custom_lock_file_and_settings_classpath. Hosted keeps its blanket rejection oflockFile, because the hosted planner can't rewrite such a lock.plan: refuted as a blocker, noted for follow-up. The lock pass reuses theScriptGraphthatplanalready builds. Per call it lists two directories per project and reads each lock once. That is the same order as the existing per-call cost, since everyplancall (one per patch, and one per entry invexliveness) already re-collects and re-tokenizes the whole script graph. Lock files are small line-oriented text. A real per-root cache has to cover the graph and the locks together, and it needs a planning context threaded throughplan,plan_with_external,wired,entry_wired_checkedand the CLIvexloop. That is better done as its own change than bolted onto locks alone.ScriptGraph::read_locksis now the single inventory. Hosted discovery and the vendored check both use it,ScriptGraph::custom_lock_fileis built oncustom_lock_files, andis_settings_lockmoved togradle::locks(re-exported frompatch::redirect::gradle). The hosted planner's lock rewrite still lists files through the samelockfile_pathsbecause it needs the raw text to edit.Tests
vendor::jvm::gradle::testsfail against the previous commit'scheck_locks, verified by splicing the old function back in. They pass with this change:lock_at_another_version_withholds_livenesssettings_lock_needs_the_committed_in_block_entrytwo_vendored_versions_of_a_ga_do_not_override_each_otherunreadable_lock_files_fail_closedcustom_lock_file_is_followed_when_staticlock_at_another_version_withholds_livenessalso fails on origin/main.cargo test -p socket-patch-core --lib -- vendor::jvm:: vex::discover:: gradle::: 775 passed.cargo test -p socket-patch-core --lib -- vex::discover::testing::golden::tests::committed_fixture_corpus_matches_golden vendor:: patch::redirect::gradle: 2892 passed. No golden changes.cargo clippy --locked --workspace --all-features -- -D warningsandcargo fmt --all -- --checkare clean.Not done
gradle_range_excludes_vendored.wiredignores re-plan writes in general: a settings-classpath declaration of the GA whose in-block entry was removed after vendoring still attests when there is no settings lock. This PR closes that only for the locked case (finding 1).🤖 Generated with Claude Code