Skip to content

fix(desktop): forward macOS XPC bootstrap env vars to v2 terminal shells#3799

Open
WadeSeidule wants to merge 2 commits into
superset-sh:mainfrom
WadeSeidule:fix/desktop-xpc-env-darwin
Open

fix(desktop): forward macOS XPC bootstrap env vars to v2 terminal shells#3799
WadeSeidule wants to merge 2 commits into
superset-sh:mainfrom
WadeSeidule:fix/desktop-xpc-env-darwin

Conversation

@WadeSeidule
Copy link
Copy Markdown

@WadeSeidule WadeSeidule commented Apr 27, 2026

Title

fix(desktop): forward macOS XPC bootstrap env vars to v2 terminal shells

Body

Summary

  • forward __CFBundleIdentifier, XPC_SERVICE_NAME, XPC_FLAGS, and MallocNanoZone through the strict-shell snapshot in clean-shell-env.ts
  • demote the SSL_CERT_FILE shim in env.ts to belt-and-suspenders (it stays in for tools that prefer file-based trust roots)
  • restore the env parity with what Terminal.app and iTerm shells inherit, so anything that calls into Directory Services, Keychain, or the Security framework just works

Root Cause

spawnCleanShellEnv() seeds the helper shell with buildMinimalEnv() — a small allowlist of keys to forward (HOME, USER, PATH, TERM, …, Apple_PubSub_Socket_Render, etc.) — and then snapshots whatever command env produces. The four XPC bootstrap variables aren't on that list, so the helper shell starts without them, the snapshot doesn't capture them, and every PTY downstream inherits a base env that's missing them.

When those identifiers are stripped, the shell is technically running as the right user (the kernel still knows the uid) but anything that does a getpwuid_r(3) or a SecTrustEvaluateWithError call gets routed to a kCFErrorDomainOSStatus-backed error because the system service can't be reached. From the shell's perspective the failure surfaces as:

$ whoami
501                                     # numeric UID — getpwuid_r returned eServerError
$ dscl . -read /Users/$(id -un) UniqueID
Operation failed with error: eServerError
$ git push                              # over SSH; OpenSSH calls getpwuid()
No user exists for uid 501
fatal: Could not read from remote repository.
$ gh api repos/cli/cli
Get "https://api.github.com/repos/cli/cli": tls: failed to verify certificate: x509: OSStatus -26276

The OSStatus -26276 in that last line is what Go's crypto/x509/internal/macos.SecTrustEvaluateWithError reports when CFErrorCopyDescription falls back to printing the raw kCFErrorDomainOSStatus code — CFCopyLocalizedString is also affected by the missing services, so even the human-readable string is unavailable. The exact code isn't in any public SecBase.h; what matters is that SecTrustEvaluate itself is failing at the framework level, not at the cert level.

curl was unaffected because the existing SSL_CERT_FILE shim feeds it a file-based trust root. Anything that goes through the macOS Security framework — Go binaries (gh, terraform, helm), Apple's own command-line tools (security, dscl), OpenSSH's user-resolution path — failed until those env vars came back.

The four variables this PR forwards are the minimal set that fixes the symptoms above without weakening the existing runtime-strip allowlist:

Variable What it anchors
__CFBundleIdentifier CoreFoundation client identification; libsecinit uses it to bind to securityd
XPC_SERVICE_NAME XPC bootstrap port assigned by launchd; required by securityd and opendirectoryd clients
XPC_FLAGS XPC initialization flags inherited from launchd
MallocNanoZone nano-allocator handshake; some libsystem paths early-out when it's missing

I considered a more permissive "forward everything launchd would seed" approach, but it's hard to enumerate that set portably across macOS versions, and the four above are sufficient on 14.x and 15.x.

The runtime-strip allowlist (stripTerminalRuntimeEnv in env-strip.ts) is innocent — none of these four keys match its prefixes (npm_, ELECTRON_, VITE_, NEXT_PUBLIC_, TURBO_, HOST_) or its exact-key set. So adding them to SHELL_BOOTSTRAP_KEYS is sufficient; the strip step leaves them alone naturally.

Validation

# typecheck + lint
bun run --cwd packages/host-service typecheck
bunx @biomejs/biome@2.4.2 check --write --unsafe packages/host-service/src/terminal/

# unit tests for the env builder
bun test packages/host-service/src/terminal/env.test.ts

