git: 5bfb3045d25b - main - Allow secure-netboot

From: Simon J. Gerraty <sjg_at_FreeBSD.org>
Date: Wed, 20 Aug 2025 22:56:23 UTC
The branch main has been updated by sjg:

URL: https://cgit.FreeBSD.org/src/commit/?id=5bfb3045d25b3e097f1e55fdc5b3f929f2b7da55

commit 5bfb3045d25b3e097f1e55fdc5b3f929f2b7da55
Author:     Simon J. Gerraty <sjg@FreeBSD.org>
AuthorDate: 2025-08-20 22:49:53 +0000
Commit:     Simon J. Gerraty <sjg@FreeBSD.org>
CommitDate: 2025-08-20 22:49:53 +0000

    Allow secure-netboot
    
    When doing file verification, tftp needs to be able to handle multiple
    open files concurrently.
    We also need tftp_stat() to provide useful values for st_dev and st_ino.
    
    Allow an architecture to define NETPROTO_DEFAULT.
    The default is NET_NFS for backwards compatability.
    
    In net_parse_rootpath() fix parsing of
    <scheme>://<ip>[:<port]/<path>
    and ensure we return INADDR_NONE unless we successfully
    parsed an addr, so we don't end up clobbering rootip obtained
    from bootp().
    
    Sponsored by:   Juniper Networks, Inc.
    Differential Revision:  https://reviews.freebsd.org/D51187
---
 stand/common/dev_net.c |  32 +++++++----
 stand/defs.mk          |   2 +
 stand/libsa/globals.c  |   1 +
 stand/libsa/libsa.3    |   4 ++
 stand/libsa/net.h      |   1 +
 stand/libsa/tftp.c     | 153 +++++++++++++++++++++++++++++++++++--------------
 stand/loader.mk        |   2 -
 7 files changed, 140 insertions(+), 55 deletions(-)

diff --git a/stand/common/dev_net.c b/stand/common/dev_net.c
index 10389db27b99..d1c48d40691a 100644
--- a/stand/common/dev_net.c
+++ b/stand/common/dev_net.c
@@ -66,6 +66,10 @@
 #include "dev_net.h"
 #include "bootstrap.h"
 
+#ifndef NETPROTO_DEFAULT
+# define NETPROTO_DEFAULT NET_NFS
+#endif
+
 static char *netdev_name;
 static int netdev_sock = -1;
 static int netdev_opens;
@@ -304,7 +308,7 @@ net_getparams(int sock)
 		return (EIO);
 	}
 exit:
-	if ((rootaddr = net_parse_rootpath()) != INADDR_NONE)
+	if ((rootaddr = net_parse_rootpath()) != htonl(INADDR_NONE))
 		rootip.s_addr = rootaddr;
 
 	DEBUG_PRINTF(1,("%s: proto: %d\n", __func__, netproto));
@@ -355,7 +359,7 @@ is_tftp(void)
  * Parses the rootpath if present
  *
  * The rootpath format can be in the form
- * <scheme>://ip/path
+ * <scheme>://ip[:port]/path
  * <scheme>:/path
  *
  * For compatibility with previous behaviour it also accepts as an NFS scheme
