kern_ktrace.c: ktrpsig() leaks 4 bytes of uninitialised stack via struct ktr_psig padding

Stuart Thomas <[email protected]> Wed, 13 May 2026 10:22:06 +0100
Newsgroups gmane.os.openbsd.bugs
Message-ID <CACS-ydN3Wrvj7pp35GnNkmndFDWjypr4HW1-=mvg2+LmFkigRw@mail.gmail.com>
Hi,

In sys/kern/kern_ktrace.c, ktrpsig() stack-allocates a struct ktr_psig
and assigns its fields individually, but does not zero the struct first.
The struct contains a 4-byte alignment hole at offset 4 (between
"int signo" and "sig_t action"), which is then written verbatim to the
ktrace output file via ktrwrite().

This is the same class as the April 2026 fixes for shm_internal
(1b900a0, deraadt) and sem_base (76d3556, dgl) -- uninitialised
kernel-stack data exposed to userland through a same-UID syscall path.

Source (current master, sys/kern/kern_ktrace.c line 283):

    void
    ktrpsig(struct proc *p, int sig, sig_t action, int mask, int code,
        siginfo_t *si)
    {
        struct ktr_header kth;
        struct ktr_psig kp;      /* not memset before field assignments */

        atomic_setbits_int(&p->p_flag, P_INKTR);
        ktrinitheader(&kth, p, KTR_PSIG);
        kp.signo = (char)sig;
        kp.action = action;
        kp.mask = mask;
        kp.code = code;
        kp.si = *si;

        KERNEL_LOCK();
        ktrwrite(p, &kth, &kp, sizeof(kp));
        KERNEL_UNLOCK();
        atomic_clearbits_int(&p->p_flag, P_INKTR);
    }

struct ktr_psig layout (sys/sys/ktrace.h):

    int        signo;     /* offset  0 */
                          /* offset  4: 4-byte padding hole (sig_t
alignment) */
    sig_t      action;    /* offset  8 */
    int        mask;      /* offset 16 */
    int        code;      /* offset 20 */
    siginfo_t  si;        /* offset 24 */

The padding hole is never assigned anywhere in the function.

