Skip to content

feat: replace env file with CLI args for --elevate env restore#222

Draft
Xeonacid wants to merge 1 commit into
kxxt:mainfrom
Xeonacid:restore_env
Draft

feat: replace env file with CLI args for --elevate env restore#222
Xeonacid wants to merge 1 commit into
kxxt:mainfrom
Xeonacid:restore_env

Conversation

@Xeonacid
Copy link
Copy Markdown
Contributor

@Xeonacid Xeonacid commented Apr 20, 2026

Pass each environment variable as a separate --restore-env KEY=VALUE argument instead of writing to a temp file and passing --restore-env-file. This eliminates the file-based TOCTOU surface and the need for temp file security checks.

Summary by CodeRabbit

  • Chores
    • Improved internal mechanisms for environment variable restoration during privilege elevation operations to enhance security handling and code maintainability.
    • Updated internal test suite to validate the revised environment restoration implementation.

Pass each environment variable as a separate --restore-env KEY=VALUE
argument instead of writing to a temp file and passing --restore-env-file.
This eliminates the file-based TOCTOU surface and the need for temp file
security checks.
@vercel
Copy link
Copy Markdown

vercel Bot commented Apr 20, 2026

@Xeonacid is attempting to deploy a commit to the kxxt's projects Team on Vercel.

A member of the Team first needs to authorize it.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 20, 2026

Warning

Rate limit exceeded

@Xeonacid has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 54 minutes and 48 seconds before requesting another review.

Your organization is not enrolled in usage-based pricing. Contact your admin to enable usage-based pricing to continue reviews beyond the rate limit, or try again in 54 minutes and 48 seconds.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yml

Review profile: CHILL

Plan: Pro

Run ID: ff9e2f3f-5e8c-4b4d-b2bf-fbcaece2b6c9

📥 Commits

Reviewing files that changed from the base of the PR and between 008c59b and 28b9c8d.

📒 Files selected for processing (4)
  • crates/tracexec-core/src/cli.rs
  • crates/tracexec-core/src/elevate.rs
  • src/main.rs
  • tests/smoke.rs

Walkthrough

The PR replaces environment variable persistence during privilege escalation from file-based (temporary file with serialized KEY=VALUE\0 entries) to CLI argument-based approach. Environment variables are now passed as repeated --restore-env KEY=VALUE CLI arguments during re-execution, eliminating temp file creation and security-related file handling.

Changes

Cohort / File(s) Summary
CLI Argument Refactoring
crates/tracexec-core/src/cli.rs, src/main.rs
Changed restore_env_file: Option<PathBuf> to restore_env: Vec<OsString> in CLI struct definition. Updated Clap metadata to set long = "restore-env" and adjusted help text. Updated entrypoint to check restore_env emptiness and call new restoration function with the entries collection.
Elevation Environment Restoration
crates/tracexec-core/src/elevate.rs
Removed temp file creation, serialization/deserialization logic, and file-based restore security checks (O_NOFOLLOW, fstat, unlink). Replaced restore_env_from_file(path) with restore_env_from_entries(entries) that parses KEY=VALUE entries directly. Updated elevated re-exec argument construction to inject --restore-env pairs instead of file path argument.
Test Updates
tests/smoke.rs
Renamed test from elevate_env_file_preserves_custom_env_var to elevate_restore_env_preserves_custom_env_var. Removed temp file construction logic and direct environment variable passing via repeated --restore-env CLI arguments for TRACEXEC_TEST_MARKER, HOME, and PATH.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

Poem

🐰 A file once held our secrets tight,
With null-byte barriers, safe from sight—
But CLI args proved the lighter way,
Less temp to clean, less risk at play! ✨

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main change: replacing environment file persistence with CLI arguments for the --elevate feature.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

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)
crates/tracexec-core/src/elevate.rs (1)

181-215: ⚠️ Potential issue | 🟠 Major

Address E2BIG risk when passing large environments to elevated sudo.

The elevate_and_reexec() function captures all environment variables and serializes each one into two consecutive argv elements (--restore-env flag + KEY=VALUE). Since sudo is invoked without -E, the parent's original envp is also inherited by the child process. For users with large environments (CI runners with hundreds of variables, development shells with expansive LS_COLORS or BASH_FUNC_* entries, locales with long paths), the combined argv + envp can exceed ARG_MAX (~128 KiB to 2 MiB depending on platform/stack rlimit), causing execve(2) to fail with E2BIG. This error surfaces as an opaque err.into() on line 214 with no diagnostic hint.

