Skip to content

fix: make shm cache files owner-private — they leak/accept pickle data (#39)#82

Merged
toloco merged 1 commit into
masterfrom
fix/39-shm-file-permissions
Jun 18, 2026
Merged

fix: make shm cache files owner-private — they leak/accept pickle data (#39)#82
toloco merged 1 commit into
masterfrom
fix/39-shm-file-permissions

Conversation

@toloco

@toloco toloco commented Jun 18, 2026

Copy link
Copy Markdown
Owner

Closes #39

What & why

The mmap cache files were created in a shared directory (/dev/shm on Linux, $TMPDIR/warp_cache otherwise) with default umask permissions (typically 0644, world-readable) and predictable names (warp_cache_<module>_<qualname>_<hash>). On a multi-user host (shared server, CI runner, shared-tmp container) this is a local trust-boundary issue:

  • Confidentiality: any local user can read another user's cache file, which contains serialized — and for the pickle fallback, pickled — return values.
  • Integrity → potential RCE: a co-located hostile user can pre-create the predictably-named file with a crafted pickle payload. create_or_open opens a pre-existing file if the magic/version/sizes match (all forgeable), and deserialize_value falls back to pickle.loads on the bytes — so the victim deserializes attacker-controlled data.

The fix

  • Per-user directory warp_cache-<uid> created 0o700 (under /dev/shm on Linux, $TMPDIR otherwise). This 0o700 dir is what actually closes both vectors — other users can neither read files in it nor create files in it.
  • Every cache file (.data/.lock/.init) created 0o600 via OpenOptionsExt::mode, as defense in depth (and so the result is deterministic regardless of umask).
  • ensure_secure_dir refuses to use a pre-existing dir that isn't a directory, is owned by another user, or is group/other-accessible — defending the /dev/shm (world-writable, sticky) pre-creation/TOCTOU case where an attacker pre-creates our uid-scoped dir. It uses symlink_metadata so a symlink can't redirect us, and tightens loose perms on a dir we own.

The shm module is non-Windows only, so the unix-specific permission code always applies where the shared backend exists.

Test

TestSharedFilePermissions::test_shm_dir_and_files_are_owner_only — after creating a shared cache, asserts the per-user dir is 0o700 and every shm file has no group/other bits (mode & 0o077 == 0).

Verified fail-before → pass-after by stashing just the region.rs change: the pre-fix build doesn't place files in the secure per-user dir (and creates them 0o644); with the fix the dir is 0o700 and files are 0o600.

Test shm-dir helpers across the suite are updated for the new per-user layout (with a Windows guard where a helper is collected on Windows).

Follow-up

This fixes the file-permission half of the shared-backend risk. The pickle-deserialization documentation gap is tracked separately in #46 (SECURITY.md); the 0o700 dir already removes the realistic tampering path for the pickle vector here.

Gates run (risky — src/shm/region.rs, security/file-I/O)

  • make fmt / make lint (ruff, ty, clippy -D warnings) ✓
  • make testcargo test (11) + pytest (107, +1 new) ✓
  • make test-matrix — Python 3.10–3.13 ✓, the new test + cross-process cold-start test pass on each (3.14 skipped locally via the documented uv-resolves-stale-alpha guard; CI covers 3.14 final)
  • make bench — no regression (construction-time-only change): shared backend ~9.5M ops/s, hit-rate 72.9%

🤖 Generated with Claude Code

The mmap cache files were created in a shared directory (/dev/shm on Linux,
$TMPDIR/warp_cache otherwise) with default umask permissions (typically 0644,
world-readable) and predictable names derived from module+qualname. On a
multi-user host any local user could read another user's cache — which holds
serialized (and possibly pickled) return values — or, worse, pre-create the
predictably-named file with a crafted pickle payload that the victim then feeds
to pickle.loads (the deserialize fallback), i.e. potential code execution.

Harden the shm layout:
- Store files in a per-user directory `warp_cache-<uid>` created 0o700 (under
  /dev/shm on Linux, $TMPDIR otherwise). The 0o700 directory is what actually
  closes both vectors: other users can neither read files in it nor create files
  in it.
- Create every cache file (.data/.lock/.init) with mode 0o600 as defense in depth.
- `ensure_secure_dir` refuses to use a pre-existing directory that is not a
  directory, is owned by another user, or is group/other-accessible (a hostile
  user could pre-create our uid-scoped dir on the world-writable, sticky
  /dev/shm); it tightens loose perms on a dir we own. Uses symlink_metadata so a
  symlink can't redirect us elsewhere.

Add a regression test asserting the per-user dir is 0o700 and every shm file is
owner-only (mode & 0o077 == 0); verified fail-before -> pass-after. Update the
test shm-dir helpers for the new per-user layout, and document the invariant in
ARCHITECTURE.

The shm module is non-Windows only, so the unix-specific perms code always
applies where the shared backend exists.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@toloco toloco merged commit 80ea259 into master Jun 18, 2026
14 checks passed
@toloco toloco deleted the fix/39-shm-file-permissions branch June 18, 2026 10:22
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.

[medium] shm files created with default permissions (world-readable) in a shared directory expose cached pickled data

1 participant