Manual repro on macOS 14.6:

  1. Open a fresh v2 terminal pane in Superset.
  2. Confirm the symptoms before the patch:
    whoami                       # → "501"
    git -c core.sshCommand="ssh -v" ls-remote git@github.com:cli/cli.git 2>&1 | grep "No user"
    gh api repos/cli/cli         # → tls: failed to verify certificate: x509: OSStatus -26276
  3. Apply the patch, restart Superset, open a fresh terminal pane.
  4. Confirm:
    whoami                       # → the username, e.g. "wadeseidule"
    git ls-remote git@github.com:cli/cli.git    # succeeds
    gh api repos/cli/cli         # returns JSON
    security find-internet-password -s github.com    # responds normally
  5. Spot-check that previously-stripped categories are still stripped:
    env | grep -E '^(ELECTRON_|npm_|VITE_|HOST_)'    # no matches

Diff

--- a/packages/host-service/src/terminal/clean-shell-env.ts
+++ b/packages/host-service/src/terminal/clean-shell-env.ts
@@ -16,6 +16,14 @@ const SHELL_BOOTSTRAP_KEYS = [
 	"LC_CTYPE",
 	"__CF_USER_TEXT_ENCODING",
 	"Apple_PubSub_Socket_Render",
+	// macOS XPC bootstrap — without these, the helper shell (and every PTY
+	// downstream) loses its Mach handles to securityd/opendirectoryd, which
+	// breaks getpwuid_r (whoami → uid; OpenSSH "No user exists for uid N")
+	// and SecTrustEvaluateWithError (gh, terraform: "x509: OSStatus -26276").
+	"__CFBundleIdentifier",
+	"XPC_SERVICE_NAME",
+	"XPC_FLAGS",
+	"MallocNanoZone",
 	"COMSPEC",
 	"USERPROFILE",
 	"SYSTEMROOT",

--- a/packages/host-service/src/terminal/env.ts
+++ b/packages/host-service/src/terminal/env.ts
@@ -181,8 +181,9 @@ export function buildV2TerminalEnv(
 		env.SUPERSET_HOME_DIR = supersetHomeDir;
 	}
 
-	// Electron child processes can't access macOS Keychain for TLS cert verification,
-	// causing "x509: OSStatus -26276" in Go binaries like `gh`. File-based fallback.
+	// Belt-and-suspenders for tools that prefer file-based trust roots even
+	// when the macOS Security framework is reachable (clean-shell-env now
+	// forwards the XPC bootstrap vars, so trust evaluation works directly).
 	if (
 		os.platform() === "darwin" &&
 		!env.SSL_CERT_FILE &&

Notes

  • This restores behavior to what users had pre-V2 terminal env #3184 for these specific variables only; everything else the allowlist filters stays filtered.
  • The four variables are low-risk to forward: __CFBundleIdentifier is just the parent's bundle ID, XPC_SERVICE_NAME and XPC_FLAGS are launchd-assigned identifiers, and MallocNanoZone is a runtime allocator handshake. None of them carry user data, credentials, or workspace state.
  • If there's a desire to be even more conservative, dropping MallocNanoZone still fixes whoami, gh, and git push over SSH on the macOS versions I tested. I'd keep it in to avoid an unrelated nano-allocator warning that some libsystem paths emit.
  • Left the SSL_CERT_FILE shim in place. It's redundant once the XPC vars flow, but it's also harmless and protects tools that explicitly prefer file-based trust roots (curl, anything with OPENSSL_CONF-driven trust resolution).

Commit message

fix(desktop): forward macOS XPC bootstrap env vars to v2 terminal shells

Forward __CFBundleIdentifier, XPC_SERVICE_NAME, XPC_FLAGS, and
MallocNanoZone through the strict-shell snapshot on Darwin.

Without these, the helper shell that buildMinimalEnv() seeds for
spawnCleanShellEnv() loses its Mach handles to securityd and
opendirectoryd. That propagates into every PTY downstream and breaks
getpwuid_r (whoami returns the uid; OpenSSH fails with "No user exists
for uid N") and SecTrustEvaluateWithError (Go binaries like gh report
"x509: OSStatus -26276"). curl was unaffected because of the
SSL_CERT_FILE shim, which is now belt-and-suspenders rather than the
primary fix.

The rest of the runtime-strip allowlist stays as-is; Electron, npm,
Vite, and host runtime variables remain stripped.

Summary by CodeRabbit

  • New Features

    • Allowlist for shell bootstrap environment made publicly accessible and extended to include additional macOS XPC/bootstrap variables.
  • Tests

    • Added tests to verify the extended macOS bootstrap keys are preserved in the terminal base environment.
  • Chores

    • Clarified macOS SSL certificate rationale in terminal environment docs/comments.

Summary by cubic

Forward critical macOS XPC bootstrap env vars to v2 terminal shells so Keychain, Directory Services, and TLS trust evaluation work as expected. Restores parity with Terminal/iTerm and keeps the SSL_CERT_FILE shim as a fallback. Adds a test to prevent regressions.

  • Bug Fixes
    • Forward __CFBundleIdentifier, XPC_SERVICE_NAME, XPC_FLAGS, and MallocNanoZone in clean-shell-env.ts.
    • Demote SSL_CERT_FILE in env.ts to a fallback for tools that prefer file-based trust.
    • Export SHELL_BOOTSTRAP_KEYS and add a unit test to ensure the XPC keys flow from seed to PTY env unchanged.
    • Fixes: whoami shows username (not UID), OpenSSH/Git “No user exists for uid N”, Go tools TLS x509: OSStatus -26276, and security/dscl errors.

Written for commit 35c7019. Summary will update on new commits. Review in cubic

Forward __CFBundleIdentifier, XPC_SERVICE_NAME, XPC_FLAGS, and
MallocNanoZone through the strict-shell snapshot on Darwin.

Without these, the helper shell that buildMinimalEnv() seeds for
spawnCleanShellEnv() loses its Mach handles to securityd and
opendirectoryd. That propagates into every PTY downstream and breaks
getpwuid_r (whoami returns the uid; OpenSSH fails with "No user exists
for uid N") and SecTrustEvaluateWithError (Go binaries like gh report
"x509: OSStatus -26276"). curl was unaffected because of the
SSL_CERT_FILE shim, which is now belt-and-suspenders rather than the
primary fix.

The rest of the runtime-strip allowlist stays as-is; Electron, npm,
Vite, and host runtime variables remain stripped.
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 27, 2026

📝 Walkthrough

Walkthrough

Exports the shell bootstrap allowlist and extends it with macOS XPC/bootstrap keys; updates a comment about macOS SSL_CERT_FILE fallback rationale; adds tests asserting the new keys are present and preserved in terminal base env.

Changes

Cohort / File(s) Summary
Shell bootstrap allowlist & tests
packages/host-service/src/terminal/clean-shell-env.ts, packages/host-service/src/terminal/env.test.ts
Made SHELL_BOOTSTRAP_KEYS exported and added additional macOS XPC/bootstrap keys to the allowlist; added tests to verify the keys exist and are preserved in the terminal base environment.
Terminal env comment update
packages/host-service/src/terminal/env.ts
Clarified the macOS SSL_CERT_FILE fallback comment to cite the expanded XPC/bootstrap variable forwarding behavior and frame the file-based trust root as a belt-and-suspenders preference; no functional changes.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Poem

🐰 I hopped through envs with curious feet,
Added keys so macOS and shell can meet,
Tests snugly check each tiny string,
Bootstrap whispers make the login shell sing. 🥕✨

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately describes the main change: forwarding macOS XPC bootstrap environment variables to v2 terminal shells, which is the primary focus of the entire changeset.
Description check ✅ Passed The PR description is comprehensive and well-structured, covering all template sections including summary, root cause analysis, validation steps, and additional notes. It provides extensive context and testing details.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ 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.

@greptile-apps
Copy link
Copy Markdown

greptile-apps Bot commented Apr 27, 2026

Greptile Summary

This PR fixes a macOS-specific regression where the helper shell spawned by spawnCleanShellEnv() was missing the four XPC/CoreFoundation bootstrap variables needed to reach securityd and opendirectoryd. As a result any process downstream of the PTY that called getpwuid_r or SecTrustEvaluateWithError (OpenSSH, Go binaries, Apple CLI tools) received kernel-level errors. The fix adds the four variables to SHELL_BOOTSTRAP_KEYS in clean-shell-env.ts and demotes the existing SSL_CERT_FILE shim to a belt-and-suspenders comment in env.ts; all existing denylist logic in env-strip.ts is untouched and continues to strip Electron/npm/VITE/HOST_ vars correctly.

Confidence Score: 5/5

Safe to merge — minimal, targeted change that restores parity with Terminal.app behavior; no secrets or sensitive data is forwarded; existing strip logic is unaffected.

Both changed files have no P0/P1 findings. The new keys are proven low-risk (no credentials, no workspace state), they are correctly exempt from the runtime denylist in env-strip.ts, and the if (value) guard in buildMinimalEnv means they are silently skipped on non-macOS platforms. The only finding is a P2 suggestion for additional unit test coverage.

No files require special attention.

Important Files Changed

Filename Overview
packages/host-service/src/terminal/clean-shell-env.ts Adds four macOS XPC bootstrap keys to SHELL_BOOTSTRAP_KEYS so the helper shell is seeded with them before the env snapshot is taken; no other logic changes.
packages/host-service/src/terminal/env.ts Comment-only update to the SSL_CERT_FILE shim block, reflecting it is now belt-and-suspenders rather than the primary TLS fix; no functional logic changes.

Sequence Diagram

sequenceDiagram
    participant ES as Electron/host-service
    participant BME as buildMinimalEnv()
    participant CS as spawnCleanShellEnv()
    participant SH as Login Shell (-i -l)
    participant PTY as PTY (v2 terminal)
    participant SVC as macOS securityd / opendirectoryd

    ES->>BME: process.env lookup
    Note over BME: Copies SHELL_BOOTSTRAP_KEYS<br/>(incl. __CFBundleIdentifier,<br/>XPC_SERVICE_NAME, XPC_FLAGS,<br/>MallocNanoZone — NEW)
    BME-->>CS: minimalEnv
    CS->>SH: spawn(shell, ["-i","-l","-c","command env"], {env: minimalEnv})
    Note over SH: Shell rc files run,<br/>XPC vars present → Mach handles intact
    SH-->>CS: stdout (env snapshot)
    CS-->>ES: parsedEnv
    ES->>PTY: buildV2TerminalEnv(strippedSnapshot)
    PTY->>SVC: getpwuid_r / SecTrustEvaluateWithError
    Note over SVC: XPC_SERVICE_NAME + __CFBundleIdentifier<br/>present → bootstrap port resolved ✓
    SVC-->>PTY: username / cert chain OK
Loading
Prompt To Fix All With AI
This is a comment left during a code review.
Path: packages/host-service/src/terminal/clean-shell-env.ts
Line: 24-27

Comment:
**Consider adding unit tests for the new XPC bootstrap keys**

The `env.test.ts` file has good coverage for `stripTerminalRuntimeEnv` and `buildV2TerminalEnv`, but there are no tests specifically asserting that `__CFBundleIdentifier`, `XPC_SERVICE_NAME`, `XPC_FLAGS`, and `MallocNanoZone` survive both `buildMinimalEnv()` seeding (i.e. are copied from `process.env` when present) and the `stripTerminalRuntimeEnv` pass (i.e. are not removed by any denylist prefix). A test seeding those four keys into `process.env`, calling `initTerminalBaseEnv`, and asserting they appear in `getTerminalBaseEnv()` would protect against accidental future regression in the strip rules.

How can I resolve this? If you propose a fix, please make it concise.

Reviews (1): Last reviewed commit: "fix(desktop): forward macOS XPC bootstra..." | Re-trigger Greptile

Comment thread packages/host-service/src/terminal/clean-shell-env.ts
Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai Bot left a comment

Choose a reason for hiding this comment

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

No issues found across 2 files

Export SHELL_BOOTSTRAP_KEYS so the test can assert on the seed list,
then add one test that pins both halves of the flow:

- the four XPC keys are in SHELL_BOOTSTRAP_KEYS, so buildMinimalEnv()
  copies them from process.env into the helper shell;
- the same keys survive stripTerminalRuntimeEnv into the PTY base env
  (no denylist prefix or exact-key match).

Guards against future regression in either the seed list or the strip
rules.
Copy link
Copy Markdown
Contributor

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

🧹 Nitpick comments (2)
packages/host-service/src/terminal/clean-shell-env.ts (1)

7-7: Optional: freeze the exported allowlist to prevent test mutation.

Now that SHELL_BOOTSTRAP_KEYS is exported, a careless consumer (or test) could .push() into it and pollute every subsequent buildMinimalEnv() call (the cache in getStrictShellEnvironment would also serve poisoned results until cleared). A trivial guard:

♻️ Proposed refactor
-export const SHELL_BOOTSTRAP_KEYS = [
+export const SHELL_BOOTSTRAP_KEYS = Object.freeze([
 	"HOME",
 	...
 	"SYSTEMROOT",
-];
+] as const);

Skip if you'd rather keep the diff minimal — current consumers only read it.

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

In `@packages/host-service/src/terminal/clean-shell-env.ts` at line 7, The
exported array SHELL_BOOTSTRAP_KEYS should be made immutable to prevent external
mutation; update the export so consumers cannot push/pop into it (e.g., export a
frozen copy or expose it as a readonly array) and ensure buildMinimalEnv and
getStrictShellEnvironment continue to read from the immutable version to avoid
poisoned cached results; locate SHELL_BOOTSTRAP_KEYS and change its definition
to return or hold an Object.freeze(...) (or a readonly tuple/type) so
tests/consumers cannot mutate the exported allowlist.
packages/host-service/src/terminal/env.test.ts (1)

221-244: Nit: test placement and a more realistic XPC_SERVICE_NAME value.

Two purely optional polish points:

  1. The test asserts both seed-list membership and getTerminalBaseEnv preservation, but it lives under describe("stripTerminalRuntimeEnv"). The membership assertions are independent of strip behavior; you could either move them to a sibling describe("SHELL_BOOTSTRAP_KEYS") block or rename the enclosing describe to make the dual scope explicit.
  2. XPC_SERVICE_NAME: "0" is not a realistic macOS value (actual values look like application.com.example.app.123.456). Using a representative string makes the test self-documenting and would catch any future quoting/escaping logic that mishandles characters real values contain (., digits, etc.).
♻️ Suggested tweak (value only)
-			XPC_SERVICE_NAME: "0",
+			XPC_SERVICE_NAME: "application.com.example.app.123456.654321",
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/host-service/src/terminal/env.test.ts` around lines 221 - 244, The
test mixes two concerns: membership of SHELL_BOOTSTRAP_KEYS and behavior of
stripTerminalRuntimeEnv/getTerminalBaseEnv; either move the expect(...)
membership assertions into a new or sibling describe block for
SHELL_BOOTSTRAP_KEYS (or rename the current describe to reflect both concerns)
so the test scope is clear, and update the initTerminalBaseEnv call to use a
more realistic XPC_SERVICE_NAME string (e.g.
"application.com.example.app.123.456") to better exercise quoting/escaping
logic; locate these changes around the symbols SHELL_BOOTSTRAP_KEYS,
resetTerminalBaseEnvForTests, initTerminalBaseEnv, and getTerminalBaseEnv in the
test.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@packages/host-service/src/terminal/clean-shell-env.ts`:
- Line 7: The exported array SHELL_BOOTSTRAP_KEYS should be made immutable to
prevent external mutation; update the export so consumers cannot push/pop into
it (e.g., export a frozen copy or expose it as a readonly array) and ensure
buildMinimalEnv and getStrictShellEnvironment continue to read from the
immutable version to avoid poisoned cached results; locate SHELL_BOOTSTRAP_KEYS
and change its definition to return or hold an Object.freeze(...) (or a readonly
tuple/type) so tests/consumers cannot mutate the exported allowlist.

In `@packages/host-service/src/terminal/env.test.ts`:
- Around line 221-244: The test mixes two concerns: membership of
SHELL_BOOTSTRAP_KEYS and behavior of stripTerminalRuntimeEnv/getTerminalBaseEnv;
either move the expect(...) membership assertions into a new or sibling describe
block for SHELL_BOOTSTRAP_KEYS (or rename the current describe to reflect both
concerns) so the test scope is clear, and update the initTerminalBaseEnv call to
use a more realistic XPC_SERVICE_NAME string (e.g.
"application.com.example.app.123.456") to better exercise quoting/escaping
logic; locate these changes around the symbols SHELL_BOOTSTRAP_KEYS,
resetTerminalBaseEnvForTests, initTerminalBaseEnv, and getTerminalBaseEnv in the
test.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: c4cf3755-119a-4491-a2f2-8b6f3bf30725

📥 Commits

Reviewing files that changed from the base of the PR and between d73900e and 35c7019.

📒 Files selected for processing (2)
  • packages/host-service/src/terminal/clean-shell-env.ts
  • packages/host-service/src/terminal/env.test.ts

@WadeSeidule
Copy link
Copy Markdown
Author

@Kitenite sorry if you are the wrong person to tag here, but can I get a review on this? This issue has made Superset basically unusable for me.

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