Skip to content

fix: read /proc/self/environ so PSST_PASSWORD survives ptrace-based sandbox injection#36

Open
LucasRoesler wants to merge 1 commit intoMichaelliv:mainfrom
LucasRoesler:fix/bun-proc-environ-fallback
Open

fix: read /proc/self/environ so PSST_PASSWORD survives ptrace-based sandbox injection#36
LucasRoesler wants to merge 1 commit intoMichaelliv:mainfrom
LucasRoesler:fix/bun-proc-environ-fallback

Conversation

@LucasRoesler
Copy link
Copy Markdown

@LucasRoesler LucasRoesler commented May 5, 2026

Problem

psst unlock fails with "Failed to unlock vault" when PSST_PASSWORD is injected by a ptrace-based sandbox supervisor (e.g. nono in supervised mode), even though the variable is present in the process environment.

The root cause is a timing gap between Bun and ptrace injection. Bun snapshots process.env from envp[] very early in its startup sequence — before a ptrace supervisor has had a chance to write into the process environment. The injected variable is present in the kernel's live view (/proc/self/environ) but absent from process.env.

This affects any runtime that snapshots envp[] at startup (Bun, Go, JVM). It does not affect Node.js or bash, which call through glibc's getenv() and see the live environ pointer.

Reproduction

# 1. create a demo directory and init a vault
mkdir psst-demo && cd psst-demo
PSST_PASSWORD=demo-password-1234 psst init

# 2. add an example secret
echo "my-secret-value" | PSST_PASSWORD=demo-password-1234 psst set --stdin EXAMPLE_SECRET

# 3. verify it works normally outside the sandbox
PSST_PASSWORD=demo-password-1234 psst list
# ● EXAMPLE_SECRET
# 1 secret(s)

# 4. store the password where nono can inject it (1Password shown; secret-tool works too)
#    op item create --title psst-demo --vault Personal password=demo-password-1234

# 5. run inside the sandbox — nono injects PSST_PASSWORD via ptrace after execve()
nono run --env-credential-map 'op://Personal/psst-demo/password' PSST_PASSWORD --allow-cwd -- psst list

Before fix:

✗ Failed to unlock vault

After fix (with PSST_DEBUG=1):

[psst debug] native-env: /proc/self/environ has 86 entries, PSST_PASSWORD=present
[psst debug] unlock: PSST_PASSWORD=set(len=20)

Secrets

● EXAMPLE_SECRET

1 secret(s)

Changes

  • Add src/vault/native-env.ts: reads /proc/self/environ at call-time and exposes getenvNative(name). Linux-only; returns null on other platforms. Result is cached after first read.
  • In SqliteBackend.unlock(): fall back to getenvNative() when process.env.PSST_PASSWORD is unset, so injected credentials are found even if Bun missed them at startup.
  • Fix unlock priority order: env var (both sources) → keychain. Previously keychain was checked first, which could fail or block inside a sandbox before the env var fallback was tried.
  • Add PSST_DEBUG=1 logging to the unlock path (itself read via both sources) to make this class of failure diagnosable without rebuilding.

Bun snapshots process.env from envp[] very early in its startup sequence.
Sandbox supervisors that inject credentials via ptrace (e.g. nono) write
into the kernel's live environ after execve(), making those vars visible
in /proc/self/environ but invisible to process.env.

Add native-env.ts which reads /proc/self/environ at call-time (Linux only,
no-op on other platforms) and export getenvNative() as a fallback for any
env var lookup that must survive ptrace-style injection.

Apply the fallback in SqliteBackend.unlock(): check process.env first, then
/proc/self/environ, then keychain. This order ensures that injected passwords
are found before a sandbox-restricted keychain attempt fails.

Also add PSST_DEBUG=1 gated logging (itself read via both paths) to make the
unlock flow diagnosable without rebuilding.
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