Skip to content

fix(cli): clear LD_PRELOAD after one-shot-token library loads#1232

Merged
Mossaka merged 1 commit intomainfrom
fix/023-ld-preload-deno-conflict
Mar 11, 2026
Merged

fix(cli): clear LD_PRELOAD after one-shot-token library loads#1232
Mossaka merged 1 commit intomainfrom
fix/023-ld-preload-deno-conflict

Conversation

@Mossaka
Copy link
Collaborator

@Mossaka Mossaka commented Mar 11, 2026

Summary

  • Modifies the one-shot-token Rust LD_PRELOAD library to unset LD_PRELOAD and LD_LIBRARY_PATH from the process environment after initialization
  • The library remains loaded in the current process's address space, so getenv interception continues to work for token protection
  • Child processes no longer inherit these environment variables, fixing Deno 2.x's scoped --allow-run permission model

Problem

AWF's LD_PRELOAD=/usr/local/lib/one-shot-token.so conflicts with Deno 2.x's scoped permission model. When Deno tests use --allow-run=deno, Deno refuses to spawn subprocesses because they would inherit LD_PRELOAD and LD_LIBRARY_PATH, which Deno considers a security risk.

Error: NotCapable: Requires --allow-run permissions to spawn subprocess with LD_LIBRARY_PATH, LD_PRELOAD environment variables

Solution

After init_token_list() completes (the library is loaded and ready to intercept getenv calls), call unsetenv("LD_PRELOAD") and unsetenv("LD_LIBRARY_PATH"). Since the shared library is already mapped into the process's address space, the getenv interception 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

  • Rust unit tests pass (3 tests including new test_clear_ld_preload_removes_env_vars)
  • TypeScript build passes
  • All 839 unit tests pass
  • Lint passes (0 errors)
  • CI integration tests pass

Fixes #1001

🤖 Generated with Claude Code

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>
Copilot AI review requested due to automatic review settings March 11, 2026 17:24
@github-actions
Copy link
Contributor

✅ Coverage Check Passed

Overall Coverage

Metric Base PR Delta
Lines 82.50% 82.64% 📈 +0.14%
Statements 82.50% 82.63% 📈 +0.13%
Functions 82.69% 82.69% ➡️ +0.00%
Branches 74.78% 74.87% 📈 +0.09%
📁 Per-file Coverage Changes (1 files)
File Lines (Before → After) Statements (Before → After)
src/docker-manager.ts 84.0% → 84.5% (+0.54%) 83.3% → 83.8% (+0.52%)

Coverage comparison generated by scripts/ci/compare-coverage.ts

@Mossaka Mossaka changed the title fix(security): clear LD_PRELOAD after one-shot-token library loads fix(cli): clear LD_PRELOAD after one-shot-token library loads Mar 11, 2026
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 to unsetenv("LD_PRELOAD") and unsetenv("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 unsets LD_LIBRARY_PATH. In this repo, containers/agent/entrypoint.sh deliberately sets LD_LIBRARY_PATH for Java (AWF_JAVA_HOME/...) with a note that it is required for libjli.so; clearing it inside the preloaded library means the running shell/workload will lose that configuration and any subsequent exec will not see it. If the intent is only to satisfy Deno's --allow-run restriction, consider making LD_LIBRARY_PATH clearing 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);
Copy link

Copilot AI Mar 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
clear_ld_preload(state.debug_enabled);

Copilot uses AI. Check for mistakes.
Comment on lines +460 to +464
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);

Copy link

Copilot AI Mar 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment on lines +241 to +243
libc::unsetenv(ld_preload.as_ptr());
let ld_library_path = CString::new("LD_LIBRARY_PATH").unwrap();
libc::unsetenv(ld_library_path.as_ptr());
Copy link

Copilot AI Mar 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
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
);
}

Copilot uses AI. Check for mistakes.
@github-actions
Copy link
Contributor

Smoke Test Results — Copilot Engine

Test Result
GitHub MCP (last 2 merged PRs) #1222 test: add --skip-pull integration test, #1221 test: add --allow-host-ports validation tests
Playwright (github.com title) ✅ Title contains "GitHub"
File writing /tmp/gh-aw/agent/smoke-test-copilot-22965562364.txt created
Bash tool ✅ File verified via cat

Overall: PASS

PR author: @Mossaka — no assignees.

📰 BREAKING: Report filed by Smoke Copilot for issue #1232

@github-actions
Copy link
Contributor

PR title: test: add --skip-pull integration test
PR title: test: add --allow-host-ports validation tests
GitHub MCP (last 2 merged PRs): ✅
safeinputs-gh pr list: ✅
Playwright title check: ✅
Tavily search: ❌ (tool unavailable)
File write + cat: ✅
Discussion comment: ✅
Build npm ci && npm run build: ✅
Overall: FAIL

🔮 The oracle has spoken through Smoke Codex for issue #1232

@github-actions
Copy link
Contributor

🏗️ Build Test Suite Results

Ecosystem Project Build/Install Tests Status
Bun elysia N/A ❌ FAIL
Bun hono N/A ❌ FAIL
C++ fmt N/A ❌ FAIL
C++ json N/A ❌ FAIL
Deno oak N/A ❌ FAIL
Deno std N/A ❌ FAIL
.NET hello-world N/A ❌ FAIL
.NET json-parse N/A ❌ FAIL
Go color N/A ❌ FAIL
Go env N/A ❌ FAIL
Go uuid N/A ❌ FAIL
Java gson N/A ❌ FAIL
Java caffeine N/A ❌ FAIL
Node.js clsx N/A ❌ FAIL
Node.js execa N/A ❌ FAIL
Node.js p-limit N/A ❌ FAIL
Rust fd N/A ❌ FAIL
Rust zoxide N/A ❌ FAIL

Overall: 0/8 ecosystems passed — ❌ FAIL

❌ Error Details

ALL_CLONES_FAILED: All 8 repository clones failed because GH_TOKEN is not set in this environment.

gh: To use GitHub CLI in a GitHub Actions workflow, set the GH_TOKEN environment variable. Example:
  env:
    GH_TOKEN: ${{ github.token }}

All ecosystem test repos (gh-aw-firewall-test-bun, gh-aw-firewall-test-cpp, gh-aw-firewall-test-deno, gh-aw-firewall-test-dotnet, gh-aw-firewall-test-go, gh-aw-firewall-test-java, gh-aw-firewall-test-node, gh-aw-firewall-test-rust) could not be cloned. No tests were executed.

Resolution: Ensure the workflow passes a valid GH_TOKEN environment variable when invoking this agent.

Generated by Build Test Suite for issue #1232 ·

@github-actions
Copy link
Contributor

Smoke Test Results — PASS

💥 [THE END] — Illustrated by Smoke Claude for issue #1232

@github-actions
Copy link
Contributor

Chroot Version Comparison Results

Runtime Host Version Chroot Version Match?
Python Python 3.12.12 Python 3.12.3 ❌ NO
Node.js v24.14.0 v20.20.0 ❌ NO
Go go1.22.12 go1.22.12 ✅ YES

Overall: FAILED — Python and Node.js versions differ between host and chroot environments.

Tested by Smoke Chroot for issue #1232

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

LD_PRELOAD (one-shot-token) breaks Deno scoped --allow-run permissions

2 participants