Consider:

  1. Pre-exec validation: Measure the aggregate argv size before calling exec() and either produce a targeted error message or fall back to an alternative mechanism (e.g., memfd_create + --restore-env-fd <fd>).
  2. Filter non-restorable entries: Skip shell-specific entries like BASH_FUNC_*%%, which are often large and unnecessary for the elevated process.
  3. Document the limit: Add a note to the --elevate help text explaining the environment size constraints.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/tracexec-core/src/elevate.rs` around lines 181 - 215,
elevate_and_reexec currently serializes every env var into argv entries (via
capture_env_entries / build_elevated_args) and then calls
Command::new("sudo").arg(&exe).args(&elevated_args).exec(), which risks execve
E2BIG when argv+envp exceed ARG_MAX; add a pre-exec validation that computes the
total byte size of argv (exe + elevated_args) plus current envp and if it
exceeds a conservative threshold emit a clear error suggesting workarounds (or
fall back to an alternative like memfd + a new flag), and also filter out
large/unrestorable shell-specific vars (e.g., keys matching BASH_FUNC_* or other
noisy patterns) in capture_env_entries/build_elevated_args so they are not
included in --restore-env; update the error path to include the size measured
and mention --restore-env limits in the --elevate help text.
🧹 Nitpick comments (3)
tests/smoke.rs (1)

70-80: Optional: consider var_os + OsString composition for the PATH argument.

std::env::var("PATH").unwrap_or_default() silently drops bytes if the caller's PATH is not UTF-8. Since the whole point of the surrounding code is to preserve arbitrary OsString env values, you could build the --restore-env argument with OsString + push("PATH=") + push(os_path) for parity with production behavior. Not a correctness issue under typical CI, so feel free to defer.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/smoke.rs` around lines 70 - 80, The test currently uses
std::env::var("PATH").unwrap_or_default() which can drop non-UTF8 bytes; change
the construction of the PATH restore argument to use std::env::var_os("PATH")
and compose an OsString by starting with "PATH=" (OsString) then push the
returned OsString (or empty OsString if None) so the .arg(...) call receives an
OsString preserving non-UTF8 content; update the spot that builds the PATH
restore arg (the expression passed to .arg(format!("PATH={}", ...)) in the test)
to use this OsString composition instead.
crates/tracexec-core/src/cli.rs (1)

79-85: Help text is worded as singular, but the flag can be repeated.

Since the field is Vec<OsString> and users will pass --restore-env KEY=VALUE multiple times, consider wording that conveys repeatability, e.g. "Internal: restore an environment variable from --elevate (KEY=VALUE); may be specified multiple times". The flag is hidden from --help, but it still shows up in generated shell completions.

✏️ Proposed wording tweak
   #[arg(
     long = "restore-env",
     hide = true,
     conflicts_with = "elevate",
