DragonFlyBSD Kernel Audit
DF-2823 / df2823_enolck.c
← back to finding ↓ download raw
/*
 * DF-2823 -- DragonFlyBSD sys/kern/kern_lockf.c
 *
 * BUG: lf_setlock() pre-charges a "worst case" POSIX-lock-count headroom for
 * the *unlocker's* uid before clipping (kern_lockf.c:510-524).  When the uid
 * is at its RLIMIT_POSIXLOCKS / kern.maxposixlocksperuid limit, a partial
 * F_UNLCK that clips into the head (or tail) of the caller's own existing
 * locks FAILS WITH ENOLCK.  POSIX (fcntl(2)/POSIX.1-2008 2.9.7) requires
 * F_UNLCK of one's own locks to succeed; it shall not fail with ENOLCK.
 *
 * Reachability on a default system:
 *   - kern.maxposixlocksperuid defaults to maxproc*4  (nonzero => enforced)
 *   - any process may lower its own RLIMIT_POSIXLOCKS (lowering never needs
 *     privilege), so a non-root daemon with a low rlimit exhibits this on a
 *     default install.
 *
 * Probe outline:
 *   1. setrlimit(RLIMIT_POSIXLOCKS, 2)
 *   2. take lock A=[0,1] and lock B=[2,3] (F_WRLCK)
 *   3. spam disjoint one-byte locks until the lazily-rolled uid counter
 *      exceeds the limit (per-cpu pups flush into ui_posixlocks past +/-32,
 *      after which further creations are refused with ENOLCK -- expected)
 *   4. F_UNLCK [1,2]: clips A to [0,0] and B to [3,3].  POSIX: must succeed.
 *      BUG: returns -1/ENOLCK.
 *   5. full-range F_UNLCK still works (unlock_override path) -- the only
 *      recovery available to a wedged process.
 *   6. supplementary: negative l_len is rejected with EINVAL (POSIX expects
 *      the range [l_start+l_len, l_start-1] to be accepted).
 */
#include <sys/types.h>
#include <sys/resource.h>
#include <errno.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

#ifndef RLIMIT_POSIXLOCKS
#define RLIMIT_POSIXLOCKS 11		/* DragonFly */
#endif

static int
setlk(int fd, int type, off_t start, off_t len)
{
	struct flock fl;

	memset(&fl, 0, sizeof(fl));
	fl.l_type = type;
	fl.l_whence = SEEK_SET;
	fl.l_start = start;
	fl.l_len = len;
	return (fcntl(fd, F_SETLK, &fl));
}

int
main(void)
{
	struct rlimit rl;
	char path[64];
	int fd, i, rc, nlocks = 0, refusals = 0;

	setvbuf(stdout, NULL, _IONBF, 0);

	snprintf(path, sizeof(path), "/tmp/df2823.%d", getpid());
	fd = open(path, O_RDWR | O_CREAT | O_TRUNC, 0600);
	if (fd < 0) {
		perror("open");
		exit(2);
	}
	unlink(path);

	rl.rlim_cur = 2;
	rl.rlim_max = 2;
	if (setrlimit(RLIMIT_POSIXLOCKS, &rl) != 0) {
		perror("setrlimit(RLIMIT_POSIXLOCKS)");
		exit(2);
	}
	printf("T1: RLIMIT_POSIXLOCKS lowered to 2 (unprivileged)\n");

	if (setlk(fd, F_WRLCK, 0, 2) != 0) {
		perror("T1: lock A [0,1]");
		exit(2);
	}
	if (setlk(fd, F_WRLCK, 2, 2) != 0) {
		perror("T1: lock B [2,3]");
		exit(2);
	}
	printf("T1: A=[0,1] and B=[2,3] acquired (own locks)\n");

	/*
	 * Force the uid-wide counter to roll past the limit.  The kernel
	 * keeps per-cpu deltas and only folds them into ui_posixlocks when
	 * a cpu's delta exceeds +/-32, so we need >32 charges.
	 */
	for (i = 0; i < 400; ++i) {
		rc = setlk(fd, F_WRLCK, 10 + (off_t)i * 2, 2);
		if (rc == 0) {
			++nlocks;
		} else if (errno == ENOLCK) {
			++refusals;
		} else {
			fprintf(stderr, "T1: filler %d: %s\n", i,
				strerror(errno));
			exit(2);
		}
	}
	printf("T1: filler locks ok=%d refused(ENOLCK)=%d\n", nlocks,
	       refusals);
	/*
	 * ENOLCK refusals prove the uid-wide counter is at/over the limit
	 * (creation checks the same condition the unlock headroom uses).
	 * Note: A and B plus the fillers must exceed the per-cpu rollup
	 * threshold (32) before ui_posixlocks is charged at all.
	 */
	if (refusals == 0 || nlocks < 30) {
		printf("T1_RESULT: SETUP-FAILED (uid counter never rolled; "
		       "bug state not reached)\n");
		exit(3);
	}

	/* ---- the actual bug probe: partial unlock clipping A and B ---- */
	errno = 0;
	rc = setlk(fd, F_UNLCK, 1, 2);		/* unlock [1,2] */
	if (rc == 0) {
		printf("T1_RESULT: NOT-REPRODUCED (F_UNLCK succeeded)\n");
	} else {
		printf("T1_RESULT: BUG-REPRODUCED F_UNLCK of own locks "
		       "failed rc=%d errno=%d (%s); POSIX requires success\n",
		       rc, errno, strerror(errno));
	}

	/* recovery path: whole-file unlock (unlock_override) */
	errno = 0;
	rc = setlk(fd, F_UNLCK, 0, 0);
	printf("T1: full-range F_UNLCK rc=%d errno=%d (%s) [expected 0]\n",
	       rc, errno, strerror(errno));

	/* supplementary: negative l_len handling */
	errno = 0;
	rc = setlk(fd, F_WRLCK, 10, -5);
	printf("T3: F_SETLK(start=10,len=-5) rc=%d errno=%d (%s) "
	       "[POSIX expects success: range [5,9]]\n", rc, errno,
	       strerror(errno));

	return (0);
}