Tested on OpenBSD 7.7 arm64 (GENERIC.MP #361, 22 Apr 2025) in a UTM VM.
A PoC that traces its own process with KTRFAC_PSIG and raises 32 signals
interleaved with various other syscalls produces 32 PSIG records of
which 13 (40%) carry the value 0xffffff80 in the padding hole -- the
upper 32 bits of an arm64 kernel virtual address (TTBR1 region prefix).
The remaining 19 (59%) carry 0x00000000, consistent with the slot being
overwritten by zero by an intervening kernel function on those code
paths.

Verbatim hexdump of a leaking record:

    00000080  1e 00 00 00 80 ff ff ff  c0 14 9e 95 01 00 00 00
              signo=30      PADDING       action=0x1959e14c0
              SIGUSR1       LEAKED
                            0xffffff80

I am not claiming this is an exploitable KASLR weakening -- on arm64
the 0xffffff80 prefix is constant across the architecture, so it does
not disclose anything secret. But the structurally-equivalent reference
fixes were treated as worth correcting on hygiene grounds, and I think
the same applies here: the kernel should not be writing uninitialised
stack contents into a user-readable file, even on an opt-in trace
interface. On a different stack history or a different architecture
(amd64 not tested in this session) the leaked bytes could plausibly
carry less innocuous data.

Suggested fix:

    --- sys/kern/kern_ktrace.c
    +++ sys/kern/kern_ktrace.c
    @@ -285,6 +285,7 @@ ktrpsig(struct proc *p, int sig, sig_t action,
         struct ktr_header kth;
         struct ktr_psig kp;

    +    memset(&kp, 0, sizeof(kp));
         atomic_setbits_int(&p->p_flag, P_INKTR);
         ktrinitheader(&kth, p, KTR_PSIG);
         kp.signo = (char)sig;

PoC sources attached:
- POC_ktr_psig_padding.c: minimal reproducer, 16 PSIG records
- POC_ktr_entropy.c: extended PoC, 32 records across 4 signal types
  with interleaved syscalls, showing the 40%/59% distribution

Both build with: cc -O0 -o poc poc.c

Honestly noted:
- amd64 not tested by me; padding rules are arch-agnostic on LP64 so
  the layout will be the same, but the values landing in the slot may
  differ.
- The bug is opt-in (ktrace must be enabled) and same-UID.
- The constant 0xffffff80 prefix is not by itself a KASLR defeat.

Reporting this primarily as a hygiene/consistency fix matching
1b900a0 and 76d3556.

Thanks,
Stuart Thomas



--
*please note, there is no expectation for you to read/reply to my email
outside your normal working hours. *
POC_ktr_psig_padding.c (application/octet-stream, 3.4 KB)
/*
 * POC: ktr_psig stack-allocated struct in kern_ktrace.c is not memset
 * before fields are individually assigned. On amd64/arm64 the layout is:
 *   offset 0   int signo       (4 bytes)
 *   offset 4   PADDING HOLE    (4 bytes, alignment for sig_t)
 *   offset 8   sig_t action    (8 bytes)
 *   offset 16  int mask        (4 bytes)
 *   offset 20  int code        (4 bytes)
 *   offset 24  siginfo_t si
 *
 * Hypothesis: the 4-byte hole at offset 4 contains uninitialized kernel
 * stack data, observable by the tracing process (which can be the same
 * UID as the traced process per ktrcanset()).
 *
 * Procedure:
 *   1) ktrace -f outfile -t p ./self  (PSIG trace)
 *   2) self installs SIGUSR1 handler, raises SIGUSR1
 *   3) parse outfile, locate KTR_PSIG record, dump bytes 4..7
 *   4) repeat N times; non-deterministic non-zero → uninit leak
 */
#include <sys/types.h>
#include <sys/ktrace.h>
#include <sys/uio.h>
#include <sys/wait.h>
#include <signal.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>

static void handler(int s) { (void)s; }

int main(void)
{
    /* Child traces self, parent will inspect the file */
    /* Pre-create the output file (ktrace(2) requires it to exist) */
    int ofd = open("/tmp/ktrace.out", O_CREAT|O_RDWR|O_TRUNC, 0644);
    if (ofd < 0) { perror("open(create)"); return 1; }
    close(ofd);
    pid_t pid = fork();
    if (pid == 0) {
        struct sigaction sa;
        memset(&sa, 0, sizeof(sa));
        sa.sa_handler = handler;
        sigaction(SIGUSR1, &sa, NULL);
        /* Enable ktrace on self for PSIG only */
        if (ktrace("/tmp/ktrace.out", KTROP_SET,
                   KTRFAC_PSIG, getpid()) == -1) {
            perror("ktrace");
            _exit(1);
        }
        for (int i = 0; i < 16; i++) {
            raise(SIGUSR1);
        }
        ktrace("/tmp/ktrace.out", KTROP_CLEAR, KTRFAC_PSIG, getpid());
        _exit(0);
    }
    int status; waitpid(pid, &status, 0);

    int fd = open("/tmp/ktrace.out", O_RDONLY);
    if (fd < 0) { perror("open"); return 1; }
    unsigned char buf[8192];
    ssize_t n = read(fd, buf, sizeof(buf));
    close(fd);
    printf("ktrace.out size: %zd bytes\n", n);

    /* Walk records. struct ktr_header ends with size_t ktr_len.
     * Record layout: header || payload(ktr_len).
     * Header size depends on _MAXCOMLEN — we scan for KTR_PSIG type (0x05). */
    /* Simpler: dump hex of any payload immediately following type==KTR_PSIG */
    /* But ktrace file format requires real parsing. Use kdump for now and
     * also dump raw 32-byte windows showing the offset-4 padding hole.    */
    int found = 0;
    for (ssize_t i = 0; i + 64 < n; i++) {
        /* Heuristic: locate ktr_psig payload by scanning for a known
         * action-handler pointer (handler symbol) preceded by 4 bytes of
         * arbitrary padding. */
        uint64_t cand;
        memcpy(&cand, buf + i + 8, 8);
        uint32_t signo;
        memcpy(&signo, buf + i, 4);
        if (signo == SIGUSR1 && cand > 0x1000 && cand < 0x800000000000ULL) {
            uint32_t pad;
            memcpy(&pad, buf + i + 4, 4);
            printf("PSIG@%zd: signo=%u  PAD=0x%08x  action=0x%lx\n",
                   i, signo, pad, (unsigned long)cand);
            found++;
        }
    }
    if (!found) printf("No PSIG records matched heuristic. Run kdump manually.\n");
    return 0;
}
POC_ktr_entropy.c (application/octet-stream, 2.7 KB)
/*
 * ktr_entropy.c — force kernel-stack variance between PSIG records.
 * Tests Council/ChatGPT's high-value condition: does the padding hole
 * carry different values across invocations when interleaved with
 * other syscalls?
 */
#include <sys/types.h>
#include <sys/ktrace.h>
#include <sys/wait.h>
#include <sys/stat.h>
#include <sys/socket.h>
#include <sys/un.h>
#include <fcntl.h>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>

static void noop(int s) { (void)s; }

int
main(void)
{
	pid_t pid = getpid();
	unlink("/tmp/ktr_entropy.out");
	int fd = open("/tmp/ktr_entropy.out", O_CREAT|O_WRONLY|O_TRUNC, 0600);
	if (fd < 0) { perror("open"); return 1; }
	close(fd);

	if (ktrace("/tmp/ktr_entropy.out", KTROP_SET,
	           KTRFAC_PSIG, pid) < 0) { perror("ktrace"); return 1; }

	signal(SIGUSR1, noop);
	signal(SIGUSR2, noop);
	signal(SIGHUP,  noop);
	signal(SIGURG,  noop);

	/* 32 iterations, each preceded by a DIFFERENT syscall path to
	 * vary what's left on the kernel stack before sendsig() runs. */
	int sigs[] = { SIGUSR1, SIGUSR2, SIGHUP, SIGURG };
	for (int i = 0; i < 32; i++) {
		switch (i % 8) {
		case 0: { struct stat st; stat("/etc/passwd", &st); break; }
		case 1: { int s = socket(AF_UNIX, SOCK_STREAM, 0); close(s); break; }
		case 2: { getppid(); getpgrp(); getsid(0); break; }
		case 3: { char buf[64]; getcwd(buf, sizeof buf); break; }
		case 4: { int f = open("/dev/null", O_RDONLY);
		          if (f >= 0) { read(f, (char[16]){0}, 16); close(f); }
		          break; }
		case 5: { struct timespec ts; clock_gettime(CLOCK_MONOTONIC, &ts); break; }
		case 6: { char nm[64]; gethostname(nm, sizeof nm); break; }
		case 7: { (void)getuid(); (void)getgid(); break; }
		}
		kill(pid, sigs[i % 4]);
	}

	ktrace(NULL, KTROP_CLEAR, KTRFAC_PSIG, pid);

	/* Parse and dump padding bytes per record */
	fd = open("/tmp/ktr_entropy.out", O_RDONLY);
	if (fd < 0) { perror("open ro"); return 1; }
	unsigned char b[224];   /* one record */
	int rec = 0, hits = 0, zeros = 0;
	unsigned int seen[64] = {0}; int n_seen = 0;
	while (read(fd, b, sizeof b) == (ssize_t)sizeof b) {
		unsigned int pad = b[60] | (b[61]<<8) | (b[62]<<16) | (b[63]<<24);
		printf("rec %02d  signo=%-3d  PAD=0x%08x  action=...\n",
		       rec++, b[56], pad);
		if (pad == 0) zeros++;
		else { hits++;
			int dup = 0;
			for (int k = 0; k < n_seen; k++) if (seen[k] == pad) dup = 1;
			if (!dup && n_seen < 64) seen[n_seen++] = pad;
		}
	}
	close(fd);
	printf("\nSummary: %d records, %d non-zero, %d zero, %d unique non-zero values\n",
	       rec, hits, zeros, n_seen);
	if (n_seen > 1) printf("ENTROPY CONFIRMED: padding carries varying kernel data\n");
	return 0;
}