[agent] Found by the scheduled Gradle bug-hunt routine (ledger #319).
Summary
For a Gradle build, scan --mode hosted prints a redirect_gradle_manual_snippet warning. It tells the user to paste an exclusiveContent block and to bump the dependency to <base>-socket.<hex8>, and says this "is fail-closed by repo exclusivity". That only holds when nothing else in the graph asks for the artifact. Gradle's version ordering puts a version with an extra non-numeric part below the bare version (1.10.0-socket.4d5e6f70 < 1.10.0). So if any other dependency transitively requests the base version, conflict resolution silently picks upstream 1.10.0 from Maven Central. The build exits 0 with the unpatched jar on the classpath.
Impact
The documented guarantee is broken silently. Vulnerable artifacts are usually pulled in transitively (commons-text is a dependency of commons-configuration2, for example), so following the snippet's instructions exactly still ships the vulnerable bytes, and nothing shows it.
Repro
- Run
scan --mode hosted against a mock API that returns a maven2 override with mavenSuffixedVersion = 1.10.0-socket.4d5e6f70, plus a warm MAVEN_REPO_LOCAL holding commons-text 1.10.0 (the only way discovery finds it; see the sibling issue on discovery). socket-patch prints this snippet (index URL replaced by a local file:// repo that serves the patched jar and the re-versioned pom):
plugins { id 'java' }
repositories { mavenCentral() }
repositories {
exclusiveContent {
forRepository {
maven { url "file:///tmp/gw/socketrepo" }
}
filter {
includeVersion("org.apache.commons", "commons-text", "1.10.0-socket.4d5e6f70")
}
}
}
dependencies {
implementation 'org.apache.commons:commons-text:1.10.0-socket.4d5e6f70' // bumped, as instructed
implementation 'org.apache.commons:commons-configuration2:2.9.0' // depends on commons-text 1.10.0
}
gradle dependencyInsight --dependency commons-text --configuration runtimeClasspath:
org.apache.commons:commons-text:1.10.0
Selection reasons:
- By conflict resolution: between versions 1.10.0 and 1.10.0-socket.4d5e6f70
org.apache.commons:commons-text:1.10.0-socket.4d5e6f70 -> 1.10.0
runtimeClasspath holds commons-text-1.10.0.jar, the build exits 0, and there's no warning.
Expected vs actual
- Expected (docs/ecosystems.md, "Gradle (hosted Maven)"): "It is fail-closed by repository exclusivity: the
exclusiveContent filter routes only the suffixed version to the Socket repo, which is the only place it exists." The patched jar should be the one used, or the build should fail.
- Actual: the suffixed version is silently upgraded away to the unpatched upstream
1.10.0.
Changing the declaration to a strict version fixes it on 9.8.0: implementation('org.apache.commons:commons-text') { version { strictly '1.10.0-socket.4d5e6f70' } } resolves commons-text-1.10.0-socket.4d5e6f70.jar. A constraints { … strictly … } block or a resolutionStrategy.force would also work, and those also cover the transitive-only case where the user has no declaration to bump.
Matrix (Linux; each cell run twice with the same result)
| Gradle |
JDK |
Direct dep only |
Direct + transitive base request |
| 6.9.4 |
11 |
patched ✅ |
unpatched, exit 0 ❌ |
| 7.6.6 |
17 |
patched ✅ |
unpatched, exit 0 ❌ |
| 8.14.3 |
21 |
patched ✅ |
unpatched, exit 0 ❌ |
| 9.8.0 |
21 |
patched ✅ |
unpatched, exit 0 ❌ |
Version ordering is OS-independent Gradle core behaviour, so macOS and Windows weren't probed.
First bad release
v4.0.0. The snippet doesn't exist in v3.3.0.
Suspect code
crates/socket-patch-core/src/patch/redirect/mod.rs:7139 (gradle_snippet): emits a plain declaration bump with no strictly / constraint.
- docs/ecosystems.md "Gradle (hosted Maven)": makes the fail-closed claim.
[agent] Found by the scheduled Gradle bug-hunt routine (ledger #319).
Summary
For a Gradle build,
scan --mode hostedprints aredirect_gradle_manual_snippetwarning. It tells the user to paste anexclusiveContentblock and to bump the dependency to<base>-socket.<hex8>, and says this "is fail-closed by repo exclusivity". That only holds when nothing else in the graph asks for the artifact. Gradle's version ordering puts a version with an extra non-numeric part below the bare version (1.10.0-socket.4d5e6f70 < 1.10.0). So if any other dependency transitively requests the base version, conflict resolution silently picks upstream1.10.0from Maven Central. The build exits 0 with the unpatched jar on the classpath.Impact
The documented guarantee is broken silently. Vulnerable artifacts are usually pulled in transitively (commons-text is a dependency of commons-configuration2, for example), so following the snippet's instructions exactly still ships the vulnerable bytes, and nothing shows it.
Repro
scan --mode hostedagainst a mock API that returns amaven2override withmavenSuffixedVersion=1.10.0-socket.4d5e6f70, plus a warmMAVEN_REPO_LOCALholding commons-text 1.10.0 (the only way discovery finds it; see the sibling issue on discovery). socket-patch prints this snippet (index URL replaced by a localfile://repo that serves the patched jar and the re-versioned pom):plugins { id 'java' } repositories { mavenCentral() } repositories { exclusiveContent { forRepository { maven { url "file:///tmp/gw/socketrepo" } } filter { includeVersion("org.apache.commons", "commons-text", "1.10.0-socket.4d5e6f70") } } } dependencies { implementation 'org.apache.commons:commons-text:1.10.0-socket.4d5e6f70' // bumped, as instructed implementation 'org.apache.commons:commons-configuration2:2.9.0' // depends on commons-text 1.10.0 }gradle dependencyInsight --dependency commons-text --configuration runtimeClasspath:runtimeClasspathholdscommons-text-1.10.0.jar, the build exits 0, and there's no warning.Expected vs actual
exclusiveContentfilter routes only the suffixed version to the Socket repo, which is the only place it exists." The patched jar should be the one used, or the build should fail.1.10.0.Changing the declaration to a strict version fixes it on 9.8.0:
implementation('org.apache.commons:commons-text') { version { strictly '1.10.0-socket.4d5e6f70' } }resolvescommons-text-1.10.0-socket.4d5e6f70.jar. Aconstraints { … strictly … }block or aresolutionStrategy.forcewould also work, and those also cover the transitive-only case where the user has no declaration to bump.Matrix (Linux; each cell run twice with the same result)
Version ordering is OS-independent Gradle core behaviour, so macOS and Windows weren't probed.
First bad release
v4.0.0. The snippet doesn't exist in v3.3.0.
Suspect code
crates/socket-patch-core/src/patch/redirect/mod.rs:7139(gradle_snippet): emits a plain declaration bump with nostrictly/ constraint.