fix(cli): clear LD_PRELOAD after one-shot-token library loads#1232
fix(cli): clear LD_PRELOAD after one-shot-token library loads#1232
Conversation
The one-shot-token LD_PRELOAD library now unsets LD_PRELOAD and LD_LIBRARY_PATH from the environment after initialization. The library remains loaded in the current process's address space so getenv interception continues to work, but child processes no longer inherit these variables. This fixes Deno 2.x's scoped --allow-run permissions which reject spawning subprocesses when LD_PRELOAD is set in the environment. Fixes #1001 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
✅ Coverage Check PassedOverall Coverage
📁 Per-file Coverage Changes (1 files)
Coverage comparison generated by |
There was a problem hiding this comment.
Pull request overview
Updates the Rust one-shot-token LD_PRELOAD library to remove LD_PRELOAD and LD_LIBRARY_PATH from the process environment after initialization, aiming to avoid Deno 2.x scoped --allow-run failures while keeping getenv interception active in-process.
Changes:
- Call a new
clear_ld_preload()helper at the end of token-list initialization tounsetenv("LD_PRELOAD")andunsetenv("LD_LIBRARY_PATH"). - Add a Rust unit test asserting the environment variables are removed.
Comments suppressed due to low confidence (1)
containers/agent/one-shot-token/src/lib.rs:243
clear_ld_preload()unconditionally unsetsLD_LIBRARY_PATH. In this repo,containers/agent/entrypoint.shdeliberately setsLD_LIBRARY_PATHfor Java (AWF_JAVA_HOME/...) with a note that it is required forlibjli.so; clearing it inside the preloaded library means the running shell/workload will lose that configuration and any subsequentexecwill not see it. If the intent is only to satisfy Deno's--allow-runrestriction, consider makingLD_LIBRARY_PATHclearing opt-in (env flag) or narrowing it to cases where it was set specifically for the preload mechanism, to avoid breaking Java and other runtimes that rely on it.
unsafe {
let ld_preload = CString::new("LD_PRELOAD").unwrap();
libc::unsetenv(ld_preload.as_ptr());
let ld_library_path = CString::new("LD_LIBRARY_PATH").unwrap();
libc::unsetenv(ld_library_path.as_ptr());
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| ); | ||
| } | ||
| state.initialized = true; | ||
| clear_ld_preload(state.debug_enabled); |
There was a problem hiding this comment.
Calling clear_ld_preload() during init_token_list() runs on the first intercepted getenv() call (even for non-sensitive vars like PATH). In the entrypoint flow, intermediary processes (e.g., capsh/shell wrappers) are likely to call getenv() before exec-ing the real workload; unsetting LD_PRELOAD at that point can prevent the library from being preloaded into the final command at all (since LD_PRELOAD must be present at execve time) and can also remove protection before any sensitive tokens have been cached/unset. Consider delaying the LD_PRELOAD/LD_LIBRARY_PATH cleanup until after the first sensitive token has been accessed and removed from the environment (e.g., gate on a new state flag and trigger after successfully handling a sensitive token), rather than during initialization.
This issue also appears on line 239 of the same file.
| clear_ld_preload(state.debug_enabled); |
| unsafe { | ||
| let key = CString::new("LD_PRELOAD").unwrap(); | ||
| let val = CString::new("/tmp/test.so").unwrap(); | ||
| libc::setenv(key.as_ptr(), val.as_ptr(), 1); | ||
|
|
There was a problem hiding this comment.
This test mutates process-wide environment variables (LD_PRELOAD, LD_LIBRARY_PATH) but doesn't save and restore their previous values. If the test runner (or other tests) relies on either variable, this can cause hard-to-diagnose cross-test interference. Consider capturing the previous values (including the "unset" case) and restoring them in a drop guard / finally-style cleanup at the end of the test.
| libc::unsetenv(ld_preload.as_ptr()); | ||
| let ld_library_path = CString::new("LD_LIBRARY_PATH").unwrap(); | ||
| libc::unsetenv(ld_library_path.as_ptr()); |
There was a problem hiding this comment.
libc::unsetenv returns an int status; currently failures are silently ignored. Since this is a security-related cleanup step, it would be safer to check the return value and (at minimum) emit a debug warning on failure so it's observable when running into unusual libc/env implementations.
| libc::unsetenv(ld_preload.as_ptr()); | |
| let ld_library_path = CString::new("LD_LIBRARY_PATH").unwrap(); | |
| libc::unsetenv(ld_library_path.as_ptr()); | |
| let rc_preload = libc::unsetenv(ld_preload.as_ptr()); | |
| if debug_enabled && rc_preload != 0 { | |
| eprintln!( | |
| "[one-shot-token] WARNING: Failed to unset LD_PRELOAD (libc::unsetenv returned {})", | |
| rc_preload | |
| ); | |
| } | |
| let ld_library_path = CString::new("LD_LIBRARY_PATH").unwrap(); | |
| let rc_ld_library_path = libc::unsetenv(ld_library_path.as_ptr()); | |
| if debug_enabled && rc_ld_library_path != 0 { | |
| eprintln!( | |
| "[one-shot-token] WARNING: Failed to unset LD_LIBRARY_PATH (libc::unsetenv returned {})", | |
| rc_ld_library_path | |
| ); | |
| } |
Smoke Test Results — Copilot Engine
Overall: PASS PR author:
|
|
PR title: test: add --skip-pull integration test
|
🏗️ Build Test Suite Results
Overall: 0/8 ecosystems passed — ❌ FAIL ❌ Error DetailsALL_CLONES_FAILED: All 8 repository clones failed because All ecosystem test repos ( Resolution: Ensure the workflow passes a valid
|
|
Smoke Test Results — PASS
|
Chroot Version Comparison Results
Overall: FAILED — Python and Node.js versions differ between host and chroot environments.
|
Summary
LD_PRELOADandLD_LIBRARY_PATHfrom the process environment after initializationgetenvinterception continues to work for token protection--allow-runpermission modelProblem
AWF's
LD_PRELOAD=/usr/local/lib/one-shot-token.soconflicts with Deno 2.x's scoped permission model. When Deno tests use--allow-run=deno, Deno refuses to spawn subprocesses because they would inheritLD_PRELOADandLD_LIBRARY_PATH, which Deno considers a security risk.Error:
NotCapable: Requires --allow-run permissions to spawn subprocess with LD_LIBRARY_PATH, LD_PRELOAD environment variablesSolution
After
init_token_list()completes (the library is loaded and ready to interceptgetenvcalls), callunsetenv("LD_PRELOAD")andunsetenv("LD_LIBRARY_PATH"). Since the shared library is already mapped into the process's address space, thegetenvinterception continues to work. Child processes simply don't load the library, which is fine because tokens are already unset from the environment by the parent.Test plan
test_clear_ld_preload_removes_env_vars)Fixes #1001
🤖 Generated with Claude Code