Legerix extracts a co-bundling consumer's natives into its own cache, and validates them as its own
Summary. extractAllFromResourceDir reads from the jar that contains Legerix.class. When Legerix is shaded into a consumer's fat jar, that is the consumer's jar — so Legerix extracts everything under <tier>/ from it, including natives Legerix never shipped, and then loads and asserts against those files as if they were its own bundled payload.
This is not the classpath-walk mechanism I described in #20 and withdrew — that was wrong, and getCodeSource() is a single location. The point here is the opposite: getCodeSource() does not prevent co-bundling. It is what makes it happen.
Mechanism
extractAllFromResourceDir resolves Legerix.class.getProtectionDomain().getCodeSource().getLocation() (Legerix.java:473) and extracts every entry under the resolved tier directory, rather than the specific files in librariesFor(). It has no way to distinguish "a native Legerix published" from "a native that happens to sit under the same directory name in the same jar".
Evidence — three hosts, byte-exact
macOS (arm64). oculixide-4.0.0-complete-mac.jar shades Legerix — it contains io/github/julienmerconsulting/legerix/Legerix.class and META-INF/maven/io.github.oculix-org/legerix/ — and carries its own darwin-aarch64/:
23,988,664 darwin-aarch64/libopencv_java4100.dylib
2,911,736 darwin-aarch64/libtesseract.5.dylib
2,510,824 darwin-aarch64/libleptonica.6.dylib
The resulting cache on that machine:
~/.cache/legerix/5.5.0/darwin-aarch64/
23,988,664 libopencv_java4100.dylib <- byte-exact match, and not a Legerix artifact
2,911,736 libtesseract.5.dylib
2,510,824 libleptonica.6.dylib
~/.cache/legerix/5.5.0-8/darwin-aarch64/
2,911,736 libtesseract.5.dylib
2,510,824 libleptonica.6.dylib
Linux (x86-64). Same shape, independently: oculixide-4.0.0-complete-lux.jar shades Legerix and carries linux-x86-64/libopencv_java4100.so (66,800,824), libleptonica.so.6 (3,134,816), libtesseract.so.5 (4,204,424). ~/.cache/legerix/5.5.0-8/linux-x86-64/ holds those same three at those same sizes.
OpenCV is not in librariesFor(), not in publish-maven-central.yml, and not among the release assets. It reaches Legerix's cache purely because it shares a directory name inside the consumer's jar.
Why it matters
1. The bundled-version guarantee is weaker than it looks. assertBundledTesseract() verifies the file Legerix extracted, by absolute path. When the code source is a consumer jar, that file is the consumer's copy. Compare the tier's payloads:
| file |
in the OculiX fat jar |
in Legerix's own v5.5.0-9-DO-NOT-USE release |
libtesseract.5-darwin-aarch64 |
2,911,736 |
2,929,824 |
libleptonica.6-darwin-aarch64 |
2,510,824 |
2,528,912 |
Different bytes. Part of that is simply version skew — the fat jar embeds 5.5.0-8. The structural point stands regardless: what Legerix loads, and what its own assertion validates, is whatever the consumer put under that directory name — and Legerix cannot tell the difference. The check confirms internal consistency, not provenance.
2. Disk. ~24 MB of unrelated native per cache key on macOS, ~67 MB on Linux, duplicated for every version key that accumulates. Two keys are already present on the host above.
3. It compounds the cache-key behaviour. extractIfMissing skips files that already exist, so a tier directory keyed on a bare version can end up holding the union of whatever several different code sources contributed over time. That makes cache contents hard to reason about after the fact — we hit exactly this ambiguity while investigating #20 and could not resolve it.
What we did not establish
- No malfunction is demonstrated. We have not shown a wrong-library load causing incorrect behaviour. The claim is about provenance and the strength of the guarantee, not a reproduced failure.
- We have not checked whether other consumers in the wild shade Legerix this way; our evidence is OculiX on two platforms.
- Whether this is worth changing is your call — shading is a legitimate consumer choice, and a library cannot fully control how it is repackaged.
Possible directions, offered tentatively
- Extract only the entries named in
librariesFor() (plus tessdata) rather than everything under the tier directory. That alone would have kept OpenCV out.
- Or verify extracted names against an expected manifest and log — rather than fail on — anything unexpected, which would also have surfaced this years earlier.
Reported from the #20 investigation. Happy to test any patch across macOS arm64, Linux x86-64 (both tiers) and native Windows.
Consumer-side detection, for reference
OculiX now detects this from its own side rather than waiting on a fix here — oculix-org/Oculix#463. It compares Legerix's code source against its own: identical means Legerix extracted from the consumer's jar, which is exact rather than heuristic. That is only detection, though; the extraction behaviour itself is Legerix's to change.
Legerix extracts a co-bundling consumer's natives into its own cache, and validates them as its own
Summary.
extractAllFromResourceDirreads from the jar that containsLegerix.class. When Legerix is shaded into a consumer's fat jar, that is the consumer's jar — so Legerix extracts everything under<tier>/from it, including natives Legerix never shipped, and then loads and asserts against those files as if they were its own bundled payload.This is not the classpath-walk mechanism I described in #20 and withdrew — that was wrong, and
getCodeSource()is a single location. The point here is the opposite:getCodeSource()does not prevent co-bundling. It is what makes it happen.Mechanism
extractAllFromResourceDirresolvesLegerix.class.getProtectionDomain().getCodeSource().getLocation()(Legerix.java:473) and extracts every entry under the resolved tier directory, rather than the specific files inlibrariesFor(). It has no way to distinguish "a native Legerix published" from "a native that happens to sit under the same directory name in the same jar".Evidence — three hosts, byte-exact
macOS (arm64).
oculixide-4.0.0-complete-mac.jarshades Legerix — it containsio/github/julienmerconsulting/legerix/Legerix.classandMETA-INF/maven/io.github.oculix-org/legerix/— and carries its owndarwin-aarch64/:The resulting cache on that machine:
Linux (x86-64). Same shape, independently:
oculixide-4.0.0-complete-lux.jarshades Legerix and carrieslinux-x86-64/libopencv_java4100.so(66,800,824),libleptonica.so.6(3,134,816),libtesseract.so.5(4,204,424).~/.cache/legerix/5.5.0-8/linux-x86-64/holds those same three at those same sizes.OpenCV is not in
librariesFor(), not inpublish-maven-central.yml, and not among the release assets. It reaches Legerix's cache purely because it shares a directory name inside the consumer's jar.Why it matters
1. The bundled-version guarantee is weaker than it looks.
assertBundledTesseract()verifies the file Legerix extracted, by absolute path. When the code source is a consumer jar, that file is the consumer's copy. Compare the tier's payloads:v5.5.0-9-DO-NOT-USEreleaselibtesseract.5-darwin-aarch64libleptonica.6-darwin-aarch64Different bytes. Part of that is simply version skew — the fat jar embeds
5.5.0-8. The structural point stands regardless: what Legerix loads, and what its own assertion validates, is whatever the consumer put under that directory name — and Legerix cannot tell the difference. The check confirms internal consistency, not provenance.2. Disk. ~24 MB of unrelated native per cache key on macOS, ~67 MB on Linux, duplicated for every version key that accumulates. Two keys are already present on the host above.
3. It compounds the cache-key behaviour.
extractIfMissingskips files that already exist, so a tier directory keyed on a bare version can end up holding the union of whatever several different code sources contributed over time. That makes cache contents hard to reason about after the fact — we hit exactly this ambiguity while investigating #20 and could not resolve it.What we did not establish
Possible directions, offered tentatively
librariesFor()(plustessdata) rather than everything under the tier directory. That alone would have kept OpenCV out.Reported from the #20 investigation. Happy to test any patch across macOS arm64, Linux x86-64 (both tiers) and native Windows.
Consumer-side detection, for reference
OculiX now detects this from its own side rather than waiting on a fix here — oculix-org/Oculix#463. It compares Legerix's code source against its own: identical means Legerix extracted from the consumer's jar, which is exact rather than heuristic. That is only detection, though; the extraction behaviour itself is Legerix's to change.