Hi, I noticed an inconsistency between pledge_kill() in sys/kern/kern_pledge.c and the documented per-category syscall list in pledge(2). Submitting as an inquiry into whether the code or the manpage should be the source of truth -- both options have a straightforward fix.
Observation
-----------
sys/kern/kern_pledge.c (current master):
int
pledge_kill(struct proc *p, pid_t pid)
{
if ((p->p_p->ps_flags & PS_PLEDGE) == 0)
return 0;
if (p->p_pledge & PLEDGE_PROC)
return 0;
if (pid == 0 || pid == p->p_p->ps_pid)
return 0;
return pledge_fail(p, EPERM, PLEDGE_PROC);
}
The `pid == 0 || pid == p->p_p->ps_pid` branch returns 0 regardless of
which promise the process holds. So kill(2) with pid=0 (BSD pgrp-wide
kill) or pid=self is permitted under every pledge category, not just
"proc".
pledge(2) manpage (verbatim from the running OpenBSD 7.7 system):
stdio ... [70+ syscalls listed, no kill(2)]
proc Allows the following process relationship operations:
fork(2), vfork(2), kill(2), getpriority(2), setpriority(2),
setrlimit(2), setpgid(2), setsid(2)
kill(2) is listed only under "proc". The manpage's introduction to stdio
calls it "actions ... that only occur inside the process". The pid==0
exception that lets a stdio-pledged process signal its process group
(parent shell, sibling pipeline processes) does not appear anywhere in
the manpage.
PoC (OpenBSD 7.7 arm64, run as a uid=0 user with setpgid isolation so
the invoking shell is not in the test pgrp):
[parent] pid=99933 pgid=64730 testing pledge("stdio")
[parent] [+] BYPASS CONFIRMED: pledge("stdio")-restricted attacker
SIGKILL'd an unpledged victim in its pgrp.
Full source in POC_kill_pledged_v3.c (attached). The attacker process
calls pledge("stdio", NULL), then kill(0, SIGKILL), and the unpledged
victim process in the attacker's pgrp is terminated by SIGKILL.
Where this matters
------------------
A stdio-pledged process is, per the documented contract, supposed to
only act on already-open file descriptors and process-internal state.
If such a process compromises (parser bug, etc.), the worst observable
side effect should be its own death. With the pid==0 exception, it can
SIGKILL its parent shell or sibling pipeline stages -- documented
behaviour for "proc"-pledged processes, but not for "stdio".
This is not a privilege escalation. It is a sandbox-degradation /
documentation-implementation mismatch.
Two ways to make behaviour and documentation consistent
-------------------------------------------------------
Either documenting the exception or tightening the check would close
the gap.
A. Documentation patch -- add to the "stdio" section of pledge(2):
kill(2) is permitted if pid is 0 or the calling process's PID
(for raise(3) and pgrp-wide self-signalling in shell pipelines).
B. Code patch -- remove the pid==0 universal exception from
pledge_kill(), requiring PLEDGE_PROC for kill(0,...). raise(3) and
abort(3) (pid==self) still work; pgrp-wide kill becomes proc-only.
I have not audited the ports tree to identify programs that rely on
the current behaviour. The pid==self arm is clearly load-bearing
(raise/abort under stdio); the pid==0 arm may be load-bearing for
shell-pipeline patterns and is the discretionary call.
This sits in similar territory to my prior commit to kern_unveil.c
(UNVEIL-01 / #if 0 dead-code path, ok beck@), though narrower: that
was dead code being re-enabled; this is live code diverging from the
documented contract.
Thanks,
Stuart Thomas
--
Kind regards,
Stuart
*please note, there is no expectation for you to read/reply to my email
outside your normal working hours. *
*This email is private and confidential and only intended for the
recipients above. Should you have received this email in error, please
notify me, and then securely delete it.*
POC_kill_pledged_v3.c
Description: Binary data
