Skip to content

Fix sentry-init racing environ mutation during global init - #174

Open
azooz2003-bit wants to merge 1 commit into
mainfrom
fix/sentry-init-environ-race
Open

Fix sentry-init racing environ mutation during global init#174
azooz2003-bit wants to merge 1 commit into
mainfrom
fix/sentry-init-environ-race

Conversation

@azooz2003-bit

@azooz2003-bit azooz2003-bit commented Jul 30, 2026

Copy link
Copy Markdown

Fixes the iOS crash that hit cmux INTERNAL three times on 2026-07-30 (builds 20260730090940 and 20260730213932, twice back-to-back): EXC_BAD_ACCESS byte read on the sentry-init thread while the main thread was inside ghostty_init -> ensureLocale -> setlocale during the first terminal-surface mount of a session.

Mechanism: ghostty_init snapshots the raw C environ pointer array. global.init spawned the sentry-init thread (crash.init) BEFORE ensureLocale(), whose setenv("LANG"/"LANGUAGE") can realloc the environ array and free the snapshotted block; syncEnviron() re-syncs only after. On iOS (non-macOS branch), sentry-init then walked the freed snapshot via global.environMap() (src/crash/sentry.zig), which has no concurrency control, and byte-scanned a garbage entry pointer (near-null 0x2fb1 in one crash, wild pointers in the other two, matching freed-block reuse).

Fix, two layers:

  • global.init runs ensureLocale() + syncEnviron() before crash.init, so no other thread exists while init mutates the environment.
  • sentry.init resolves the Sentry cache directory on the spawning thread from an explicit Environ.Map and passes it into initThread; the spawned thread no longer touches the shared environ snapshot at all, so this crash class stays dead even if some later code calls setenv after startup.

The regression test pins the seam: cache-dir resolution must consume the caller-provided map (whose test value does not exist in the live process environment), so it fails if resolution ever reaches back into global process state; a mutation check confirmed the test executes and fails when broken (73 pass, 1 fail). A second macOS-gated test pins the preserved NSCachesDirectory fallback for unset/empty XDG_CACHE_HOME. A red/green two-commit structure is not applicable here: the defect is a data race whose pre-fix failure mode is undefined behavior, and the testable seam does not exist pre-fix.

Verified with zig build test -Dtest-filter="sentry cache dir" on zig 0.16.0 (86/86 steps, 74/74 tests).

Full triage with the crash evidence and most-likely-cause ranking lives in cmuxterm-hq out/crash-20260730/triage.md; the cmux submodule bump PR follows and must merge only after this lands on main.

🤖 Generated with Claude Code


View with [code]smith Autofix with [code]smith
Need help on this PR? Tag @codesmith-bot with what you need. Autofix is disabled.


Summary by cubic

Fixes an iOS startup crash caused by the sentry-init thread reading a freed environ snapshot during locale setup. We now finish locale/env setup before spawning Sentry and avoid reading the global env from the Sentry thread.

  • Bug Fixes
    • Run ensureLocale() + syncEnviron() before crash.init so no thread reads the environment while it’s being mutated.
    • Resolve the Sentry cache directory on the spawning thread from an explicit Environ.Map, pass it into initThread, and stop touching global.environ in the spawned thread.
    • Add regression tests to ensure cache-dir resolution uses the caller-provided map and to keep the macOS fallback to NSCachesDirectory when XDG_CACHE_HOME is unset or empty.

Written for commit abcf569. Summary will update on new commits.

Review in cubic

Three iOS crashes on 2026-07-30 (cmux INTERNAL builds 20260730090940 and
20260730213932) shared one interleaving: the main thread was inside
ghostty_init -> ensureLocale -> setlocale during the first terminal-surface
mount, and the sentry-init thread died on a byte-read SIGSEGV walking the
environ snapshot. ensureLocale's setenv("LANG"/"LANGUAGE") can realloc the
environ pointer array, freeing the block the ghostty_init snapshot points
at; syncEnviron() only re-syncs after ensureLocale returns, and crash.init
had already spawned sentry-init, which read the stale snapshot through
global.environMap() with no concurrency control.

Two changes close the race:

- global.init now runs ensureLocale() + syncEnviron() BEFORE crash.init,
  so no other thread exists while the environment is mutated.
- sentry.init resolves the Sentry cache directory on the spawning thread
  from an explicit environ map and passes it into initThread; the spawned
  thread no longer touches the shared environ snapshot at all, so any
  later setenv on another thread cannot revive this crash class.

The regression test pins the new seam: cache-dir resolution must consume
the caller-provided map (a value absent from the live process
environment), so it fails if the resolution ever reaches back into global
process state. A second test pins the preserved macOS behavior (empty or
unset XDG_CACHE_HOME falls back to NSCachesDirectory).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jul 30, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Sentry cache resolution now occurs on the spawning thread using a caller-provided environment map. Global initialization synchronizes locale and environment state before crash reporting initialization, and targeted tests cover environment isolation and macOS fallback behavior.

Changes

Sentry environment initialization

Layer / File(s) Summary
Move environment synchronization earlier
src/global.zig
ensureLocale() and syncEnviron() now run before crash.init(), replacing the later initialization block.
Pass resolved cache directory to Sentry thread
src/crash/sentry.zig
init() resolves and passes cache_dir to initThread, which no longer reads global environment state and now owns cleanup; tests cover supplied environment maps and macOS fallback resolution.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Suggested reviewers: mitchellh

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly captures the main fix: a race between sentry init and environment mutation during global initialization.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
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.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/sentry-init-environ-race

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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/crash/sentry.zig`:
- Around line 365-382: Extend the macOS cache-directory tests around
resolveCacheDir with a separate case whose environ_map contains no
XDG_CACHE_HOME entry. Verify it also resolves to the NSCachesDirectory-based
path ending in “sentry”, while keeping the existing empty-value case intact.
🪄 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: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: c271c41f-8267-4420-8d74-c716453550ca

📥 Commits

Reviewing files that changed from the base of the PR and between ad6984c and abcf569.

📒 Files selected for processing (2)
  • src/crash/sentry.zig
  • src/global.zig

Comment thread src/crash/sentry.zig
Comment on lines +365 to +382
test "sentry cache dir prefers NSCachesDirectory on macOS without XDG" {
// Pins the behavior the refactor preserved from the old thread-side
// code: on macOS an unset OR empty XDG_CACHE_HOME falls back to the
// NSCachesDirectory-based path instead of the XDG path.
if (comptime builtin.os.tag != .macos) return error.SkipZigTest;

const testing = std.testing;
const alloc = testing.allocator;

var environ_map = try testing.environ.createMap(alloc);
defer environ_map.deinit();
try environ_map.put("XDG_CACHE_HOME", "");

const cache_dir = try resolveCacheDir(alloc, &environ_map);
defer alloc.free(cache_dir);

try testing.expect(std.mem.indexOf(u8, cache_dir, "Caches") != null);
try testing.expect(std.mem.endsWith(u8, cache_dir, "sentry"));

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Cover the unset XDG_CACHE_HOME fallback separately.

This test sets XDG_CACHE_HOME to an empty value, but never exercises the missing-key branch of environ_map.get(...) orelse "". Add a macOS case where the supplied map has no XDG_CACHE_HOME entry, matching the stated unset-or-empty contract.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/crash/sentry.zig` around lines 365 - 382, Extend the macOS
cache-directory tests around resolveCacheDir with a separate case whose
environ_map contains no XDG_CACHE_HOME entry. Verify it also resolves to the
NSCachesDirectory-based path ending in “sentry”, while keeping the existing
empty-value case intact.

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