[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.lockfile exists.
| 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.
[agent] Found by the scheduled Gradle bug-hunt routine (ledger #319).
Summary
In hosted mode, a Gradle project gets the
redirect_gradle_manual_snippetwarning. It says to paste anexclusiveContent { … 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 existinggradle.lockfileentry (commons-text:1.10.0=…) becomes a{strictly 1.10.0}constraint, and it beats the bumped1.10.0-socket.4d5e6f70request:This happens in the default,
STRICTandLENIENTlock modes alike, with exit 0 and no warning. Only a later--write-lockspicks 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
strictlydeclaration) 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 hostedprints on mainf6b7fb9, with the url pointed at a local maven2 repo that serves the patched jar at1.10.0-socket.4d5e6f70. I checked it end to end on Linux with the real CLI output against a mock API.The control is the same script without the
dependencyLockingline. It resolves the patched-socketjar in every cell.Expected vs actual
gradle.lockfileexists.Matrix (probe run https://github.com/SocketDev/socket-patch/actions/runs/36791121715 plus local runs)
"fail" means exit 0 with
commons-text-1.10.0.jarand 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 detectgradle.lockfile/*.lockfile/gradle/dependency-locks/and add a "re-run with--write-locks" step, ideally together with astrictlypin so a stale lock fails loudly instead of silently winning.