fix(oak-render): fold over-long POSIX shm names for macOS

macOS caps shm_open names at PSHMNAMLEN (31 bytes including the leading
slash) and answers ENAMETOOLONG beyond that, while Linux allows 255. The
dispatcher tests name segments oak-procpool-ut-<pid>-<label>, so every
procpool test failed on macOS as soon as the earlier crash stopped
masking them. Fold longer keys into a deterministic FNV-1a short name in
one place; owner, worker and unlink all derive the name through it, so
they keep meeting on the same segment.
This commit is contained in:
2026-09-24 16:22:39 +08:00
parent 9a7d0765a5
commit 61cb4b71ed
+62 -3
View File
@@ -1405,6 +1405,31 @@ impl SharedMemoryRegion {
format!("olive-rw-{owner_pid}-{worker_index}")
}
/// The `shm_open` name for `key`. POSIX names must start with a single
/// slash and contain no others. macOS additionally caps them at
/// `PSHMNAMLEN` (31 bytes including the slash) and answers
/// `ENAMETOOLONG` beyond that, while Linux allows 255 — so longer
/// keys are folded into a deterministic short name. Both the owner
/// and the worker derive the name from the same key through here, so
/// they always agree.
#[cfg(unix)]
fn shm_name_for_key(key: &str) -> String {
const MAX: usize = 31;
let raw = format!("/{}", key.replace('/', "_"));
if raw.len() <= MAX {
return raw;
}
// FNV-1a 64: stable across processes and releases (unlike
// `DefaultHasher`, whose algorithm is not a compatibility
// surface).
let mut hash: u64 = 0xcbf2_9ce4_8422_2325;
for byte in raw.as_bytes() {
hash ^= u64::from(*byte);
hash = hash.wrapping_mul(0x0000_0100_0000_01b3);
}
format!("/oak-{hash:016x}")
}
// -------------------------------------------------------------------
// Unix backend (POSIX shm_open/mmap/munmap/shm_unlink)
// -------------------------------------------------------------------
@@ -1416,7 +1441,7 @@ impl SharedMemoryRegion {
/// segment stays valid — POSIX unlinks only remove the name.
#[cfg(unix)]
pub fn unlink_key(key: &str) {
let shm_name = format!("/{}", key.replace('/', "_"));
let shm_name = Self::shm_name_for_key(key);
if let Ok(name_c) = std::ffi::CString::new(shm_name) {
// SAFETY: a NUL-terminated name; unlink is safe whether or not
// the segment exists (ENOENT is ignored by the caller).
@@ -1518,8 +1543,9 @@ impl SharedMemoryRegion {
}
// POSIX shared-memory names must start with a single slash and
// contain no others.
let shm_name = format!("/{}", key.replace('/', "_"));
// contain no others; over-long names are folded (macOS caps POSIX
// shm names at 31 bytes).
let shm_name = Self::shm_name_for_key(key);
let name_c = match std::ffi::CString::new(shm_name.clone()) {
Ok(c) => c,
Err(_) => {
@@ -2921,6 +2947,39 @@ mod tests {
assert_eq!(SharedMemoryRegion::make_key(1, 0), "olive-rw-1-0");
}
#[cfg(unix)]
#[test]
fn region_shm_name_folds_long_keys_for_macos() {
// macOS rejects POSIX shm names longer than PSHMNAMLEN (31 bytes
// including the leading slash) with ENAMETOOLONG; longer keys must
// fold to a deterministic short name so an owner and a worker
// still meet on the same segment.
let long = "oak-procpool-ut-4242-audio-release";
let name = SharedMemoryRegion::shm_name_for_key(long);
assert!(name.len() <= 31, "folded name too long: {name}");
assert!(name.starts_with('/') && !name[1..].contains('/'), "{name}");
assert_eq!(
name,
SharedMemoryRegion::shm_name_for_key(long),
"the fold is deterministic"
);
assert_ne!(
name,
SharedMemoryRegion::shm_name_for_key("oak-procpool-ut-4242-audio-other"),
"different keys keep different names"
);
// Short keys keep their exact spelling.
assert_eq!(
SharedMemoryRegion::shm_name_for_key("olive-rw-4242-0"),
"/olive-rw-4242-0"
);
assert_eq!(
SharedMemoryRegion::shm_name_for_key("a/b/c"),
"/a_b_c",
"slashes become underscores before folding"
);
}
#[test]
fn region_shm_spike_512mb_segment() {
// M15 S1 shm spike: one POSIX shm segment must ftruncate to at