Skip to content

Maven global/agent discovery looks in $M2_HOME/repository and ignores settings.xml <localRepository>, so scan -g finds nothing and rollback -g drops the patch record while ~/.m2 stays patched #423

Description

[agent] Found by the scheduled Maven bug-hunt routine (ledger #318).

Summary

MavenCrawler::m2_repo_path() decides which Maven local repository agent mode and -g work on. It checks, in order, $MAVEN_REPO_LOCAL, then $M2_HOME/repository, then ~/.m2/repository. That doesn't match Maven:

  1. M2_HOME is the Maven installation directory. No Maven version keeps or looks for a local repository under it. Many machines still export it (Jenkins tool installers, Windows setup guides, older Docker images). With it set, socket-patch looks in /opt/apache-maven-x/repository, which doesn't exist, while Maven keeps using ~/.m2/repository.
  2. <localRepository> in ~/.m2/settings.xml is never read. That's the standard way to move the local repository (corporate setups, CI caches, a separate disk). Maven uses the configured directory, and socket-patch looks at the empty or stale ~/.m2/repository.

In both cases the crawler finds an empty or missing directory and every command treats it as "nothing installed":

  • scan -g, scan -g --mode agent --yes, and project-mode scan --mode agent report scannedPackages: 0 with status: success and exit 0. Nothing is checked and nothing is patched.
  • rollback -g exits 0 with skipped: package_not_installed. It also removes the manifest entry (manifest.removedEntries) and garbage-collects the blob. The jar or pom in the real local repository stays patched, and since the record is gone, a later rollback -g with a correct environment can't restore it (rolledBack: 0).
  • apply -g does fail loudly (exit 1, skipped: 1), as the contract says, but the message calls the package not installed when it is.

Impact

scan -g misses every global Maven install on any machine with M2_HOME set or a relocated local repository. The rollback case loses data: the user is told the tree is clean (exit 0, "not installed"), the only record of the patch is deleted, and Maven goes on building with the patched bytes from the real local repository.

Repro (Linux, Maven 3.9.11, JDK 21)

You need a stub patch API that serves one agent patch for pkg:maven/org.apache.commons/commons-text@1.10.0: it appends <!-- SOCKET-PATCH-GLOBAL-MARKER --> to commons-text-1.10.0.pom, with the same shapes as the wiremock mock in tests/docker_e2e_maven.rs.

SP=target/release/socket-patch
A="--api-url http://127.0.0.1:18998 --api-token fake --org org --ecosystems maven"
export HOME=$(mktemp -d); export MAVEN_OPTS=-Duser.home=$HOME
REL=org/apache/commons/commons-text/1.10.0/commons-text-1.10.0.pom
# a real global install in the default local repository
mvn -q org.apache.maven.plugins:maven-dependency-plugin:3.1.2:get -Dartifact=org.apache.commons:commons-text:1.10.0
cd $(mktemp -d)
$SP scan -g --mode agent --yes --json $A      # applied: 1; the global pom now has the marker

# Trigger 1: M2_HOME pointing at the Maven install (Maven itself still uses ~/.m2/repository)
export M2_HOME=/opt/apache-maven-3.9.11
mvn -X -o validate | grep 'Using local repository'   # -> $HOME/.m2/repository
$SP scan -g --json $A | grep scannedPackages        # -> 0
$SP rollback -g --yes --json $A                     # rc=0, skipped package_not_installed,
                                                    # removedEntries=[...commons-text@1.10.0], removedBlobs=1
grep -c GLOBAL-MARKER ~/.m2/repository/$REL         # -> 1 (still patched)
cat .socket/manifest.json                           # -> {"patches":{}}
unset M2_HOME; $SP rollback -g --yes --json $A      # rolledBack: 0, still patched; nothing left to restore it from

# Trigger 2: <localRepository> in ~/.m2/settings.xml
echo '<settings><localRepository>/data/m2</localRepository></settings>' > ~/.m2/settings.xml
# (with the global install living in /data/m2) the same results: scan -g -> scannedPackages 0;
# rollback -g -> package_not_installed, the record is dropped, /data/m2 stays patched

Control: without M2_HOME and with the repository at ~/.m2/repository, the same scan -g reports scannedPackages: 244, and rollback -g restores the pom byte for byte. --global-prefix /data/m2 also finds all 244 packages.

Expected vs actual

  • Expected: -g (and agent mode in a project) uses the local repository Maven actually uses. The maintainer brief for global mode asks for that ("~/.m2/repository, settings.xml <localRepository>, MAVEN_REPO_LOCAL, -Dmaven.repo.local"), and the crawler's own doc says it returns ~/.m2/repository/. CLI_CONTRACT.md (rollback contract, "Exit rules") says "Everything that leaves the system still patched DOES flip it to partial_failure exit 1".
  • Actual: it uses $M2_HOME/repository and ignores settings.xml. The scan reports success with 0 packages, and the rollback reports success while leaving the system patched and deleting the record.

Matrix (Linux, JDK 21; each run twice)

Maven's own choice was checked with mvn -X ("Using local repository at …"). Every line uses ~/.m2/repository when M2_HOME is set, and the <localRepository> directory when settings.xml sets one.

Maven M2_HOME set: scan -g / rollback -g settings.xml <localRepository>: scan -g / rollback -g
3.6.3 fail (0 scanned) / fail fail / fail
3.8.8 fail / fail fail / fail
3.9.11 fail / fail fail / fail
4.0.0-rc-7 fail / fail fail / fail

The crawler doesn't depend on the Maven version, so the scan and rollback rows are the same socket-patch result on every line. macOS and Windows are untested, but the code path doesn't depend on the OS (on Windows %M2_HOME% is set even more often).

First bad version: not a v5 regression. The same m2_repo_path() (with M2_HOME, without settings.xml) is already in 4.0.0 (f6b7fb9). Tested on main 2463257.

Suspect code

  • crates/socket-patch-core/src/crawlers/maven_crawler.rs:709 m2_repo_path(): the M2_HOME arm, and no settings.xml lookup (~/.m2/settings.xml, then $MAVEN_HOME/conf/settings.xml / ${maven.home}, with ${user.home} / ${env.X} interpolation).
  • maven_crawler.rs:565 get_maven_repo_paths() returns an empty list when the directory is missing, so the misdiscovery turns into "nothing installed" with no warning.
  • Rollback dropping manifest entries and blobs for package_not_installed turns the misdiscovery into data loss. At least under -g, a not-found entry should keep its record, or warn.

The NuGet version of this bug (globalPackagesFolder ignored) is #397.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions