Bug Description
forge segfaults on macOS arm64 (SIGSEGV, EXC_BAD_ACCESS, KERN_INVALID_ADDRESS at a tiny non-zero address, on a tokio-rt-worker thread) whenever the machine has a user account whose primary GID has no corresponding group entry.
I traced this from the crash report through the stripped binary's disassembly to the exact upstream source. The root cause is in the sysinfo crate's get_group_name(), which misuses the POSIX getgrgid_r contract.
getgrgid_r returns 0 for "group not found." It signals the miss by setting the result out-param to NULL, and in that case it leaves the caller's struct group completely untouched. Checking only the return value is not sufficient.
sysinfo/src/unix/users.rs:
pub(crate) unsafe fn get_group_name(id: libc::gid_t, buffer: &mut Vec<libc::c_char>) -> Option<String> {
let mut g = std::mem::MaybeUninit::<libc::group>::uninit();
let mut tmp_ptr = std::ptr::null_mut();
...
if retry_eintr!(set_to_0 => last_errno => getgrgid_r(
id as _, g.as_mut_ptr() as _, buffer.as_mut_ptr(),
buffer.capacity() as _, &mut tmp_ptr as _
)) != 0
{ ... return None; }
break;
}
let g = g.assume_init(); // <-- UB: never written on a miss
super::utils::cstr_to_rust(g.gr_name) // <-- dereferences stack garbage
}
}
tmp_ptr is declared and passed, but its value is never read. On a "not found" result the function calls assume_init() on memory getgrgid_r never wrote, then walks g.gr_name as a NUL-terminated C string. Whatever stale value happens to sit in that stack slot is dereferenced.
Verification
12 crash reports on this machine over ~1 week, all with byte-identical top frames:
signal: SIGSEGV / EXC_BAD_ACCESS
subtype: KERN_INVALID_ADDRESS at 0x0000000000000001
thread: "tokio-rt-worker"
lifetime: 168 ms - 12 s
Disassembling the fault site in the shipped (stripped) binary shows the missing check directly. The result out-param is zeroed at sp+0x58 before the call and never loaded again:
1013c7cec: str xzr, [sp, #0x58] ; tmp_ptr = NULL
1013c7d14: bl getgrgid_r ; x1=&group (sp+0x38), x4=&tmp_ptr (sp+0x58)
1013c7d3c: cbz w0, 0x1013c7d74 ; ret == 0 -> assumed success
...
1013c7d74: ldr x8, [sp, #0x38] ; g.gr_name (never written by libc)
1013c7d84: cbz x8, ... ; only a NULL check
1013c7d88: ldrb w23, [x8] ; <-- SIGSEGV, x8 = 0x1
Confirming the libc semantics on macOS 26.6.2:
struct group g, *res = NULL; char buf[2048];
g.gr_name = (char *)0x1; // stale stack value
int r = getgrgid_r(4242424, &g, buf, sizeof buf, &res);
// r = 0, errno = 0, res = NULL, g.gr_name = 0x1 (left untouched)
gid=20 ret=0 result=0x16b569918 gr_name=0x16b569940
gid=4242424 ret=0 result=0x0 gr_name=0x1 <-- returns 0 but NOT FOUND
The trigger on my machine is a service account installed by an endpoint-management agent (Automox): _automoxserviceaccount has PrimaryGroupID: 1000, and no group with GID 1000 exists. It is the only such account out of 268 passwd entries — but one is enough, since forge enumerates all of them at startup.
$ dscl . -read /Users/_automoxserviceaccount PrimaryGroupID
PrimaryGroupID: 1000
$ dscl . -search /Groups PrimaryGroupID 1000
(no results)
This also explains why the crash is intermittent for some people: it only faults when the stale stack slot happens to be non-zero. When it happens to be 0, the cbz NULL check catches it and forge survives.
Steps to Reproduce
- On macOS, have any user account whose primary GID has no matching group. Any machine with Automox installed will have this; otherwise it can be created for testing.
- Run any
forge command, e.g. forge -p "hello".
zsh: segmentation fault forge -p "hello", exit status 139.
Maintainers can reproduce the faulty branch directly without such an account by calling sysinfo's group lookup with any GID that has no group (e.g. 4242424) and observing that getgrgid_r returns 0 while leaving struct group uninitialized.
Expected Behavior
forge starts normally. A GID that does not resolve to a group should yield None (or be skipped), not a dereference of uninitialized memory.
Actual Behavior
Immediate SIGSEGV before the session gets anywhere. Exit status 139. Reproduces on every invocation.
Suggested Fix
Check the result out-param, which is the documented way to distinguish "found" from "not found":
break;
}
if tmp_ptr.is_null() {
return None; // no group with this gid
}
let g = g.assume_init();
super::utils::cstr_to_rust(g.gr_name)
This is an upstream sysinfo bug and is worth reporting there as well, but it reaches users through forge, and forge can pick it up via a dependency bump once fixed. Note also that Cargo.lock pins sysinfo 0.29.11 while Cargo.toml requests 0.38.3, which may be worth a look independently.
Possibly related
#2645 reported the same signature — KERN_INVALID_ADDRESS at 0x0000000000000002, faulting thread tokio-rt-worker, "Data Abort — byte read Translation fault". It was closed as fixed by #2794, but that PR replaced an unrelated transmute_copy in forge_select. The byte-read-at-a-tiny-address crash on a tokio worker is still present in 2.13.21, and I believe this is its actual cause.
Forge Version
2.13.21
Operating System & Version
macOS 26.6.2 (build 25G83), Apple Silicon arm64 (Mac16,7)
AI Provider
Other / OpenAI-compatible
Model
qwen/qwen3.8-27b (irrelevant — the crash happens before any model call completes)
Installation Method
curl -fsSL https://forgecode.dev/cli | sh (pre-built binary)
Bug Description
forgesegfaults on macOS arm64 (SIGSEGV,EXC_BAD_ACCESS,KERN_INVALID_ADDRESSat a tiny non-zero address, on atokio-rt-workerthread) whenever the machine has a user account whose primary GID has no corresponding group entry.I traced this from the crash report through the stripped binary's disassembly to the exact upstream source. The root cause is in the
sysinfocrate'sget_group_name(), which misuses the POSIXgetgrgid_rcontract.getgrgid_rreturns0for "group not found." It signals the miss by setting theresultout-param toNULL, and in that case it leaves the caller'sstruct groupcompletely untouched. Checking only the return value is not sufficient.sysinfo/src/unix/users.rs:tmp_ptris declared and passed, but its value is never read. On a "not found" result the function callsassume_init()on memorygetgrgid_rnever wrote, then walksg.gr_nameas a NUL-terminated C string. Whatever stale value happens to sit in that stack slot is dereferenced.Verification
12 crash reports on this machine over ~1 week, all with byte-identical top frames:
Disassembling the fault site in the shipped (stripped) binary shows the missing check directly. The
resultout-param is zeroed atsp+0x58before the call and never loaded again:Confirming the libc semantics on macOS 26.6.2:
The trigger on my machine is a service account installed by an endpoint-management agent (Automox):
_automoxserviceaccounthasPrimaryGroupID: 1000, and no group with GID 1000 exists. It is the only such account out of 268passwdentries — but one is enough, sinceforgeenumerates all of them at startup.This also explains why the crash is intermittent for some people: it only faults when the stale stack slot happens to be non-zero. When it happens to be
0, thecbzNULL check catches it andforgesurvives.Steps to Reproduce
forgecommand, e.g.forge -p "hello".zsh: segmentation fault forge -p "hello", exit status 139.Maintainers can reproduce the faulty branch directly without such an account by calling
sysinfo's group lookup with any GID that has no group (e.g.4242424) and observing thatgetgrgid_rreturns0while leavingstruct groupuninitialized.Expected Behavior
forgestarts normally. A GID that does not resolve to a group should yieldNone(or be skipped), not a dereference of uninitialized memory.Actual Behavior
Immediate
SIGSEGVbefore the session gets anywhere. Exit status 139. Reproduces on every invocation.Suggested Fix
Check the
resultout-param, which is the documented way to distinguish "found" from "not found":This is an upstream
sysinfobug and is worth reporting there as well, but it reaches users throughforge, andforgecan pick it up via a dependency bump once fixed. Note also thatCargo.lockpinssysinfo 0.29.11whileCargo.tomlrequests0.38.3, which may be worth a look independently.Possibly related
#2645 reported the same signature —
KERN_INVALID_ADDRESS at 0x0000000000000002, faulting threadtokio-rt-worker, "Data Abort — byte read Translation fault". It was closed as fixed by #2794, but that PR replaced an unrelatedtransmute_copyinforge_select. The byte-read-at-a-tiny-address crash on a tokio worker is still present in 2.13.21, and I believe this is its actual cause.Forge Version
2.13.21
Operating System & Version
macOS 26.6.2 (build 25G83), Apple Silicon arm64 (Mac16,7)
AI Provider
Other / OpenAI-compatible
Model
qwen/qwen3.8-27b (irrelevant — the crash happens before any model call completes)
Installation Method
curl -fsSL https://forgecode.dev/cli | sh (pre-built binary)