-    help = "Internal: restore an environment variable from --elevate (KEY=VALUE)"
+    help = "Internal: restore an environment variable from --elevate (KEY=VALUE); may be repeated"
   )]
   pub restore_env: Vec<OsString>,
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/tracexec-core/src/cli.rs` around lines 79 - 85, Help text for the
restore_env argument is singular but the field is Vec<OsString> and the flag can
be repeated; update the help string on the #[arg(...)] for restore_env to
indicate repeatability (e.g., "Internal: restore environment variables from
--elevate (KEY=VALUE); may be specified multiple times") so shell completions
and users understand it accepts multiple instances. Locate the restore_env field
and its #[arg(...)] attributes and replace the current help text accordingly,
preserving hide = true and conflicts_with = "elevate".
crates/tracexec-core/src/elevate.rs (1)

245-303: Tests reimplement parsing locally rather than exercising restore_env_from_entries.

test_restore_env_from_entries_roundtrip, test_restore_env_handles_non_utf8, and test_restore_env_from_entries_rejects_missing_equals all duplicate the split-on-= logic inline instead of calling restore_env_from_entries. I understand the real function clobbers the process env and is unsafe to call from the test harness — but then the current tests only cover a parallel copy of the parser, so a divergence (e.g. switching to splitn, rejecting empty keys, normalizing Unicode) could regress silently.

Options:

  • Extract the &OsStr -> Result<(OsString, OsString)> parsing into a pure fn parse_entry(...) helper and test that directly. restore_env_from_entries then becomes a thin clear + for entry { parse + set_var } wrapper.
  • Or run the real function inside a forked child / subprocess test where clobbering env is harmless.

The first option is low-risk and gives you real coverage of the production parser, including the error path for missing =.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/tracexec-core/src/elevate.rs` around lines 245 - 303, Tests duplicate
the split-on-'=' logic instead of exercising restore_env_from_entries; extract
the parsing logic into a pure helper fn parse_entry(entry: &OsStr) ->
Result<(OsString, OsString), ParseError> (or similar) that implements the exact
current behavior (handles non-UTF8, preserves bytes after the first '=', returns
Err when '=' missing or key empty), update restore_env_from_entries to call
parse_entry for each entry and then set/clear the env, and change the three
tests (test_restore_env_from_entries_roundtrip,
test_restore_env_handles_non_utf8,
test_restore_env_from_entries_rejects_missing_equals) to call parse_entry
directly and assert on Ok/Err results so parsing is covered without clobbering
process env.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@crates/tracexec-core/src/elevate.rs`:
- Around line 9-12: The docstring in elevate.rs currently states that
environment variables passed as CLI args "are only visible to root (via
/proc/<pid>/cmdline)" which is incorrect; update the Security section to say
that /proc/<pid>/cmdline is typically world-readable on standard Linux (so
CLI-passed secrets can be read by unprivileged local users) unless procfs is
mounted with hidepid=1|2, and explicitly call out that this makes --restore-env
entries potentially exposed for the lifetime of the elevated tracexec process;
in the same docstring (around the existing Security header and any mention of
--restore-env) add a brief note about mitigations/alternatives (e.g., use
hidepid, avoid passing secrets on cmdline, or use a more secure secret-passing
mechanism) and remove the incorrect claim that only root can view
/proc/<pid>/cmdline.

---

Outside diff comments:
In `@crates/tracexec-core/src/elevate.rs`:
- Around line 181-215: elevate_and_reexec currently serializes every env var
into argv entries (via capture_env_entries / build_elevated_args) and then calls
Command::new("sudo").arg(&exe).args(&elevated_args).exec(), which risks execve
E2BIG when argv+envp exceed ARG_MAX; add a pre-exec validation that computes the
total byte size of argv (exe + elevated_args) plus current envp and if it
exceeds a conservative threshold emit a clear error suggesting workarounds (or
fall back to an alternative like memfd + a new flag), and also filter out
large/unrestorable shell-specific vars (e.g., keys matching BASH_FUNC_* or other
noisy patterns) in capture_env_entries/build_elevated_args so they are not
included in --restore-env; update the error path to include the size measured
and mention --restore-env limits in the --elevate help text.

---

Nitpick comments:
In `@crates/tracexec-core/src/cli.rs`:
- Around line 79-85: Help text for the restore_env argument is singular but the
field is Vec<OsString> and the flag can be repeated; update the help string on
the #[arg(...)] for restore_env to indicate repeatability (e.g., "Internal:
restore environment variables from --elevate (KEY=VALUE); may be specified
multiple times") so shell completions and users understand it accepts multiple
instances. Locate the restore_env field and its #[arg(...)] attributes and
replace the current help text accordingly, preserving hide = true and
conflicts_with = "elevate".

In `@crates/tracexec-core/src/elevate.rs`:
- Around line 245-303: Tests duplicate the split-on-'=' logic instead of
exercising restore_env_from_entries; extract the parsing logic into a pure
helper fn parse_entry(entry: &OsStr) -> Result<(OsString, OsString), ParseError>
(or similar) that implements the exact current behavior (handles non-UTF8,
preserves bytes after the first '=', returns Err when '=' missing or key empty),
update restore_env_from_entries to call parse_entry for each entry and then
set/clear the env, and change the three tests
(test_restore_env_from_entries_roundtrip, test_restore_env_handles_non_utf8,
test_restore_env_from_entries_rejects_missing_equals) to call parse_entry
directly and assert on Ok/Err results so parsing is covered without clobbering
process env.

In `@tests/smoke.rs`:
- Around line 70-80: The test currently uses
std::env::var("PATH").unwrap_or_default() which can drop non-UTF8 bytes; change
the construction of the PATH restore argument to use std::env::var_os("PATH")
and compose an OsString by starting with "PATH=" (OsString) then push the
returned OsString (or empty OsString if None) so the .arg(...) call receives an
OsString preserving non-UTF8 content; update the spot that builds the PATH
restore arg (the expression passed to .arg(format!("PATH={}", ...)) in the test)
to use this OsString composition instead.
🪄 Autofix (Beta)

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.yml

Review profile: CHILL

Plan: Pro

Run ID: a7f083cb-6e6f-46be-8c38-7e062a77168d

📥 Commits

Reviewing files that changed from the base of the PR and between 03bda74 and 008c59b.

📒 Files selected for processing (4)
  • crates/tracexec-core/src/cli.rs
  • crates/tracexec-core/src/elevate.rs
  • src/main.rs
  • tests/smoke.rs

Comment on lines 9 to +12
//! ## Security
//!
//! The environment file is created with mode 0600 and owned by the current user.
//! On restore, the file is opened with `O_NOFOLLOW` (rejecting symlinks), its
//! metadata is checked via `fstat` (avoiding TOCTOU races), and it is unlinked
//! immediately after reading. Only the original user and root can access it.
//! Environment variables are passed as CLI arguments. The data is only visible
//! to root (via /proc/<pid>/cmdline) after the sudo exec.
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

Is /proc/<pid>/cmdline world-readable by default on Linux, and how does the hidepid procfs mount option change this?

💡 Result:

Yes, /proc/<pid>/cmdline is world-readable by default on Linux. The hidepid procfs mount option restricts access to it for non-root users or non-owners.

Citations:


🏁 Script executed:

cat -n crates/tracexec-core/src/elevate.rs | head -20

Repository: kxxt/tracexec

Length of output: 863


Security documentation inaccuracy — /proc/<pid>/cmdline is not root-only by default.

The docstring at lines 9–12 claims environment variable data "is only visible to root (via /proc//cmdline) after the sudo exec." This is incorrect on standard Linux systems where /proc/<pid>/cmdline is world-readable by default (that is how ps and top work for unprivileged users). Access is only restricted under non-default hidepid=1|2 procfs mount options.

This means any unprivileged local user can read --restore-env entries containing secrets (e.g., AWS_SECRET_ACCESS_KEY, GITHUB_TOKEN, Kerberos credentials) for the lifetime of the elevated tracexec process. This is a documented trade-off against the previous tempfile approach, which was less visible in cmdline but had TOCTOU surface, yet users relying on the false guarantee here have no visibility into that trade-off.

Update the docstring to accurately describe the security posture and thread model:

Suggested docstring fix
 //! ## Security
 //!
-//! Environment variables are passed as CLI arguments. The data is only visible
-//! to root (via /proc/<pid>/cmdline) after the sudo exec.
+//! Environment variables are passed as CLI arguments to the elevated process.
+//! Note that `/proc/<pid>/cmdline` is world-readable on default Linux procfs
+//! mounts, so any local user can observe these entries (including any secrets
+//! they contain) for the lifetime of the elevated tracexec process. This is a
+//! deliberate trade-off against the previous tempfile-based approach, which
+//! avoided cmdline exposure but added file-handling TOCTOU surface. If your
+//! threat model includes unprivileged local users, avoid running tracexec
+//! under `--elevate` with sensitive variables in your environment, or mount
+//! /proc with `hidepid=2`.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/tracexec-core/src/elevate.rs` around lines 9 - 12, The docstring in
elevate.rs currently states that environment variables passed as CLI args "are
only visible to root (via /proc/<pid>/cmdline)" which is incorrect; update the
Security section to say that /proc/<pid>/cmdline is typically world-readable on
standard Linux (so CLI-passed secrets can be read by unprivileged local users)
unless procfs is mounted with hidepid=1|2, and explicitly call out that this
makes --restore-env entries potentially exposed for the lifetime of the elevated
tracexec process; in the same docstring (around the existing Security header and
any mention of --restore-env) add a brief note about mitigations/alternatives
(e.g., use hidepid, avoid passing secrets on cmdline, or use a more secure
secret-passing mechanism) and remove the incorrect claim that only root can view
/proc/<pid>/cmdline.

@Xeonacid Xeonacid marked this pull request as draft April 20, 2026 14:44
@codecov
Copy link
Copy Markdown

codecov Bot commented Apr 20, 2026

Codecov Report

❌ Patch coverage is 90.59829% with 11 lines in your changes missing coverage. Please review.
✅ Project coverage is 72.95%. Comparing base (03bda74) to head (28b9c8d).

Files with missing lines Patch % Lines
crates/tracexec-core/src/elevate.rs 90.17% 11 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main     #222      +/-   ##
==========================================
- Coverage   73.04%   72.95%   -0.09%     
==========================================
  Files          80       80              
  Lines       18194    18113      -81     
==========================================
- Hits        13289    13215      -74     
+ Misses       4905     4898       -7     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant