CodexBarClaudeWatchdog post-spawn setpgid EACCES race + kill(-pgid) can signal parent process group
Maintainers usually reply within 1 day
Assessment
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Newbie friendliness
- 35/100
Research direction
Read Sources/CodexBarClaudeWatchdog/main.swift alongside Sources/CodexBarCore/Host/Process/SpawnedProcessGroup.swift and ProcessOwnershipReaper.swift, which the issue cites as examples. Check both reported defects: child process group setup and kill targeting, then wait status handling when the parent exits. Done means the watchdog safely terminates and reaps its child while reporting the child's actual exit code; linked pull request #4830 is already open.
Written by the indexing model from the issue text.
Description
Summary
While auditing Sources/CodexBarClaudeWatchdog/main.swift (and comparing it with Sources/CodexBarCore/Host/Process/SpawnedProcessGroup.swift and ProcessOwnershipReaper.swift), we found and reproduced on macOS (Darwin arm64) two defects in CodexBarClaudeWatchdog:
- Post-spawn
setpgidrace (EACCES) + unguardedkill(-pgid)(Sources/CodexBarClaudeWatchdog/main.swift:17-38, 71, 85):posix_spawnpruns withattrp = nil(inheriting the watchdog's process groupgetpgrp()), and_ = setpgid(globalChildPID, globalChildPID)at line 85 fails witherrno = 13 (EACCES)once the child hasexec'd—leavinggetpgid(childPID) == getpgrp(). WhenkillProcessTreeruns,if pgid > 0 { kill(-pgid, SIGTERM/SIGKILL) }checks onlypgid > 0without excludinggetpgrp(), broadcastingSIGTERMandSIGKILLto the watchdog's own process group (killing the watchdog itself onSIGKILLescalation beforewaitpidcan reap the child, as well as any caller sharing the watchdog's process group). killProcessTreereapschildPIDand discards its wait status, causinggetppid() == 1teardown to exit0afterECHILD(Sources/CodexBarClaudeWatchdog/main.swift:28-29, 101, 108-118).
1. Post-spawn setpgid EACCES race and unguarded kill(-pgid) (Sources/CodexBarClaudeWatchdog/main.swift:17-38, 71, 85)
In Sources/CodexBarCore/Host/Process/SpawnedProcessGroup.swift:362-367, child processes are placed into their own process group atomically inside the kernel before exec via POSIX_SPAWN_SETPGROUP + posix_spawnattr_setpgroup(&attributes, 0), and ProcessOwnershipReaper.swift:38 explicitly guards negative-pgid kills with guard group > 0, group != getpgrp() else { return }.
By contrast, Sources/CodexBarClaudeWatchdog/main.swift:
- Spawns the child at line 71 with
attrp = nil:
so the child inherits the watchdog's process group (posix_spawnp(&pid, childPath, nil, nil, cBuffer.baseAddress, environ)getpgrp()). - Calls
_ = setpgid(globalChildPID, globalChildPID)from the parent at line 85 afterposix_spawnpreturns, ignoring errors. Onceposix_spawnphasexec'd the child image in the kernel,setpgid(2)from the parent fails withEACCES(errno = 13), leavingglobalChildPIDin the watchdog's process group (getpgid(childPID) == getpgrp()). - In
killProcessTree(main.swift:18-23, 33-37), checks onlyif pgid > 0:
Whenlet pgid = getpgid(childPID) if pgid > 0 { kill(-pgid, SIGTERM) } ... if pgid > 0 { kill(-pgid, SIGKILL) }setpgidfails withEACCES,pgid == getpgrp() > 0:- Line 20 sends
SIGTERMto-getpgrp()(includingCodexBarClaudeWatchdogitself and any process sharing the watchdog's group when invoked outsideSpawnedProcessGroup.launchPTY). - If the child does not exit within
0.5s, line 34 sendsSIGKILLto-getpgrp(), which immediately terminatesCodexBarClaudeWatchdogitself before lines 111/117 can reapglobalChildPIDor report the exit code.
- Line 20 sends
Live macOS (Darwin) Reproduction
swift -e '
import Darwin
import Foundation
var pid: pid_t = 0
var argv: [UnsafeMutablePointer<CChar>?] = [strdup("/bin/sleep"), strdup("2"), nil]
_ = posix_spawnp(&pid, "/bin/sleep", nil, nil, &argv, environ)
usleep(20_000) // child image has execd
let rc = setpgid(pid, pid)
let err = errno
let childPgid = getpgid(pid)
let myPgrp = getpgrp()
kill(pid, SIGKILL)
var st: Int32 = 0
_ = waitpid(pid, &st, 0)
print("setpgid rc=\(rc) errno=\(err) (EACCES=\(EACCES)), childPgid=\(childPgid), watchdogPgrp=\(myPgrp), sameGroup=\(childPgid == myPgrp)")
'
Output on macOS:
setpgid rc=-1 errno=13 (EACCES=13), childPgid=84710, watchdogPgrp=84710, sameGroup=true
Suggested Fix:
- Pass a
posix_spawnattr_tconfigured solely withPOSIX_SPAWN_SETPGROUP(posix_spawnattr_setpgroup(&attr, 0)) toposix_spawnpatmain.swift:71so the child enters its own process group (pgid == pid) atomically beforeexecwhile preserving existing descriptor inheritance. - Guard negative-pgid kills in
killProcessTree(main.swift:19, 33) withif pgid > 0, pgid != getpgrp() { kill(-pgid, ...) } else { kill(childPID, ...) }(matchingProcessOwnershipReaper.swift:38).
2. Double waitpid in CodexBarClaudeWatchdog discards exit status on getppid() == 1 (main.swift:28-29, 101, 115-118)
In killProcessTree (main.swift:26-31), when childPID exits during the 0.5s SIGTERM grace period:
var status: Int32 = 0
while Date() < deadline {
let rc = waitpid(childPID, &status, WNOHANG)
if rc == childPID { return }
usleep(50000)
}
waitpid reaps childPID into the local status variable and returns without propagating status. Then in the main loop (lines 101, 115-118):
var status: Int32 = 0
while true {
...
if getppid() == 1 {
terminateChild()
_ = waitpid(globalChildPID, &status, 0)
Darwin.exit(exitCode(fromWaitStatus: status))
}
the second waitpid(globalChildPID, &status, 0) fails with -1 (errno = ECHILD), leaving the outer status at 0 (initialized at line 101), so Darwin.exit(exitCode(fromWaitStatus: status)) exits with 0 instead of 143 (128 + SIGTERM).
Live macOS (Darwin) Reproduction
swift -e '
import Darwin
import Foundation
var pid: pid_t = 0
var argv: [UnsafeMutablePointer<CChar>?] = [strdup("/bin/sleep"), strdup("10"), nil]
_ = posix_spawnp(&pid, "/bin/sleep", nil, nil, &argv, environ)
// 1. killProcessTree (main.swift:17-31)
kill(pid, SIGTERM)
var innerStatus: Int32 = 0
for _ in 0..<10 {
if waitpid(pid, &innerStatus, WNOHANG) == pid { break }
usleep(50_000)
}
let actualExit = (innerStatus & 0x7F) != 0 ? (128 + (innerStatus & 0x7F)) : ((innerStatus >> 8) & 0xFF)
// 2. Main loop getppid() == 1 branch (main.swift:101, 116-118)
var outerStatus: Int32 = 0
let secondWait = waitpid(pid, &outerStatus, 0)
let secondErrno = errno
let reportedExit = (outerStatus & 0x7F) != 0 ? (128 + (outerStatus & 0x7F)) : ((outerStatus >> 8) & 0xFF)
print("firstWait actualExit=\(actualExit) (128+SIGTERM), secondWait rc=\(secondWait) errno=\(secondErrno) (ECHILD=\(ECHILD)), watchdog reportedExit=\(reportedExit)")
'
Output on macOS:
firstWait actualExit=143 (128+SIGTERM), secondWait rc=-1 errno=10 (ECHILD=10), watchdog reportedExit=0
Suggested Fix:
Have killProcessTree / terminateChild return Int32? with the reaped wait status when waitpid(childPID, &status, WNOHANG) == childPID, and only call blocking waitpid(globalChildPID, &status, 0) at lines 111/117 when terminateChild() returns nil.
Note on Falsified Findings (Filtered Out Before Filing)
We ran this audit using a 4-pass Ponder session (Builder, Skeptic, Dreamer, Empath). On Pass 1, CredentialFileWriter.swift was flagged for potential permission-window / symlink exposure; on Pass 2, Skeptic inspected CredentialFileWriter.writePrivate (Sources/CodexBarCore/CredentialFileWriter.swift:38-74), verified its 0700 staging directory, O_CREAT | O_EXCL 0600 descriptor creation, fchmod(0600) before write, synchronize(), and atomic rename(2), and retracted that finding (op: "delete") before filing.
- Full 4-pass convergence report & apply plan:
studies/ecosystem-audits/codexbar/report.md
Generated by Ponder — AI that knows when to speak, and when to shut up.
- Dominant language
- Swift
- Stars
- 22.8k
- Forks
- 2.2k
- Avg merge
- 21h 49m
- Merged PRs (30d)
- 368
Getting set up
This project ships no dev container, Dockerfile or contributing guide, so setting up is up to you: start from its README, and see our first-contribution guide for the general steps.
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
More from steipete/CodexBar
-
Improve readability of red pace text in stacked menu bar layoutPossibly taken @steipete claimed this 2 days ago. Openclawsweeper:fix-shape-clear clawsweeper:needs-live-repro clawsweeper:needs-maintainer-review clawsweeper:needs-product-decision clawsweeper:no-new-fix-pr impact:ux-friction issue-rating: 🐚 platinum hermit P2
Difficulty 2/5 1-3 hours Newbie friendliness 62/100
steipete/CodexBar#4714 · 3 comments · 1 reaction ·
Maintainers usually reply within 1 day
-
clawsweeper:fix-shape-clear clawsweeper:needs-maintainer-review clawsweeper:needs-product-decision clawsweeper:no-new-fix-pr clawsweeper:source-repro impact:other issue-rating: 🦞 diamond lobster P3
Difficulty 5/5 Over a week Newbie friendliness 38/100
steipete/CodexBar#4833 · 4 comments · 1 reaction ·
Maintainers usually reply within 1 day
-
MiniMax China web-session discovery does not include minimax.cnPossibly taken @tangwz claimed this 1 day ago. Openclawsweeper:linked-pr-open clawsweeper:no-new-fix-pr clawsweeper:source-repro impact:auth-provider issue-rating: 🦞 diamond lobster P2
Difficulty 4/5 3-5 days Newbie friendliness 35/100
steipete/CodexBar#4831 · 1 comment · 1 reaction ·
Maintainers usually reply within 1 day
-
clawsweeper:needs-product-decision clawsweeper:needs-security-review clawsweeper:no-new-fix-pr clawsweeper:source-repro impact:auth-provider impact:ux-release-blocker issue-rating: 🦞 diamond lobster P0
Difficulty 4/5 3-5 days Newbie friendliness 43/100
steipete/CodexBar#4827 · 1 comment · 1 reaction ·
Maintainers usually reply within 1 day
-
Perplexity: organization pool credits are ignored, Credits shows 100% while the member still has org creditsPossibly taken @Abelo9996 claimed this 1 day ago. Openclawsweeper:fix-shape-clear clawsweeper:queueable-fix clawsweeper:source-repro impact:ux-friction issue-rating: 🦞 diamond lobster no-stale P2
Difficulty 3/5 1-2 days Newbie friendliness 35/100
steipete/CodexBar#4826 · 1 comment · 1 reaction ·
Maintainers usually reply within 1 day
All issues in steipete/CodexBar
Similar issues
-
area: browser
Difficulty 2/5 1-3 hours Newbie friendliness 62/100
manaflow-ai/cmux#27619 · 1 comment ·
Maintainers usually reply within 1 day
-
Difficulty 1/5 Under an hour Newbie friendliness 82/100
matteocrippa/awesome-swift#2028 ·
Maintainers usually reply within 3 days
-
documentation
Difficulty 2/5 1-3 hours Newbie friendliness 66/100
Maintainers usually reply within 1 day
-
CTC keyword spotter hears silence: MelSpectrogram on the Neural Engine yields all-blank CTC framesOpen
Difficulty 2/5 1-3 hours Newbie friendliness 78/100
FluidInference/FluidAudio#1074 ·
Maintainers usually reply within 1 day
-
Difficulty 2/5 1-3 hours Newbie friendliness 76/100
paritytech/trinity-user-agents#1783 ·
Maintainers usually reply within 1 day