DragonFlyBSD Kernel Audit
DF-2824 / df2824_deadlock.c
← back to finding ↓ download raw
/*
 * DF-2824 -- DragonFlyBSD sys/kern/kern_lockf.c
 *
 * BUG: POSIX deadlock detection (EDEADLK) in lf_setlock()
 * (kern_lockf.c:406-413) only scans the blocked list of the *same* lockf
 * (same file) for the conflicting owner.  A deadlock cycle that spans two
 * (or more) files is not detected: the process closing the cycle is put to
 * sleep instead of getting EDEADLK, and all members of the cycle hang in
 * F_SETLKW until signaled (the sleep is PCATCH, so they are killable).
 *
 * POSIX.1-2008 2.9.7 (locked when a deadlock would occur): fcntl(F_SETLKW)
 * shall fail with [EDEADLK] if the lock would cause a deadlock.
 *
 * Deterministic probe (two-phase sync so both children hold their first
 * locks before either attempts the second):
 *   child1: F_SETLK  WRLCK all of A --check rc--  [gate]  F_SETLKW WRLCK on B
 *   child2: F_SETLK  WRLCK all of B --check rc--  [gate]  F_SETLKW WRLCK on A
 * If detection worked, child2 (the cycle closer) returns EDEADLK(11)
 * quickly.  Bug => both children remain blocked after the timeout window.
 */
#include <errno.h>
#include <fcntl.h>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/wait.h>
#include <unistd.h>

static int
lockall(int fd, int wait)
{
	struct flock fl;

	memset(&fl, 0, sizeof(fl));
	fl.l_type = F_WRLCK;
	fl.l_whence = SEEK_SET;
	fl.l_start = 0;
	fl.l_len = 0;
	return (fcntl(fd, wait ? F_SETLKW : F_SETLK, &fl));
}

int
main(void)
{
	char pa[64], pb[64], c;
	int fda, fdb, syncready[2], syncgo[2];
	int i, st, rc, blocked1 = 1, blocked2 = 1;
	pid_t c1, c2, wp;

	setvbuf(stdout, NULL, _IONBF, 0);

	snprintf(pa, sizeof(pa), "/tmp/df2824.a.%d", getpid());
	snprintf(pb, sizeof(pb), "/tmp/df2824.b.%d", getpid());
	fda = open(pa, O_RDWR | O_CREAT | O_TRUNC, 0600);
	fdb = open(pb, O_RDWR | O_CREAT | O_TRUNC, 0600);
	if (fda < 0 || fdb < 0 || pipe(syncready) != 0 || pipe(syncgo) != 0) {
		perror("setup");
		exit(2);
	}
	unlink(pa);
	unlink(pb);

	c1 = fork();
	if (c1 == 0) {
		setvbuf(stdout, NULL, _IONBF, 0);
		rc = lockall(fda, 0);		/* child1 owns A */
		if (rc != 0) {
			printf("child1: first lock A failed: %s\n",
			    strerror(errno));
			_exit(9);
		}
		write(syncready[1], "a", 1);
		if (read(syncgo[0], &c, 1) != 1)
			_exit(9);		/* wait for gate */
		errno = 0;
		rc = lockall(fdb, 1);		/* F_SETLKW on B (held by c2) */
		printf("child1[%d]: F_SETLKW(B) -> rc=%d errno=%d (%s)\n",
		    getpid(), rc, errno, rc ? strerror(errno) : "ok");
		_exit(0);
	}
	c2 = fork();
	if (c2 == 0) {
		setvbuf(stdout, NULL, _IONBF, 0);
		rc = lockall(fdb, 0);		/* child2 owns B */
		if (rc != 0) {
			printf("child2: first lock B failed: %s\n",
			    strerror(errno));
			_exit(9);
		}
		write(syncready[1], "b", 1);
		if (read(syncgo[0], &c, 1) != 1)
			_exit(9);		/* wait for gate */
		errno = 0;
		rc = lockall(fda, 1);		/* closes the cycle */
		printf("child2[%d]: F_SETLKW(A) -> rc=%d errno=%d (%s)\n",
		    getpid(), rc, errno, rc ? strerror(errno) : "ok");
		if (rc == -1 && errno == EDEADLK)
			printf("child2: EDEADLK detected (correct)\n");
		_exit(0);
	}

	/* wait until both children verifiably hold their first locks */
	if (read(syncready[0], &c, 1) != 1 || read(syncready[0], &c, 1) != 1) {
		perror("ready sync");
		exit(2);
	}
	/* open the gate simultaneously */
	write(syncgo[1], "g", 1);
	write(syncgo[1], "g", 1);
	printf("T2: both children hold their first locks; gate opened\n");

	/* 4s window for EDEADLK to be delivered to the cycle closer */
	for (i = 0; i < 40; ++i) {
		usleep(100000);
		if (blocked1 && waitpid(c1, &st, WNOHANG) == c1)
			blocked1 = 0;
		if (blocked2 && waitpid(c2, &st, WNOHANG) == c2)
			blocked2 = 0;
		if (!blocked1 || !blocked2)
			break;
	}

	if (blocked1 && blocked2) {
		printf("T2_RESULT: BUG-REPRODUCED both processes still blocked "
		    "in F_SETLKW after 4s; POSIX requires EDEADLK for the "
		    "cycle closer (sleep is PCATCH: killable)\n");
	} else {
		printf("T2_RESULT: NOT-REPRODUCED a child returned early "
		    "(deadlock detection worked)\n");
	}

	kill(c1, SIGKILL);
	kill(c2, SIGKILL);
	waitpid(c1, &st, 0);
	waitpid(c2, &st, 0);
	printf("T2: children reaped, guest clean\n");

	return ((blocked1 && blocked2) ? 0 : 1);
}