@@ -370,10 +374,10 @@ is_tftp(void)
 uint32_t
 net_parse_rootpath(void)
 {
-	n_long addr = htonl(INADDR_NONE);
+	n_long addr = 0;
 	size_t i;
 	char ip[FNAME_SIZE];
-	char *ptr, *val;
+	char *ptr, *portp, *val;
 
 	netproto = NET_NONE;
 
@@ -388,7 +392,7 @@ net_parse_rootpath(void)
 	ptr = rootpath;
 	/* Fallback for compatibility mode */
 	if (netproto == NET_NONE) {
-		netproto = NET_NFS;
+		netproto = NETPROTO_DEFAULT;
 		(void)strsep(&ptr, ":");
 		if (ptr != NULL) {
 			addr = inet_addr(rootpath);
@@ -401,16 +405,21 @@ net_parse_rootpath(void)
 		if (*ptr == '/') {
 			/* we are in the form <scheme>://, we do expect an ip */
 			ptr++;
-			/*
-			 * XXX when http will be there we will need to check for
-			 * a port, but right now we do not need it yet
-			 */
+			portp = val = strchr(ptr, ':');
+			if (val != NULL) {
+				val++;
+				rootport = strtol(val, NULL, 10);
+			}
 			val = strchr(ptr, '/');
 			if (val != NULL) {
+				if (portp == NULL)
+					portp = val;
 				snprintf(ip, sizeof(ip), "%.*s",
-				    (int)((uintptr_t)val - (uintptr_t)ptr),
+				    (int)(portp - ptr),
 				    ptr);
 				addr = inet_addr(ip);
+				DEBUG_PRINTF(1,("ip=%s addr=%#x\n",
+					ip, addr));
 				bcopy(val, rootpath, strlen(val) + 1);
 			}
 		} else {
@@ -418,6 +427,7 @@ net_parse_rootpath(void)
 			bcopy(ptr, rootpath, strlen(ptr) + 1);
 		}
 	}
-
+	if (addr == 0)
+		addr = htonl(INADDR_NONE);
 	return (addr);
 }
diff --git a/stand/defs.mk b/stand/defs.mk
index 8ef84267b198..eb4133b604eb 100644
--- a/stand/defs.mk
+++ b/stand/defs.mk
@@ -207,6 +207,8 @@ LOADER_INTERP?=${LOADER_DEFAULT_INTERP}
 # Make sure we use the machine link we're about to create
 CFLAGS+=-I.
 
+.include "${BOOTSRC}/veriexec.mk"
+
 all: ${PROG}
 
 CLEANFILES+= teken_state.h
diff --git a/stand/libsa/globals.c b/stand/libsa/globals.c
index 2797045d4faf..6bd3a4243d73 100644
--- a/stand/libsa/globals.c
+++ b/stand/libsa/globals.c
@@ -17,6 +17,7 @@
 u_char	bcea[6] = BA;			/* broadcast ethernet address */
 
 char	rootpath[FNAME_SIZE] = "/";	/* root mount path */
+int	rootport;			/* port for rootpath server */
 char	bootfile[FNAME_SIZE];		/* bootp says to boot this */
 char	hostname[FNAME_SIZE];		/* our hostname */
 int	hostnamelen;
diff --git a/stand/libsa/libsa.3 b/stand/libsa/libsa.3
index 3e3f70610516..0947f97a0a1f 100644
--- a/stand/libsa/libsa.3
+++ b/stand/libsa/libsa.3
@@ -781,6 +781,10 @@ The same as
 but for
 .Xr bzip2 1 Ns -compressed
 files.
+.It Va pkgfs_fsops
+File access from a tar file typically streamed via TFTP.
+The order of files in the tar file must match the order they are
+to be consumed as rewind is not practical.
 .El
 .Pp
 The array of
diff --git a/stand/libsa/net.h b/stand/libsa/net.h
index d4823d88f58b..945b6b9ea45f 100644
--- a/stand/libsa/net.h
+++ b/stand/libsa/net.h
@@ -75,6 +75,7 @@ enum net_proto {
 
 extern	u_char bcea[6];
 extern	char rootpath[FNAME_SIZE];
+extern  int rootport;
 extern	char bootfile[FNAME_SIZE];
 extern	char hostname[FNAME_SIZE];
 extern	int hostnamelen;
diff --git a/stand/libsa/tftp.c b/stand/libsa/tftp.c
index 0584246a6dea..656c402683bb 100644
--- a/stand/libsa/tftp.c
+++ b/stand/libsa/tftp.c
@@ -50,6 +50,10 @@
 #include <netinet/in_systm.h>
 #include <arpa/tftp.h>
 
+#ifdef LOADER_VERIEXEC
+#include <verify_file.h>
+#endif
+
 #include <string.h>
 
 #include "stand.h"
@@ -85,7 +89,6 @@ struct fs_ops tftp_fsops = {
 };
 
 static int	tftpport = 2000;
-static int	is_open = 0;
 
 /*
  * The legacy TFTP_BLKSIZE value was SEGSIZE(512).
@@ -99,10 +102,14 @@ static int	is_open = 0;
  * Jumbo frames in the future.
  */
 #define	TFTP_MAX_BLKSIZE 9008
-#define TFTP_TRIES 2
+#define TFTP_TRIES 3
 
 struct tftp_handle {
 	struct iodesc  *iodesc;
+	struct iodesc	io;
+	int		id;
+	ino_t		ino;
+	int		port;
 	int		currblock;	/* contents of lastdata */
 	unsigned int	islastblock:1;	/* flag */
 	unsigned int	tries:4;	/* number of read attempts */
@@ -178,6 +185,9 @@ tftp_sendack(struct tftp_handle *h, u_short block)
 	wbuf.t.th_block = htons(block);
 	wtail += 2;
 
+	DEBUG_PRINTF(5,("%s: myport=%hu xid=%lu, block=%hu\n",
+	    __func__, h->iodesc->myport, h->iodesc->xid, block));
+
 	sendudp(h->iodesc, &wbuf.t, wtail - (char *)&wbuf.t);
 }
 
@@ -191,6 +201,7 @@ recvtftp(struct iodesc *d, void **pkt, void **payload, time_t tleft,
 	void *ptr = NULL;
 	ssize_t len;
 	int tftp_error;
+	unsigned short block;
 
 	errno = 0;
 	extra = recv_extra;
@@ -204,19 +215,22 @@ recvtftp(struct iodesc *d, void **pkt, void **payload, time_t tleft,
 	}
 
 	extra->rtype = ntohs(t->th_opcode);
-	switch (ntohs(t->th_opcode)) {
+	block = ntohs(t->th_block);
+	DEBUG_PRINTF(6,("%s: myport=%hu xid=%lu, block=%hu, opcode=%hu\n",
+	    __func__, d->myport, d->xid, block, extra->rtype));
+	switch (extra->rtype) {
 	case DATA: {
 		int got;
 
-		if (htons(t->th_block) < (u_short)d->xid) {
+		if (block < (u_short)d->xid) {
 			/*
 			 * Apparently our ACK was missed, re-send.
 			 */
-			tftp_sendack(h, htons(t->th_block));
+			tftp_sendack(h, block);
 			free(ptr);
 			return (-1);
 		}
-		if (htons(t->th_block) != (u_short)d->xid) {
+		if (block != (u_short)d->xid) {
 			/*
 			 * Packet from the future, drop this.
 			 */
@@ -242,9 +256,7 @@ recvtftp(struct iodesc *d, void **pkt, void **payload, time_t tleft,
 			printf("illegal tftp error %d\n", tftp_error);
 			errno = EIO;
 		} else {
-#ifdef TFTP_DEBUG
-			printf("tftp-error %d\n", tftp_error);
-#endif
+			DEBUG_PRINTF(0, ("tftp-error %d\n", tftp_error));
 			errno = tftperrors[tftp_error];
 		}
 		free(ptr);
@@ -285,9 +297,7 @@ recvtftp(struct iodesc *d, void **pkt, void **payload, time_t tleft,
 		return (0);
 	}
 	default:
-#ifdef TFTP_DEBUG
-		printf("tftp type %d not handled\n", ntohs(t->th_opcode));
-#endif
+		DEBUG_PRINTF(0, ("tftp type %hu not handled\n", extra->rtype));
 		free(ptr);
 		return (-1);
 	}
@@ -344,7 +354,7 @@ tftp_makereq(struct tftp_handle *h)
 	bcopy("0", wtail, 2);
 	wtail += 2;
 
-	h->iodesc->myport = htons(tftpport + (getsecs() & 0x3ff));
+	h->iodesc->myport = htons(h->port +  (getsecs() & 0x3ff));
 	h->iodesc->destport = htons(IPPORT_TFTP);
 	h->iodesc->xid = 1;	/* expected block */
 
@@ -352,11 +362,15 @@ tftp_makereq(struct tftp_handle *h)
 	h->islastblock = 0;
 	h->validsize = 0;
 
+	DEBUG_PRINTF(5,("%s: %s: id=%d port=%d myport=%hu xid=1\n",
+	    __func__, h->path, h->id, h->port, ntohs(h->iodesc->myport)));
 	pkt = NULL;
 	recv_extra.tftp_handle = h;
 	res = sendrecv(h->iodesc, &sendudp, &wbuf.t, wtail - (char *)&wbuf.t,
 	    &recvtftp, &pkt, (void **)&t, &recv_extra);
 	if (res == -1) {
+		DEBUG_PRINTF(3,("%s: %s: id=%d errno=%d\n",
+			__func__, h->path, h->id, errno));
 		free(pkt);
 		return (errno);
 	}
@@ -411,12 +425,18 @@ tftp_getnextblock(struct tftp_handle *h)
 
 	h->iodesc->xid = h->currblock + 1;	/* expected block */
 
+	DEBUG_PRINTF(5,("%s: %s: id=%d port=%d myport=%hu xid=%lu\n",
+	    __func__, h->path, h->id, h->port,
+	    ntohs(h->iodesc->myport), h->iodesc->xid));
+
 	pkt = NULL;
 	recv_extra.tftp_handle = h;
 	res = sendrecv(h->iodesc, &sendudp, &wbuf.t, wtail - (char *)&wbuf.t,
 	    &recvtftp, &pkt, (void **)&t, &recv_extra);
 
 	if (res == -1) {		/* 0 is OK! */
+		DEBUG_PRINTF(3,("%s: %s: id=%d errno=%d\n",
+		    __func__, h->path, h->id, errno));
 		free(pkt);
 		return (errno);
 	}
@@ -429,21 +449,32 @@ tftp_getnextblock(struct tftp_handle *h)
 	if (res < h->tftp_blksize)
 		h->islastblock = 1;	/* EOF */
 
-	if (h->islastblock == 1) {
+	DEBUG_PRINTF(5,("%s: %s: id=%d res=%d blksz=%d last=%d\n",
+		__func__, h->path, h->id, res, h->tftp_blksize, h->islastblock));
+	
+	if (h->islastblock) {
 		/* Send an ACK for the last block */
-		wbuf.t.th_block = htons((u_short)h->currblock);
-		sendudp(h->iodesc, &wbuf.t, wtail - (char *)&wbuf.t);
+		tftp_sendack(h, h->currblock);
 	}
 
 	return (0);
 }
 
+/*
+ * If doing verification we need to handle multiple
+ * files at the same time.
+ */
+#define TOPEN_MAX 8
+static struct tftp_handle *handles[TOPEN_MAX];
+
 static int
 tftp_open(const char *path, struct open_file *f)
 {
 	struct devdesc *dev;
 	struct tftp_handle *tftpfile;
 	struct iodesc	*io;
+	static int	lx = 0;
+	int		i, x;
 	int		res;
 	size_t		pathsize;
 	const char	*extraslash;
@@ -451,24 +482,39 @@ tftp_open(const char *path, struct open_file *f)
 	if (netproto != NET_TFTP)
 		return (EINVAL);
 
-	if (f->f_dev->dv_type != DEVT_NET)
+	if (f->f_dev == NULL || f->f_dev->dv_type != DEVT_NET)
 		return (EINVAL);
 
-	if (is_open)
+	tftpfile = NULL;
+	for (x = lx + 1, i = 0; i < TOPEN_MAX; i++, x++) {
+		x %= TOPEN_MAX;
+		if (handles[x] == NULL) {
+			handles[x] = tftpfile = calloc(1, sizeof(*tftpfile));
+			if (tftpfile == NULL)
+				return (ENOMEM);
+			/* id allows us to clear the slot on close */
+			tftpfile->id = lx = x;
+			/* port ensures a different session with server */
+			tftpfile->port = (tftpport + (x * tftpport)) & 0xffff;
+			DEBUG_PRINTF(1, ("%s(%s) id=%d port=%d\n",
+			    __func__, path, tftpfile->id, tftpfile->port));
+			break;
+		}
+	}
+	if (tftpfile == NULL) {
+		DEBUG_PRINTF(1, ("%s: EBUSY\n", __func__));
 		return (EBUSY);
-
-	tftpfile = calloc(1, sizeof(*tftpfile));
-	if (!tftpfile)
-		return (ENOMEM);
-
+	}
 	tftpfile->tftp_blksize = TFTP_REQUESTED_BLKSIZE;
 	dev = f->f_devdata;
-	tftpfile->iodesc = io = socktodesc(*(int *)(dev->d_opendata));
+	io = socktodesc(*(int *)(dev->d_opendata));
 	if (io == NULL) {
 		free(tftpfile);
 		return (EINVAL);
 	}
 
+	memcpy(&tftpfile->io, io, sizeof(tftpfile->io));
+	io = tftpfile->iodesc = &tftpfile->io;
 	io->destip = rootip;
 	tftpfile->off = 0;
 	pathsize = (strlen(rootpath) + 1 + strlen(path) + 1) * sizeof(char);
@@ -481,8 +527,11 @@ tftp_open(const char *path, struct open_file *f)
 		extraslash = "";
 	else
 		extraslash = "/";
-	res = snprintf(tftpfile->path, pathsize, "%s%s%s",
-	    rootpath, extraslash, path);
+	if (rootpath[0] == '/' && rootpath[1] == '\0' && path[0] == '/')
+		res = strlcpy(tftpfile->path, path, pathsize);
+	else
+		res = snprintf(tftpfile->path, pathsize, "%s%s%s",
+		    rootpath, extraslash, path);
 	if (res < 0 || res > pathsize) {
 		free(tftpfile->path);
 		free(tftpfile);
@@ -492,13 +541,13 @@ tftp_open(const char *path, struct open_file *f)
 	res = tftp_makereq(tftpfile);
 
 	if (res) {
+		handles[tftpfile->id] = NULL;
 		free(tftpfile->path);
 		free(tftpfile->pkt);
 		free(tftpfile);
 		return (res);
 	}
 	f->f_fsdata = tftpfile;
-	is_open = 1;
 	return (0);
 }
 
@@ -548,9 +597,7 @@ tftp_read(struct open_file *f, void *addr, size_t size,
 
 			rc = tftp_getnextblock(tftpfile);
 			if (rc) {	/* no answer */
-#ifdef TFTP_DEBUG
-				printf("tftp: read error\n");
-#endif
+				DEBUG_PRINTF(0, ("tftp: read error\n"));
 				if (tftpfile->tries > TFTP_TRIES) {
 					return (rc);
 				} else {
@@ -569,10 +616,8 @@ tftp_read(struct open_file *f, void *addr, size_t size,
 
 			inbuffer = tftpfile->validsize - offinblock;
 			if (inbuffer < 0) {
-#ifdef TFTP_DEBUG
-				printf("tftp: invalid offset %d\n",
-				    tftpfile->off);
-#endif
+				DEBUG_PRINTF(0, ("tftp: invalid offset %d\n",
+				    tftpfile->off));
 				return (EINVAL);
 			}
 			count = (size < inbuffer ? size : inbuffer);
@@ -587,15 +632,15 @@ tftp_read(struct open_file *f, void *addr, size_t size,
 			if ((tftpfile->islastblock) && (count == inbuffer))
 				break;	/* EOF */
 		} else {
-#ifdef TFTP_DEBUG
-			printf("tftp: block %d not found\n", needblock);
-#endif
+			DEBUG_PRINTF(0, ("tftp: block %d not found\n", needblock));
 			return (EINVAL);
 		}
 
 	}
 
 out:
+	DEBUG_PRINTF(4, ("%s(%s) res=%ld\n", __func__, tftpfile->path,
+	    (tftpfile->tftp_tsize - tftpfile->off)));
 	if (resid != NULL)
 		*resid = res;
 	return (rc);
@@ -611,15 +656,18 @@ tftp_close(struct open_file *f)
 		tftp_senderr(tftpfile, 0, "No error: file closed");
 
 	if (tftpfile) {
+		DEBUG_PRINTF(1, ("%s(%d): %s\n", __func__,
+		    tftpfile->id, tftpfile->path));
+		handles[tftpfile->id] = NULL;
 		free(tftpfile->path);
 		free(tftpfile->pkt);
 		free(tftpfile->tftp_cache);
 		free(tftpfile);
 	}
-	is_open = 0;
 	return (0);
 }
 
+
 static int
 tftp_stat(struct open_file *f, struct stat *sb)
 {
@@ -631,6 +679,29 @@ tftp_stat(struct open_file *f, struct stat *sb)
 	sb->st_uid = 0;
 	sb->st_gid = 0;
 	sb->st_size = tftpfile->tftp_tsize;
+	sb->st_mtime = 0;
+#ifdef LOADER_VERIEXEC
+	/* libsecureboot needs st_dev and st_ino at minimum;
+	 * we need to fake something that will be close enough to
+	 * unique.
+	 */
+	sb->st_dev = (dev_t)tftpfile->iodesc->destip.s_addr;
+	/* we don't want to compute this more than once */
+	if (tftpfile->ino == 0) {
+		union {
+			unsigned char digest[SHA_DIGEST_LENGTH];
+			ino_t ino;
+		} u;
+
+		hash_string(tftpfile->path, 0, u.digest, sizeof(u.digest));
+
+		tftpfile->ino = u.ino & 0x7fffffff;
+		DEBUG_PRINTF(2,("%s(%s) dev=%lu ino=%lu\n", __func__,
+		    tftpfile->path, (unsigned long)sb->st_dev,
+		    (unsigned long)tftpfile->ino));
+	}
+	sb->st_ino = tftpfile->ino;
+#endif
 	return (0);
 }
 
@@ -828,9 +899,7 @@ tftp_parse_oack(struct tftp_handle *h, char *buf, size_t len)
 		return (-1);
 	}
 
-#ifdef TFTP_DEBUG
-	printf("tftp_blksize: %u\n", h->tftp_blksize);
-	printf("tftp_tsize: %lu\n", h->tftp_tsize);
-#endif
+	DEBUG_PRINTF(2, ("tftp_blksize: %u\n", h->tftp_blksize));
+	DEBUG_PRINTF(2, ("tftp_tsize: %lu\n", h->tftp_tsize));
 	return (0);
 }
diff --git a/stand/loader.mk b/stand/loader.mk
index 0f2ff31a5343..4073e523e552 100644
--- a/stand/loader.mk
+++ b/stand/loader.mk
@@ -101,8 +101,6 @@ SRCS+=	interp_simple.c
 .error Unknown interpreter ${LOADER_INTERP}
 .endif
 
-.include "${BOOTSRC}/veriexec.mk"
-
 .if defined(BOOT_PROMPT_123)
 CFLAGS+=	-DBOOT_PROMPT_123
 .endif