Repository navigation
Hosted Gradle snippet silently builds the unpatched jar in any project using dependency locking (gradle.lockfile), on every Gradle major and OS #396
Description
[agent] Found by the scheduled Gradle bug-hunt routine (ledger #319).
Summary
In hosted mode, a Gradle project gets the redirect_gradle_manual_snippet warning. It says to paste an exclusiveContent { … includeVersion(g, a, "<base>-socket.<hex8>") } block and to "bump the … dependency declaration to version -socket. — exclusiveContent is fail-closed by repo exclusivity." When the project uses Gradle dependency locking, which is common for reproducible builds, following those instructions exactly still gives a successful build with the unpatched upstream jar. The existing gradle.lockfile entry (commons-text:1.10.0=…) becomes a {strictly 1.10.0} constraint, and it beats the bumped 1.10.0-socket.4d5e6f70 request:
runtimeClasspath
+--- org.apache.commons:commons-text:1.10.0-socket.4d5e6f70 -> 1.10.0
+--- org.apache.commons:commons-text:{strictly 1.10.0} -> 1.10.0 (c)
This happens in the default, STRICT and LENIENT lock modes alike, with exit 0 and no warning. Only a later --write-locks picks up the patched version, and neither the snippet nor docs/ecosystems.md mentions lockfiles.
This is separate from #347. There, a transitive base-version request wins version conflict resolution. Here there's no transitive at all: the lock state's strict constraint wins on its own. A fix for #347 (for example a strictly declaration) would turn this into a loud resolve failure. That's better, but the guidance would still need to say "re-run --write-locks".
Impact
In any locked Gradle build, the user sees success and follows the documented steps, but ships the vulnerable jar. The "fail-closed" claim doesn't hold.
Repro
This needs no Socket API. The snippet text is exactly what scan --mode hosted prints on main f6b7fb9, with the url pointed at a local maven2 repo that serves the patched jar at 1.10.0-socket.4d5e6f70. I checked it end to end on Linux with the real CLI output against a mock API.
echo "rootProject.name='app'" > settings.gradle
cat > build.gradle <<'EOF'
plugins { id 'java' }
repositories { mavenCentral() }
dependencyLocking { lockAllConfigurations() } // same result with lockMode = LockMode.STRICT / LENIENT
dependencies { implementation 'org.apache.commons:commons-text:1.10.0' }
tasks.register('cp', Copy) { from configurations.runtimeClasspath; into 'build/cp' }
EOF
gradle dependencies --write-locks # the project's committed lock state
# follow redirect_gradle_manual_snippet: paste the snippet and bump the declaration
cat >> build.gradle <<'EOF'
repositories {
exclusiveContent {
forRepository {
maven { url "file:///path/to/socket-repo" }
}
filter {
includeVersion("org.apache.commons", "commons-text", "1.10.0-socket.4d5e6f70")
}
}
}
EOF
sed -i "s/commons-text:1.10.0'/commons-text:1.10.0-socket.4d5e6f70'/" build.gradle
gradle cp; ls build/cp # exit 0, commons-text-1.10.0.jar (upstream, unpatched)
gradle dependencies --write-locks && gradle cp # only now: commons-text-1.10.0-socket.4d5e6f70.jar (patched)The control is the same script without the dependencyLocking line. It resolves the patched -socket jar in every cell.
Expected vs actual
- Expected (docs/ecosystems.md, "Gradle (hosted Maven)"): with the snippet pasted and the declaration bumped, the build is "fail-closed by repository exclusivity". It should consume the patched jar or fail.
- Actual: the build succeeds and uses the upstream jar whenever a
gradle.lockfileexists.
Matrix (probe run https://github.com/SocketDev/socket-patch/actions/runs/36791121715 plus local runs)
| OS | Gradle 6.9.4 (JDK 11) | 7.6.6 (JDK 17) | 8.14.3 (JDK 21) | 9.8.0 (JDK 21) |
|---|---|---|---|---|
| Linux | fail ×3 lock modes | fail ×3 | fail ×3 (also local) | fail ×3 (also local) |
| macOS | fail ×3 | fail ×3 | fail ×3 | fail ×3 |
| Windows | fail ×3 | fail ×3 | fail ×3 | fail ×3 |
| no-lock control | pass everywhere | pass | pass | pass |
"fail" means exit 0 with commons-text-1.10.0.jar and no patch marker. It's the first release with the snippet, v4.0.0 (per ledger #319).
Suspect code
crates/socket-patch-core/src/patch/redirect/mod.rs:7139 (gradle_snippet) and the call site at :6683. The guidance ignores lock state. The rewriter already receives the project files, so it could detect gradle.lockfile / *.lockfile / gradle/dependency-locks/ and add a "re-run with --write-locks" step, ideally together with a strictly pin so a stale lock fails loudly instead of silently winning.
Activity
gradle: run 2 ledger (#395, #396)
mikolalysenko commentedon Oct 1, 2026
[agent] Triage: priority:p3 (Gradle). Related to #347 and #348 (all three concern the
redirect_gradle_manual_snippetguidance) but a distinct defect: here the dependency-lock state's strict constraint overrides the bumped declaration even with no transitive request. A fix for #347 alone would not resolve it (the guidance would still need a--write-locksstep), so it is cross-linked rather than clustered or closed.Generated by Claude Code
mikolalysenko commentedon Oct 2, 2026
[agent] Claiming this issue as part of the full Gradle support campaign (agent / hosted / vendored modes, tested on Gradle 6.9/7.6/8.x/9.x on Linux, macOS and Windows). Branch: feat/gradle-support. Claim-ID: 2026-10-02T13:56:32Z-gradle21
Generated by Claude Code
Full Gradle support in agent, hosted and vendored modes (#646)
sbt, Mill and scala-cli support in agent, hosted and vendored modes (#…