Title: A quick look at __pledge_open(2)
Date: 2026-04-02 14:30

A recent article of the OpenBSD journal caught me attention: [Pledge changes in
7.9-beta]( https://undeadly.org/cgi?action=article;sid=20260320085305 )
([archive.org mirror](
https://web.archive.org/web/20260320154656/https://undeadly.org/cgi?action=article;sid=20260320085305
) as it's currently offline).

The [quoted message](https://marc.info/?l=openbsd-ports&m=177389567528083&w=2)
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](https://isopenbsdsecu.re/mitigations/pinsyscall/), which is an
indirect call away. Let's check if this is indeed a sandbox escape. The
function
[`setservent_r`](https://github.com/openbsd/src/blob/4245707926efbf4b027899ae0328f0c436ca7e10/lib/libc/net/getservent.c#L47)
has a call to `__pledge_open`, so let's jump directly on the `call` to please
pinsyscall:

```C
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <signal.h>
#include <fcntl.h>
#include <sys/types.h>
#include <sys/ptrace.h>
#include <string.h>

#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 SIGTRAP\n", 15);
	char out[20];
	int fd = 4;
	printf("Trying to read fd: %d\n", 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: %d\n", 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:

```console
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`](https://github.com/openbsd/src/blob/4245707926efbf4b027899ae0328f0c436ca7e10/sys/kern/kern_pledge.c#L645), with hardcoded paths:

```C
/*
 * 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] == '\0')) {
					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:

```console
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 with me.)
