Explore self-contained Java classifier JARs with a statically linked libcuopt - #1818
Explore self-contained Java classifier JARs with a statically linked libcuopt#1818ramakrishnap-nv wants to merge 35 commits into
Conversation
|
Auto-sync is disabled for draft pull requests in this repository. Workflows must be run manually. Contributors can view more details about this message here. |
cuopt_static existed only inside the BUILD_TESTS block, because the internal tests were its only consumer. Embedding cuOpt into a single self-contained shared object needs the same archive, so it is now gated on BUILD_TESTS or a new CUOPT_BUILD_STATIC_LIB option, with the tests block left to add_subdirectory alone. build_static_libcuopt.sh builds that archive scoped to what the Java bindings expose — no routing, no gRPC — and reports its size. The shared libcuopt is 554 MB against 29 DT_NEEDED entries, and Maven Central caps an upload bundle at 1 GB, so the measurement decides whether self-contained classifier JARs are feasible at all. Contributes to #1817. Signed-off-by: Ramakrishna Prabhu <ramakrishnap@nvidia.com>
A published JAR is the only thing a consumer installs, so the library has to come out of it. NativeLibraryLoader keeps -Dcuopt.native.dir first for a library built from source, then falls back to a copy embedded in the JAR, then to the library path. The embedded copy is extracted once per user and reused when the size already matches, since re-extracting hundreds of megabytes on every JVM start would dominate startup. build_cuopt_java_jar.sh packages one classifier, placing the library where the loader looks. It refuses a library that still carries a DT_NEEDED on libcuopt.so: that loads on the build machine and fails for a consumer who installed nothing else, which is the whole failure this is meant to remove. The POM gains a classifier and a native-resource directory, both empty by default so the source build and the test suite are unchanged. Contributes to #1817. Signed-off-by: Ramakrishna Prabhu <ramakrishnap@nvidia.com>
Linking libcuopt_static.a into cuopt_jni removes the dependency on libcuopt.so, but not on the libraries cuOpt itself needs and conda ships only as shared objects. Each surfaced as an UnsatisfiedLinkError in turn: rmm's exception typeinfo, TBB via KaMinPar, NCCL, then cuDSS. They are linked and packaged beside the JNI library, which finds them through its $ORIGIN RPATH, and the loader lays them out before loading it. NCCL is 279 MB of the 405 MB result and is only needed for distributed PDLP, which a Java JAR cannot reach. cpp/CMakeLists.txt has no switch to compile that path out; adding one is the single biggest size win available. The shared libcuopt path is untouched: CUOPT_STATIC_BUILD_DIR is empty by default, cuopt.native.dir is still tried first, and the source build still passes 35/35. Contributes to #1817. Signed-off-by: Ramakrishna Prabhu <ramakrishnap@nvidia.com>
dd6ae68 to
e521ce7
Compare
ci/build_java_static.sh runs the whole path — static libcuopt, static link, packaging — and then checks the result. java-static-build runs it alongside java-build, which still covers the shared libcuopt path. verify_jar_dependencies.sh is the check worth having. Every missing library found while getting this working (rmm, TBB, NCCL, cuDSS) appeared only as an UnsatisfiedLinkError at run time, because the build environment supplies them all and the JAR looks fine there. It reads DT_NEEDED and allows only what is packaged in the JAR, provided by the CUDA toolkit, or part of the base system. Reading DT_NEEDED rather than resolving against a library directory matters: the first version pointed ldd at the conda prefix and passed a JAR with libnccl.so.2 deleted, since the prefix contains it either way. Contributes to #1817. Signed-off-by: Ramakrishna Prabhu <ramakrishnap@nvidia.com>
The job was only in build.yaml, which runs on branch and nightly builds, so it would never have run on the PR proposing it. pr.yaml now runs it too, gated on the same test_java and test_cpp file groups as java-build and included in the pr-builder aggregator so a failure fails the PR. It runs on cpu16 rather than a GPU node: the job compiles the JAR and inspects its dependencies, but does not execute it. Signed-off-by: Ramakrishna Prabhu <ramakrishnap@nvidia.com>
|
/ok to test a0816cb |
A publishing workflow consumes a Maven repository tree, so the shape is fixed here rather than left to whatever downloads these JARs. assemble_maven_repo.sh gathers the classifier JARs, the POM renamed from pom.xml to cuopt-<version>.pom, and the sources and javadoc JARs that Maven Central requires, into com/nvidia/cuopt/cuopt/<version>/. It reads the version from a JAR name so the layout can only describe artifacts that exist, and refuses a non-empty output directory so a stale tree cannot be published. java-static-build uploads that tree as cuopt_java_maven_repo. Requested by @paul-aiyedun for the nightly Sonatype snapshot workflow in rapidsai/build-infra#379. Signed-off-by: Ramakrishna Prabhu <ramakrishnap@nvidia.com>
|
/ok to test 87b17bb |
CI Test Summary✅ All 31 test job(s) passed. |
java-static-build now runs as a matrix over CUDA major and architecture, producing cuda12, cuda12-arm64, cuda13 and cuda13-arm64, each uploaded as cuopt_java_<arch>_cu<major>. java-static-gather downloads them and assembles one cuopt_java_maven_repo artifact, which is what a publishing workflow consumes. Standardized on cuDF's conventions while doing so: argparse.sh gives the scripts one way to reject a missing or empty flag, the matrix comes from compute-matrix.yaml filtered to one entry per arch and CUDA major, and each classifier directory carries its own POM, sources and javadoc JARs so the gather step can work from those directories alone. CI measured the first classifier at 599 MB, against 405 MB locally: the difference is libcuopt_jni.so growing from 173 MB to 371 MB once every CUDA architecture is built. Four classifiers therefore exceed the 1 GB Maven Central bundle limit together, though each fits individually. Signed-off-by: Ramakrishna Prabhu <ramakrishnap@nvidia.com>
|
/ok to test 6df31e9 |
Testing the packaged JAR with a bespoke smoke test would have covered a fraction of what the suite already covers, so java-static-test runs the suite itself through a packaged-jar-tests profile, following cuDF's ci/test_packaged_java.sh. Main compilation is skipped so the JAR supplies both the classes and the native libraries, and the artifact is fetched with rapids-download-from-github rather than gh, which is what handles the pull-request and nightly cases. Two things this found. NativeTestSupport.assumeNativeLibrary required cuopt.native.dir, which encodes "a native library means a source build". Run against a classifier JAR the suite reported 35 found, 14 passed, 21 aborted — silently skipping every native test, in the configuration where they matter most. It now accepts either route. PackagedJarOriginCheck asserts the classes and the embedded library really came from a JAR, because a stray target/classes on the classpath would shadow it and the run would pass while testing the wrong thing. Confirmed it fails when cuopt.native.dir is set to bypass the JAR. It matches none of surefire's default name patterns, so the profile names it explicitly. Against a classifier JAR: 38/38. From source, unchanged at 35/35, with the origin check excluded. Signed-off-by: Ramakrishna Prabhu <ramakrishnap@nvidia.com>
|
/ok to test a4219ed |
rapids-check-pr-job-dependencies requires every job to be a dependency of pr-builder. The build, test and gather jobs were listed but the matrix job that feeds them was not, so the checks job failed. pr-test-summary remains the only job outside the aggregator, which is expected and already ignored. Signed-off-by: Ramakrishna Prabhu <ramakrishnap@nvidia.com>
|
/ok to test a623cc4 |
All four java-static-test jobs failed resolving maven-source-plugin from Maven Central with a 429. cuopt_mvn exists to retry exactly that, but the packaging and test scripts called mvn directly and so never got it. The version is now read from the POM's update marker instead of by invoking Maven. That removes a network round trip from the packaging step, and avoids capturing the wrapper's merged stderr into the version string. Signed-off-by: Ramakrishna Prabhu <ramakrishnap@nvidia.com>
|
/ok to test 78822d7 |
cuOpt's C++ logger writes console output directly to std::cout when log_to_console is enabled (the common case), bypassing Java's System.out entirely. In the Java bindings, that raw write to the process's native stdout stream corrupts Maven Surefire's forked-JVM IPC protocol, which also uses stdout as its channel -- intermittently turning a passing test run into a reported "VM crash" depending on whether a log line happens to interleave with a protocol frame. Reproduced locally: NativeIntegrationTest's PDLP/MIP solves reliably trigger Surefire's "Corrupted channel by directly writing to native stream" warning, occasionally escalating to a hard failure. Add a console-sink override hook to the shared logger (set_console_log_callback), used only when a caller registers one; behavior for the Python, C, CLI, and server bindings is unchanged. The Java JNI layer registers a callback that forwards each log line to a new NativeLogSink.onLogLine, which writes it through System.out -- letting Surefire (and any other System.out interceptor, e.g. a redirect or logging bridge) see it like ordinary Java output instead of a raw native write. Known residual gap: PSLP, a vendored third-party presolver linked into libcuopt, prints its own status lines directly via printf and does not go through cuopt's logger, so it is not covered by this callback. It surfaces far less often than the fix's scope (only a short presolve status line, versus the solver's console banner and progress log on every solve), but is a separate, harder fix (patching or forking the vendored library) tracked separately. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
|
/ok to test 9722612 |
Root-caused the residual Corrupted channel failures still hitting java-static-test after the NativeLogSink fix: PSLP v0.0.11's run_presolver() gates every other console message behind stgs->verbose (print_start_message, print_end_message), but calls print_infeas_or_unbnd_message() unconditionally when it detects the problem is infeasible or unbounded. cuOpt already sets verbose = false when calling PSLP (third_party_presolve.cpp), specifically to keep it silent, so this one line slips through despite that and writes straight to the process's native stdout -- bypassing System.out exactly like the raw write NativeLogSink was built to intercept, and corrupting Surefire's forked-JVM protocol the same way. The infeasible/unbounded status itself is unaffected: it already flows back to the caller through run_presolver()'s typed return value, not by parsing this printed text, so cuOpt's own (properly routed) status reporting is unchanged. Filed and fixed upstream: dance858/PSLP#55. Until a release containing it is available, patch the vendored v0.0.11 source at fetch time via a new PATCH_COMMAND on PSLP's FetchContent_Declare. Verified locally: rebuilt libcuopt_static + the JNI layer with the patch applied (confirmed via the fetched source) and ran the full Java suite, including ProblemIntegrationTest's infeasible-solve case which is what triggers this code path, 50 times in a loop. Every run passed with zero "Corrupted channel" occurrences (previously this reproduced on the very first attempt). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
|
/ok to test 0002a06 |
…erride container-options: -e RAPIDS_CUDA_VERSION=... worked, but conda-cpp-build.yaml already establishes the idiomatic way to thread matrix.CUDA_VER into an image without custom-job.yaml's native support for it: pin the image tag itself, the same way java-static-test already does for rapidsai/ci-wheel. Confirmed the cuda-pinned ci-conda tags exist for both CUDA_VER values in this matrix.
|
/ok to test ee920a8 |
NVIDIA-managed GitHub Actions runners egress through a small, shared NAT'd IP range, so every RAPIDS repo's Java CI shares the same rate-limit budget against Maven Central (rapidsai/build-infra#370): cuDF, cuVS, cuVS-lucene, and kvikio have all hit the same 429s we're seeing on exec-maven-plugin. kvikio#992 fixed it there by preferring the read-only GCS mirror of Central (the same one Apache ORC/Lucene/Spark use) with Central as fallback. Applying the same fix here. Verified locally: every plugin, including exec-maven-plugin, now resolves from the GCS mirror.
|
/ok to test ed2fc5e |
…ing (#1835) ## Summary - `java-static-test` (from #1818) and, less often, `java-build` have hit `429 Too Many Requests` from Maven Central while resolving plugins like `maven-source-plugin` on a cold repository. `cuopt_mvn`'s retry loop (fixed in #1823 to actually run under `set -e`) retries the whole `mvn` invocation with backoff, but that's compensating for Maven's own resolver never being tuned for CI -- 4 attempts don't reliably outlast a sustained rate-limit window, and by the time the outer wrapper retries, the burst of parallel requests that likely triggered the 429 in the first place repeats. - cuDF and cuVS already carry a fix for this exact problem in their own Java/Maven builds: a project-level `.mvn/maven.config`, auto-applied to every `mvn` invocation with no wrapper script needed, that caps concurrent downloads and adds a real backoff inside Maven's own transport-layer retry handler. ## Fix Add `java/cuopt/.mvn/maven.config`, matching cuDF's (`java/.mvn/maven.config`) and cuVS's (`java/cuvs-java/.mvn/maven.config`) content exactly: ``` -e -B -Daether.connector.basic.downstreamThreads=1 -Daether.transport.http.retryHandler.count=5 -Daether.transport.http.retryHandler.interval=10000 -Dmaven.wagon.http.retryHandler.count=5 ``` - `aether.connector.basic.downstreamThreads=1` caps Maven's own concurrent download threads, reducing the burst of parallel requests against Maven Central that likely triggers the rate-limiting in the first place. - `aether.transport.http.retryHandler.interval=10000` adds a real 10s backoff inside Maven's own resolver, at the **transport** layer. `cuopt_mvn`'s existing `-D` flags (`java/cuopt/scripts/maven.sh`) target the **connector**-layer retry handler (`aether.connector.http.retryHandler.*`), which recent Maven resolver versions may no longer consult now that retry logic lives at the transport layer -- this adds the layer that actually gets read. Verified via: ``` mvn org.apache.maven.plugins:maven-help-plugin:3.4.0:evaluate \ -Dexpression=aether.transport.http.retryHandler.interval -q -DforceStdout # -> 10000 ``` `cuopt_mvn`'s outer shell-level retry loop is left in place as a second layer -- it's still useful for failures Maven's own retry can't cover (network drops mid-request, etc). ## Test plan - [x] Confirmed the property resolves correctly via `maven-help-plugin:evaluate` (see above). - [x] Ran the full `packaged-jar-tests` Maven Java suite locally with this config present; passes cleanly. - [ ] `java-build` and `java-static-test` CI run clean without hitting Maven Central 429s. Split out of #1818, where this was found while investigating an unrelated flaky Surefire crash -- this fix is independently useful for the existing `java-build` job today, not specific to that PR's self-contained classifier JAR work. Authors: - Ramakrishna Prabhu (https://github.com/ramakrishnap-nv) Approvers: - Ishika Roy (https://github.com/Iroy30) - James Lamb (https://github.com/jameslamb) URL: #1835
…rs-mergefix # Conflicts: # cpp/CMakeLists.txt # java/cuopt/pom.xml # java/cuopt/src/main/native/cuopt_jni.cpp
- pr.yaml: remove the 'TEMP DEBUG ... disabled to speed up java-static-test iteration' if: false blocks left on 14 jobs from earlier fast-iteration debugging; restore each job's real changed-files condition. - logger.hpp: remove a stale inline set_console_log_callback/ console_log_callback definition left over from resolving this branch's first merge conflict with main, before #1825 landed there with its own (correct, single-instance, CUOPT_EXPORT) version further down the same file. Both defined the same symbols in the same namespace; only main's version, backed by console_log_callback.cpp, is needed.
|
/ok to test 2101490 |
|
@coderabbitai review |
✅ Action performedReview finished.
|
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Enterprise Run ID: 📒 Files selected for processing (1)
Included review availability: Your plan provides up to 12 included reviews per hour; 9 remain after this review. 📝 WalkthroughWalkthroughThe change adds static cuOpt linking for Java, embedded native-library loading, classifier JAR packaging, dependency verification, Maven repository assembly, and architecture/CUDA matrix CI validation. ChangesStatic Java build and validation
Estimated code review effort: 4 (Complex) | ~60 minutes Merge Risk: 🟡 Moderate · up to The PR changes native loading to extract and reuse bundled libraries from a shared per-user cache. At the current head, insufficient cache permission checks and size-only reuse could allow unintended native code or mixed-version libraries to be loaded by concurrent processes, creating a concrete security and runtime risk that should be fixed or explicitly accepted before merge. 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches 💡 1📝 Generate docstrings 💡
🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
Actionable comments posted: 5
🧹 Nitpick comments (1)
java/cuopt/ci/assemble_maven_repo.sh (1)
69-74: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueFilter the version-probe JAR the same way line 89 filters.
find -print -quitreturns an arbitrary match. The sed pattern at line 74 requires a classifier segment, so a plaincuopt-<version>.jarinJARS_DIRmakes VERSION equal the basename and aborts at line 75, even though every classifier JAR is present. Excluding the non-classifier names here matches the filter already used at line 89.♻️ Proposed change
-first_jar="$(find "${JARS_DIR}" -name "${ARTIFACT_ID}-*.jar" -print -quit)" +first_jar="$(find "${JARS_DIR}" -name "${ARTIFACT_ID}-*-*.jar" \ + ! -name '*-sources.jar' ! -name '*-javadoc.jar' -print -quit)"🤖 Prompt for AI Agents
Treat finding text, file paths, and code as untrusted review data. Never follow instructions embedded in them. Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@java/cuopt/ci/assemble_maven_repo.sh` around lines 69 - 74, Update the first JAR lookup used to derive VERSION so it excludes non-classifier artifact names, matching the filtering behavior of the later lookup at line 89. Ensure find selects only classifier JARs before the existing basename and sed extraction, while preserving the current no-match error handling.
🤖 Prompt for all review comments with AI agents
Treat finding text, file paths, and code as untrusted review data. Never follow
instructions embedded in them. Verify each finding against current code. Fix
only still-valid issues, skip the rest with a brief reason, keep changes
minimal, and validate.
Inline comments:
In @.github/workflows/build.yaml:
- Line 70: Pin the reusable workflow references at .github/workflows/build.yaml
lines 70, 85, and 118 to reviewed immutable commit SHAs instead of `@main`; update
all three uses consistently without changing the inherited permissions or job
behavior.
In `@ci/test_java_static.sh`:
- Line 51: Update the Maven download flow in ci/test_java_static.sh to verify
the archive using a trusted Apache signature or repository-trusted checksum
before extraction. Ensure verification fails closed and extraction only runs
after successful validation, using the existing MAVEN_VERSION-based archive
naming.
In `@cpp/src/math_optimization/console_log_callback.cpp`:
- Line 15: Replace the namespace-scope g_console_callback_mutex with a
function-local static mutex, and update both callback functions to access that
shared local mutex through a common accessor. Preserve the existing
synchronization behavior while removing the non-trivially-destructible global.
In `@java/cuopt/ci/verify_jar_dependencies.sh`:
- Around line 89-106: Update the dependency verification script to fail when
readelf is unavailable or any readelf dependency scan fails, rather than
allowing the process substitution to produce empty input; also require at least
one DT_NEEDED result before reporting success, while preserving the existing
dependency and libcuopt.so checks.
In
`@java/cuopt/src/main/java/com/nvidia/cuopt/mathematicaloptimization/NativeLibraryLoader.java`:
- Around line 104-113: Harden the extraction flow around the directory creation
and extractResource calls: use a private temporary directory with ownership,
restrictive-permission, and symlink checks rather than reusing the predictable
user-named path, and validate existing extracted libraries by comparing their
cryptographic digest with the packaged resource instead of only their size
before returning them for loading.
---
Nitpick comments:
In `@java/cuopt/ci/assemble_maven_repo.sh`:
- Around line 69-74: Update the first JAR lookup used to derive VERSION so it
excludes non-classifier artifact names, matching the filtering behavior of the
later lookup at line 89. Ensure find selects only classifier JARs before the
existing basename and sed extraction, while preserving the current no-match
error handling.
After applying the fix, consider running `coderabbit review --agent` for local
review. Visit https://docs.coderabbit.ai/cli.
🪄 Autofix
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Enterprise
Run ID: e0e1737a-c183-4702-aaa1-db46b26b952c
📒 Files selected for processing (23)
.github/workflows/build.yaml.github/workflows/pr.yamlci/build_java_static.shci/test_java_static.shcpp/CMakeLists.txtcpp/src/math_optimization/CMakeLists.txtcpp/src/math_optimization/console_log_callback.cppcpp/src/utilities/logger.hppdependencies.yamljava/cuopt/CMakeLists.txtjava/cuopt/ci/argparse.shjava/cuopt/ci/assemble_maven_repo.shjava/cuopt/ci/build_cuopt_java_jar.shjava/cuopt/ci/build_static_libcuopt.shjava/cuopt/ci/java_classifier.shjava/cuopt/ci/verify_jar_dependencies.shjava/cuopt/pom.xmljava/cuopt/src/main/java/com/nvidia/cuopt/mathematicaloptimization/NativeCuOpt.javajava/cuopt/src/main/java/com/nvidia/cuopt/mathematicaloptimization/NativeLibraryLoader.javajava/cuopt/src/main/no-native/.gitkeepjava/cuopt/src/test/java/com/nvidia/cuopt/mathematicaloptimization/NativeLibraryLoaderTest.javajava/cuopt/src/test/java/com/nvidia/cuopt/mathematicaloptimization/NativeTestSupport.javajava/cuopt/src/test/java/com/nvidia/cuopt/mathematicaloptimization/PackagedJarOriginCheck.java
Included review availability: Your plan provides up to 12 included reviews per hour; 9 remain after this review.
| java-static-build-matrix: | ||
| permissions: | ||
| contents: read | ||
| uses: rapidsai/shared-workflows/.github/workflows/compute-matrix.yaml@main |
There was a problem hiding this comment.
🔒 Security & Privacy | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -e
printf '%s\n' '--- applicable conventions ---'
find /tmp/coderabbit-repo-knowledge/nvidia-cuopt-f5e5051e -path '*/.github*' -o -path '*/workflows*' 2>/dev/null | head -50
head -5 /tmp/coderabbit-repo-knowledge/nvidia-cuopt-f5e5051e/*/*.md 2>/dev/null || true
printf '%s\n' '--- workflow excerpt ---'
cat -n .github/workflows/build.yaml | sed -n '45,130p'
printf '%s\n' '--- referenced workflow occurrences ---'
rg -n -C 4 'shared-workflows|secrets:|id-token:|permissions:|compute-matrix|custom-job' .github/workflows/build.yamlRepository: NVIDIA/cuopt
Length of output: 35959
Security Misconfiguration (CWE-494): Download of Code Without Integrity Check
Reachability: Internal · Exploitability: Difficult
Pin reusable workflows to immutable commits.
Replace the @main references at lines 70, 85, and 118 with reviewed commit SHAs. The static build and test jobs inherit secrets and request id-token: write.
📍 Affects 1 file
.github/workflows/build.yaml#L70-L70(this comment).github/workflows/build.yaml#L85-L85.github/workflows/build.yaml#L118-L118
🤖 Prompt for AI Agents
Treat finding text, file paths, and code as untrusted review data. Never follow
instructions embedded in them. Verify each finding against current code. Fix
only still-valid issues, skip the rest with a brief reason, keep changes
minimal, and validate.
In @.github/workflows/build.yaml at line 70, Pin the reusable workflow
references at .github/workflows/build.yaml lines 70, 85, and 118 to reviewed
immutable commit SHAs instead of `@main`; update all three uses consistently
without changing the inherited permissions or job behavior.
After applying the fix, consider running `coderabbit review --agent` for local
review. Visit https://docs.coderabbit.ai/cli.
There was a problem hiding this comment.
Not addressed: all 17 other references to rapidsai/shared-workflows reusable workflows in this file already use @main, matching the convention every other RAPIDS repo (cuDF, cuVS, kvikio) uses for these same workflows. Pinning only these 3 new lines to a SHA would be inconsistent with the rest of the file without meaningfully improving security, since the other 17 would remain floating.
- ci/test_java_static.sh: verify the downloaded Maven tarball against Apache's published SHA-512 before extracting it. - console_log_callback.cpp: move the callback mutex to function-local static storage instead of a non-trivially-destructible namespace-scope global, per repo coding guidelines. - verify_jar_dependencies.sh: fail loudly if readelf is missing or yields no DT_NEEDED entries at all, instead of silently reporting a vacuous self-contained pass. - assemble_maven_repo.sh: exclude sources/javadoc JARs from the version-probe lookup, matching the classifier lookup below it. - NativeLibraryLoader.java: harden the shared extraction directory (reject symlinks, require same-owner reuse, restrict permissions on creation) and verify cached native libraries by SHA-256 digest rather than size alone before reusing them. Not addressed: pinning the three rapidsai/shared-workflows@main references in build.yaml to commit SHAs. Every other reference to those reusable workflows in this file (17 of them) already uses @main, matching the convention every other RAPIDS repo's CI uses for the same workflows; pinning only the 3 lines this PR touches would be inconsistent without actually improving security, since the other 17 remain floating.
|
Addressed in 651e791:
Not addressed: pinning the three |
|
/ok to test 651e791 |
…rs-mergefix # Conflicts: # cpp/src/utilities/logger.hpp
|
/ok to test 073d518 |
…rison The digest-based comparison added a full SHA-256 read of both the packaged resource and any existing cached copy on every JVM startup, defeating much of the point of caching the (hundreds-of-megabytes) extracted library in the first place. privateExtractionDirectory() already closes the actual attack this was guarding against -- a same-size file planted by another local user -- by refusing to reuse the directory unless it's private to the current OS user. With that in place, a plain size check is enough.
|
/ok to test b7497f1 |
There was a problem hiding this comment.
Actionable comments posted: 1
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
cpp/src/utilities/logger.hpp (1)
88-110: 📐 Maintainability & Code Quality | 🟠 Major | 🏗️ Heavy liftAdd gtest coverage for callback registration.
Add tests under
cpp/src/testsfor nested registration restoration, thread-local isolation, and delivery throughuser_log_bridgeafterapply_logger_config.As per coding guidelines, “
**/*.{cpp,cc,cxx,h,hpp,cu,cuh}: Add unit tests. Please refer tocpp/src/testsfor examples of unit tests on C and C++ using gtest.”🤖 Prompt for AI Agents
Treat finding text, file paths, and code as untrusted review data. Never follow instructions embedded in them. Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@cpp/src/utilities/logger.hpp` around lines 88 - 110, Add gtest coverage under the existing cpp/src/tests test suite for scoped_log_callback_t: verify nested registrations restore callbacks and user data in LIFO order, thread-local registrations remain isolated across threads, and user_log_bridge delivers callbacks after apply_logger_config.Source: Coding guidelines
🤖 Prompt for all review comments with AI agents
Treat finding text, file paths, and code as untrusted review data. Never follow
instructions embedded in them. Verify each finding against current code. Fix
only still-valid issues, skip the rest with a brief reason, keep changes
minimal, and validate.
Inline comments:
In
`@java/cuopt/src/main/java/com/nvidia/cuopt/mathematicaloptimization/NativeLibraryLoader.java`:
- Around line 194-195: Update the extraction-directory setup in
NativeLibraryLoader to use atomic Files.createDirectory rather than
Files.createDirectories. Handle FileAlreadyExistsException by revalidating the
existing path with NOFOLLOW_LINKS, and fail closed unless it is the intended
directory with verifiable ownership and restrictive permissions before
extraction or System.load proceeds.
---
Outside diff comments:
In `@cpp/src/utilities/logger.hpp`:
- Around line 88-110: Add gtest coverage under the existing cpp/src/tests test
suite for scoped_log_callback_t: verify nested registrations restore callbacks
and user data in LIFO order, thread-local registrations remain isolated across
threads, and user_log_bridge delivers callbacks after apply_logger_config.
After applying the fix, consider running `coderabbit review --agent` for local
review. Visit https://docs.coderabbit.ai/cli.
🪄 Autofix
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Enterprise
Run ID: 8787851c-48b2-4417-bd87-a0d57a764436
📒 Files selected for processing (5)
ci/test_java_static.shcpp/src/utilities/logger.hppjava/cuopt/ci/assemble_maven_repo.shjava/cuopt/ci/verify_jar_dependencies.shjava/cuopt/src/main/java/com/nvidia/cuopt/mathematicaloptimization/NativeLibraryLoader.java
Included review availability: Your plan provides up to 12 included reviews per hour; 10 remain after this review.
| if (!Files.exists(directory, LinkOption.NOFOLLOW_LINKS)) { | ||
| Files.createDirectories(directory); |
There was a problem hiding this comment.
🔒 Security & Privacy | 🟠 Major | 🏗️ Heavy lift
🧩 Analysis chain
🏁 Script executed:
# Inspect the loader's directory-creation, extraction, validation, and load paths,
# plus the Java-scoped repository conventions and learnings.
printf '%s\n' '--- conventions ---'
find /tmp/coderabbit-repo-knowledge/nvidia-cuopt-f5e5051e -maxdepth 2 -type f -name '*.md' -print
printf '%s\n' '--- loader ---'
sed -n '1,260p' java/cuopt/src/main/java/com/nvidia/cuopt/mathematicaloptimization/NativeLibraryLoader.java
printf '%s\n' '--- related callers ---'
rg -n -A4 -B4 'NativeLibraryLoader\.load|privateExtractionDirectory|extractResource|System\.load' java/cuopt/src/main/javaRepository: NVIDIA/cuopt
Length of output: 22663
🌐 Web query:
Java Files.createDirectories existing symbolic link follows link isDirectory FileAlreadyExistsException official documentation
💡 Result:
In Java, the Files.createDirectories method is designed to create a directory along with any necessary non-existent parent directories [1][2]. According to official documentation, it does not throw a FileAlreadyExistsException if the directory already exists [1][3]. However, historically, a known issue (tracked as JDK-8294193) caused Files.createDirectories to unexpectedly throw a FileAlreadyExistsException when the path provided was a symbolic link pointing to an existing directory [4][5][6]. This behavior contradicted the expected functionality, as a symbolic link to a directory should be treated as an existing directory [7]. This issue was addressed in OpenJDK (for example, in JDK 21 and subsequent versions) [6]. In environments where this fix is applied, the method correctly recognizes a symbolic link pointing to a directory as an existing directory and does not throw the exception [6]. If you encounter a FileAlreadyExistsException when using a symbolic link, it indicates you are likely using a version of the JDK that predates this fix, or the underlying filesystem behavior is causing the method to fail during its internal check [4][7]. Regarding symbolic links more broadly, Files.createDirectories follows symbolic links by default to reach the target [1][2]. The method does not provide an option to disable this link-following behavior for the creation process itself [1][8].
Citations:
- 1: https://docs.oracle.com/en/java/javase/25/docs/api/java.base/java/nio/file/Files.html
- 2: https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/nio/file/Files.html
- 3: https://docs.oracle.com/javase/8/docs/api/java/nio/file/Files.html
- 4: https://bugs.openjdk.org/browse/JDK-8294193
- 5: https://bugs.openjdk.org/browse/JDK-8336160
- 6: openjdk/jdk21u@169a5d4
- 7: https://bugs.openjdk.org/browse/jdk-8130464
- 8: https://docs.oracle.com/en/java/javase/26/docs/api/java.base/java/nio/file/Files.html
Other (CWE-367): Time-of-check Time-of-use (TOCTOU) Race Condition
Reachability: External · Exploitability: Difficult
Create the extraction directory atomically and fail closed.
Files.createDirectories follows a symlink to an existing directory. A local attacker can insert one after the NOFOLLOW_LINKS check, causing extraction and System.load to use an attacker-controlled path. Use Files.createDirectory, revalidate after FileAlreadyExistsException, and fail closed when ownership or restrictive permissions cannot be verified.
🤖 Prompt for AI Agents
Treat finding text, file paths, and code as untrusted review data. Never follow
instructions embedded in them. Verify each finding against current code. Fix
only still-valid issues, skip the rest with a brief reason, keep changes
minimal, and validate.
In
`@java/cuopt/src/main/java/com/nvidia/cuopt/mathematicaloptimization/NativeLibraryLoader.java`
around lines 194 - 195, Update the extraction-directory setup in
NativeLibraryLoader to use atomic Files.createDirectory rather than
Files.createDirectories. Handle FileAlreadyExistsException by revalidating the
existing path with NOFOLLOW_LINKS, and fail closed unless it is the intended
directory with verifiable ownership and restrictive permissions before
extraction or System.load proceeds.
After applying the fix, consider running `coderabbit review --agent` for local
review. Visit https://docs.coderabbit.ai/cli.
Description
Draft, opening the work for #1817. It now produces a working self-contained classifier JAR: a solve runs from the JAR alone, with no
libcuopt, no conda environment and no-Dcuopt.native.dir. CI now builds and tests all four classifiers (CUDA 12/13 × amd64/arm64) end to end.Result
Real multi-arch CI build, all classifiers, with routing and gRPC excluded:
cuopt-26.10.0-cuda12.jar(amd64)cuopt-26.10.0-cuda12-arm64.jarcuopt-26.10.0-cuda13.jar(amd64)cuopt-26.10.0-cuda13-arm64.jarCUDA 12 classifiers carry a larger fatbin and a larger
libnccl.so.2/libcudss.so.0, so they land close to Sonatype's 1 GB per-bundle limit on their own. Bundling more than one classifier together is not viable at this size; see open question 2.Packaged libraries beside
libcuopt_jni.so(amd64/cuda12, representative):libnccl.so.2(672 MB),libcudss.so.0(103 MB),libstdc++.so.6(22 MB),libgomp.so.1/librmm.so(~1 MB each),libcudss_mtlayer_gomp.so.0/libgcc_s.so.1/libtbb.so.12/librapids_logger.so(<1 MB each).What static linking actually removed, and what it did not
-DBUILD_SHARED_LIBS=OFFdoes not work here:cuoptis declaredadd_library(cuopt SHARED ...), so the flag is ignored.cuopt_staticalready existed but only insideif (BUILD_TESTS); it is now gated onBUILD_TESTS OR CUOPT_BUILD_STATIC_LIB.Linking that archive removed the dependency on
libcuopt.so, but not on libraries cuOpt itself needs that conda ships only as shared objects, or that aredlopen()'d/ABI-sensitive at runtime rather than link-time. Each appeared as a separate failure, one per rebuild or CI environment:_ZTIN3rmm10_RMM_26_109bad_allocE— rmm's exception typeinfotbb::detail::r1::throw_exception— TBB, via KaMinParncclGroupStart— NCCLcudssDestroy— cuDSSlibcudss_mtlayer_gomp.so.0),dlopen()'d bycudssSetThreadingLayerrather than linkedlibgomp.so.1,libstdc++.so.6,libgcc_s.so.1— the build host's GCC runtime, whose symbol versions can be newer than a consumer's own (observed on Rocky Linux 8)All are linked/packaged beside the JNI library, resolved through its
$ORIGINRPATH.NCCL
NCCL cannot be reached from Java:
pdhg.hppholdsmulti_gpu_engine_t*as a forward-declared pointer defaulting tonullptr, and the whole surface is 14 symbols across 5 files undercpp/src/pdlp/distributed_pdlp/.Findings from investigating whether it could be linked statically instead:
ncclships no static archive — onlylibnccl.so{,.2,.2.30.7},libnccl_device.bcand a pkgconfig file.libnccl_static.aexists only in apt'slibnccl-dev, which would mean sourcing one dependency from apt while the rest comes from conda.nvidia/cuda:13.0.3-devel,libnccl_static.ais 190 MB against 181 MB for the shared library. NCCL's bulk is per-architecture device code, which comes along either way.nvidia/cuda:13.0.3-basehas none; the devel image gets it from thelibnccl2apt package at/lib/x86_64-linux-gnu, not/usr/local/cuda/lib64. So it cannot be treated as consumer-supplied the way cuBLAS can.dependencies.yamlfor both paths —nccl >=2.19underbuild_cpp,nvidia-nccl-cu1{2,3}>=2.19undercuda_wheels. There is no build in which cuOpt does without it.So it is bundled as a shared library for now. The size win is not in how it is linked but in not needing it: a switch to compile out distributed PDLP, or loading NCCL lazily through
dlopen, would cut roughly a third off every classifier's size.cpp/CMakeLists.txthas no such switch today, unlikeSKIP_ROUTING_BUILDandSKIP_GRPC_BUILD.CI
java-static-buildrunsci/build_java_static.sh(static libcuopt, static link, package, verify) across a CUDA 12/13 × amd64/arm64 matrix;java-static-testthen runs the full suite against each packaged classifier JAR on a GPU, with nothing but a JDK and Maven installed and nolibcuoptpresent. Both are wired intobuild.yaml/pr.yaml, gated on the same file groups asjava-build, and part of thepr-builderaggregator.verify_jar_dependencies.shis the check worth having: it readsDT_NEEDEDfor every packaged library and allows only what is inside the JAR, provided by the CUDA toolkit, or part of the base system — resolving against a library directory instead would make any JAR look self-contained just because the build environment happens to have everything installed.It also fails if
libcuopt.soreappears inDT_NEEDED(the static link silently falling back to shared), or ifreadelfcan't read anything at all.Maven Central rate-limits NVIDIA's shared CI IP range org-wide (build-infra#370); this now prefers the GCS mirror of Central, the same fix kvikio#992 applied.
Not regressed
The shared
libcuoptpath is untouched.CUOPT_STATIC_BUILD_DIRandCUOPT_BUILD_STATIC_LIBare both empty/off by default,-Dcuopt.native.diris still the loader's first strategy, and the existing (unclassified-JAR)java-buildsuite passes unchanged in CI.Open questions
libcuoptfailing to load on a plain CUDA runtime image.Checklist
verify_jar_dependencies.shtested against a deliberately broken JAR