Skip to content

fix(daemon): serialize the umask swap around the socket bind - #16

Merged
paveq merged 1 commit into
mainfrom
fix/umask-test-race
Sep 23, 2026
Merged

paveq merged 1 commit into
mainfrom
fix/umask-test-race

Conversation

@paveq

@paveq paveq commented Sep 23, 2026

Copy link
Copy Markdown
Contributor

Why

CI sometimes fails with:

socket /tmp/.tmpXXXX/airlock.sock has insecure permissions 0o755 (expected 0o700)

It failed run_embedded_exits_on_cancel on #14 (a docs-only PR) and synchronous_startup_explicit_path_uses_parent_as_sandbox_root on #13.

A socket's mode comes from the umask at bind time, and the umask is process-wide. synchronous_startup and four socket tests in src/daemon.rs each did umask(077) → bind → umask(old) without a lock, and cargo test runs them on parallel threads:

  1. Thread A calls umask(077), which returns the default 022.
  2. Thread B calls umask(077), which returns 077.
  3. Thread A restores 022.
  4. Thread B binds its socket, which gets mode 0755.
  5. The post-bind check correctly refuses it.

Change

  • New bind_owner_only(path) in src/daemon.rs. It does the umask swap and the bind under a static UMASK_LOCK.
  • synchronous_startup and the four socket-permission tests now call it.
  • CLAUDE.md's line reference to the bind-and-verify block is updated.

The lock is in production code rather than a test-only mutex in test_support. The integration test binaries (daemon_integration, e2e_helpers users) also call synchronous_startup from parallel tests, and a test-only mutex wouldn't reach them. The real daemon binds before any other thread exists, so it never waits on the lock.

Only the socket depends on the umask. The proxy CA certificate already fixes its mode with fchmod.

Verification

  • cargo fmt, cargo clippy --all-targets -D warnings: clean.
  • cargo test --lib: 480 passed. cargo test --test daemon_integration: 13 passed, 1 ignored.
  • Stress run on macOS: daemon:: lib tests, 200 iterations with --test-threads=16.
    • main's code: 5 / 200 runs failed, with the insecure-permissions panic in verify_socket_permissions_accepts_owner_only and run_embedded_exits_on_cancel.
    • This branch: 0 / 200.

🤖 Generated with Claude Code

CI fails now and then with "socket ... has insecure permissions 0o755".
It hit run_embedded_exits_on_cancel on #14 and
synchronous_startup_explicit_path_uses_parent_as_sandbox_root on #13.

The socket gets its mode from the umask at bind time, and the umask is
process-wide. synchronous_startup and four socket tests each did
umask(077) / bind / umask(old) with no lock, and cargo test runs them on
parallel threads. If one thread restores 022 between another thread's
swap and its bind, that socket comes out 0755. The post-bind check then
rightly refuses it.

The swap now lives in bind_owner_only, behind a static mutex, and
synchronous_startup and the tests both go through it. The lock is in
production code, not a test-only mutex, so the integration test
binaries that call synchronous_startup in parallel are covered too. The
real daemon binds before any other thread exists, so it never waits on
the lock.

Stress run of `daemon::` lib tests, 200 iterations at 16 threads: 5
failures before, 0 after.
@paveq
paveq merged commit b0c8b75 into main Sep 23, 2026
4 checks passed
@paveq
paveq deleted the fix/umask-test-race branch September 23, 2026 11:59
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