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.*

Attachment: POC_kill_pledged_v3.c
Description: Binary data

Reply via email to