git: 6788e42d53c6 - main - stand/efihttp: Support arbitrary http[N]://host[:port]/path devspecs

From: Warner Losh <imp_at_FreeBSD.org>
Date: Fri, 11 Sep 2026 05:04:24 UTC
The branch main has been updated by imp:

URL: https://cgit.FreeBSD.org/src/commit/?id=6788e42d53c60904943a1db11bdb27b8f9d2bb4c

commit 6788e42d53c60904943a1db11bdb27b8f9d2bb4c
Author:     Warner Losh <imp@FreeBSD.org>
AuthorDate: 2026-09-11 04:13:46 +0000
Commit:     Warner Losh <imp@FreeBSD.org>
CommitDate: 2026-09-11 04:15:28 +0000

    stand/efihttp: Support arbitrary http[N]://host[:port]/path devspecs
    
    Until now, the "http" device only worked when firmware itself booted
    loader.efi via EFI HTTP Boot: efihttp_dev_init() required a URI node on
    the loader's own boot device path, and every fetch went to that same
    server. Add a second form, using the parse_uri() helper from libsa/dev.c,
    that works anywhere: http[N]://host[:port]/path (N is a network interface
    unit, defaulting to 0). This lets a devspec like this be used as rootdev,
    in loader.conf, or interactively, independent of how loader.efi itself
    was booted.
    
    Network configuration is shared, loader-wide state (the myip/netmask/
    gateip/nameip globals from net.h): if something has already configured
    them (e.g. tftp/nfs via net_open()), the http device just reuses those
    values, applying them to EFI_IP4_CONFIG2_PROTOCOL as a static address. If
    nothing has, it drives IP4Config2's own DHCP directly instead of calling
    netdev's net_open(): net_open() opens the NIC's SNP
    EFI_OPEN_PROTOCOL_EXCLUSIVE, which would disconnect the Mnp/Ip4/.../
    HttpDxe driver chain this code depends on. That chain is also commonly
    disconnected already by whatever chained us here (e.g. iPXE excludes and
    exclusively opens SNP for its own raw I/O), so reconnect it explicitly
    with ConnectController() before giving up.
    
    The legacy EFI HTTP Boot path is untouched.
    
    Sponsored by:           Netflix
    Differential Revision:  https://reviews.freebsd.org/D59423
---
 stand/efi/libefi/efihttp.c | 344 ++++++++++++++++++++++++++++++++++++++++++++-
 1 file changed, 342 insertions(+), 2 deletions(-)

diff --git a/stand/efi/libefi/efihttp.c b/stand/efi/libefi/efihttp.c
index b18607410f3b..f725ba0709d8 100644
--- a/stand/efi/libefi/efihttp.c
+++ b/stand/efi/libefi/efihttp.c
@@ -49,11 +49,16 @@ static EFI_GUID ip4config2_guid = EFI_IP4_CONFIG2_PROTOCOL_GUID;
 
 static bool efihttp_init_done = false;
 
+struct http_devdesc;
+
 static int efihttp_dev_init(void);
 static int efihttp_dev_strategy(void *devdata, int rw, daddr_t blk, size_t size,
     char *buf, size_t *rsize);
 static int efihttp_dev_open(struct open_file *f, ...);
+static int efihttp_dev_open_legacy(struct open_file *f);
+static int efihttp_dev_open_url(struct open_file *f, struct http_devdesc *hd);
 static int efihttp_dev_close(struct open_file *f);
+static int efihttp_parsedev(struct devdesc **, const char *, const char **);
 
 static int efihttp_fs_open(const char *path, struct open_file *f);
 static int efihttp_fs_close(struct open_file *f);
@@ -79,6 +84,16 @@ struct file_efihttp {
 	bool		is_dir;
 };
 
+/*
+ * host == NULL for the legacy "httpN:" EFI HTTP Boot form; non-NULL for
+ * the "httpN://host/path" URL form.
+ */
+struct http_devdesc {
+	struct devdesc	dd;
+	char		*host;
+	int		port;
+};
+
 struct devsw efihttp_dev = {
 	.dv_name =	"http",
 	.dv_type =	DEVT_NET,
@@ -89,6 +104,7 @@ struct devsw efihttp_dev = {
 	.dv_ioctl =	noioctl,
 	.dv_print =	NULL,
 	.dv_cleanup =	nullsys,
+	.dv_parsedev =	efihttp_parsedev,
 };
 
 struct fs_ops efihttp_fsops = {
@@ -175,6 +191,135 @@ setup_ipv4_config2(EFI_HANDLE handle, MAC_ADDR_DEVICE_PATH *mac,
 	return (0);
 }
 
+/*
+ * Like setup_ipv4_config2()'s static branch, but sourced from the
+ * loader-wide myip/netmask/gateip/nameip globals instead of boot-path
+ * device nodes.
+ */
+static int
+setup_ipv4_config2_static(EFI_HANDLE handle, struct in_addr ip, n_long mask,
+    struct in_addr gw, struct in_addr dns)
+{
+	EFI_IP4_CONFIG2_PROTOCOL *ip4config2;
+	EFI_IP4_CONFIG2_MANUAL_ADDRESS manual;
+	EFI_IPv4_ADDRESS addr;
+	EFI_STATUS status;
+
+	status = BS->OpenProtocol(handle, &ip4config2_guid,
+	    (void **)&ip4config2, IH, NULL, EFI_OPEN_PROTOCOL_GET_PROTOCOL);
+	if (EFI_ERROR(status))
+		return (efi_status_to_errno(status));
+
+	status = ip4config2->SetData(ip4config2,
+	    Ip4Config2DataTypePolicy, sizeof(EFI_IP4_CONFIG2_POLICY),
+	    &(EFI_IP4_CONFIG2_POLICY) { Ip4Config2PolicyStatic });
+	if (EFI_ERROR(status))
+		return (efi_status_to_errno(status));
+
+	memset(&manual, 0, sizeof(manual));
+	memcpy(manual.Address.Addr, &ip, sizeof(manual.Address.Addr));
+	memcpy(manual.SubnetMask.Addr, &mask, sizeof(manual.SubnetMask.Addr));
+	status = ip4config2->SetData(ip4config2,
+	    Ip4Config2DataTypeManualAddress, sizeof(manual), &manual);
+	if (EFI_ERROR(status))
+		return (efi_status_to_errno(status));
+
+	if (gw.s_addr != 0) {
+		memcpy(addr.Addr, &gw, sizeof(addr.Addr));
+		status = ip4config2->SetData(ip4config2,
+		    Ip4Config2DataTypeGateway, sizeof(addr), &addr);
+		if (EFI_ERROR(status))
+			return (efi_status_to_errno(status));
+	}
+
+	if (dns.s_addr != 0) {
+		memcpy(addr.Addr, &dns, sizeof(addr.Addr));
+		status = ip4config2->SetData(ip4config2,
+		    Ip4Config2DataTypeDnsServer, sizeof(addr), &addr);
+		if (EFI_ERROR(status))
+			return (efi_status_to_errno(status));
+	}
+
+	return (0);
+}
+
+/*
+ * DHCPs via IP4Config2 directly, not netdev's net_open(): net_open()
+ * opens the NIC's SNP EFI_OPEN_PROTOCOL_EXCLUSIVE (efinet_probe()),
+ * which disconnects the MNP/IP4/HttpDxe chain this handle needs.
+ */
+static int
+efi_ip4_dhcp(EFI_HANDLE handle, struct in_addr *ip, n_long *mask,
+    struct in_addr *gw, struct in_addr *dns)
+{
+	EFI_IP4_CONFIG2_PROTOCOL *ip4config2;
+	EFI_IP4_CONFIG2_INTERFACE_INFO *info;
+	EFI_IPv4_ADDRESS dnsaddr;
+	EFI_STATUS status;
+	UINTN sz, i, nroutes;
+	int polltime;
+
+	status = BS->OpenProtocol(handle, &ip4config2_guid,
+	    (void **)&ip4config2, IH, NULL, EFI_OPEN_PROTOCOL_GET_PROTOCOL);
+	if (EFI_ERROR(status))
+		return (efi_status_to_errno(status));
+
+	status = ip4config2->SetData(ip4config2,
+	    Ip4Config2DataTypePolicy, sizeof(EFI_IP4_CONFIG2_POLICY),
+	    &(EFI_IP4_CONFIG2_POLICY) { Ip4Config2PolicyDhcp });
+	if (EFI_ERROR(status))
+		return (efi_status_to_errno(status));
+
+	info = NULL;
+	polltime = 0;
+	for (;;) {
+		free(info);
+		info = NULL;
+		sz = 0;
+		status = ip4config2->GetData(ip4config2,
+		    Ip4Config2DataTypeInterfaceInfo, &sz, NULL);
+		if (status == EFI_BUFFER_TOO_SMALL) {
+			info = malloc(sz);
+			if (info == NULL)
+				return (ENOMEM);
+			status = ip4config2->GetData(ip4config2,
+			    Ip4Config2DataTypeInterfaceInfo, &sz, info);
+		}
+		if (!EFI_ERROR(status) &&
+		    *(uint32_t *)info->StationAddress.Addr != 0)
+			break;
+		if (polltime >= EFIHTTP_POLL_TIMEOUT) {
+			free(info);
+			return (ENXIO);
+		}
+		delay(100 * 1000);
+		polltime += 100;
+	}
+
+	memcpy(ip, info->StationAddress.Addr, sizeof(*ip));
+	memcpy(mask, info->SubnetMask.Addr, sizeof(*mask));
+	gw->s_addr = 0;
+	nroutes = info->RouteTableSize / sizeof(EFI_IP4_ROUTE_TABLE);
+	for (i = 0; i < nroutes; i++) {
+		if (*(uint32_t *)info->RouteTable[i].SubnetAddress.Addr == 0 &&
+		    *(uint32_t *)info->RouteTable[i].SubnetMask.Addr == 0) {
+			memcpy(gw, info->RouteTable[i].GatewayAddress.Addr,
+			    sizeof(*gw));
+			break;
+		}
+	}
+	free(info);
+
+	dns->s_addr = 0;
+	sz = sizeof(dnsaddr);
+	status = ip4config2->GetData(ip4config2, Ip4Config2DataTypeDnsServer,
+	    &sz, &dnsaddr);
+	if (!EFI_ERROR(status) && sz >= sizeof(dnsaddr))
+		memcpy(dns, &dnsaddr, sizeof(*dns));
+
+	return (0);
+}
+
 static int
 efihttp_dev_init(void)
 {
@@ -223,6 +368,21 @@ efihttp_dev_strategy(void *devdata __unused, int rw __unused,
 
 static int
 efihttp_dev_open(struct open_file *f, ...)
+{
+	struct http_devdesc *hd;
+
+	hd = (struct http_devdesc *)f->f_devdata;
+	if (hd->host == NULL)
+		return (efihttp_dev_open_legacy(f));
+	return (efihttp_dev_open_url(f, hd));
+}
+
+/*
+ * EFI HTTP Boot path: handles http_devdesc with host == NULL, usable only
+ * when the boot image's own device path has a URI node.
+ */
+static int
+efihttp_dev_open_legacy(struct open_file *f)
 {
 	EFI_HTTP_CONFIG_DATA config;
 	EFI_HTTPv4_ACCESS_POINT config_access;
@@ -358,6 +518,133 @@ end:
 	return (err);
 }
 
+/*
+ * EFI HTTP URL form: handles http_devdesc with host != NULL, using the
+ * shared myip/netmask/gateip/nameip globals for network config -- reused
+ * if already set, otherwise populated via efi_ip4_dhcp(). No network at
+ * all is an error; nothing here retries.
+ */
+static int
+efihttp_dev_open_url(struct open_file *f, struct http_devdesc *hd)
+{
+	EFI_HTTP_CONFIG_DATA config;
+	EFI_HTTPv4_ACCESS_POINT config_access;
+	EFI_DEVICE_PATH *devpath, *trimmed;
+	EFI_HANDLE nic, handle;
+	EFI_SERVICE_BINDING_PROTOCOL *sb;
+	struct devdesc *dev;
+	struct open_efihttp *oh;
+	EFI_STATUS status;
+	struct in_addr ip, gw, dns;
+	n_long mask;
+	int err;
+
+	nic = efi_find_handle(&efinet_dev, hd->dd.d_unit);
+	if (nic == NULL)
+		return (ENXIO);
+	devpath = efi_lookup_devpath(nic);
+	if (devpath == NULL)
+		return (ENXIO);
+	trimmed = devpath;
+	status = BS->LocateDevicePath(&httpsb_guid, &trimmed, &handle);
+	if (EFI_ERROR(status)) {
+		/*
+		 * Whatever chained us here (e.g. iPXE) commonly excludes and
+		 * then exclusively opens the NIC's SNP for its own raw I/O
+		 * -- the same thing our own efinet_probe() does -- which
+		 * disconnects the Mnp/Ip4/.../HttpDxe chain. Nothing
+		 * reconnects it automatically; ask explicitly.
+		 */
+		BS->ConnectController(nic, NULL, NULL, TRUE);
+		trimmed = devpath;
+		status = BS->LocateDevicePath(&httpsb_guid, &trimmed, &handle);
+	}
+	if (EFI_ERROR(status))
+		return (efi_status_to_errno(status));
+
+	if (myip.s_addr != 0) {
+		err = setup_ipv4_config2_static(handle, myip, netmask,
+		    gateip, nameip);
+	} else {
+		err = efi_ip4_dhcp(handle, &ip, &mask, &gw, &dns);
+		if (err == 0) {
+			myip = ip;
+			netmask = mask;
+			gateip = gw;
+			nameip = dns;
+			setenv("boot.netif.ip", inet_ntoa(myip), 1);
+			setenv("boot.netif.netmask", intoa(netmask), 1);
+			setenv("boot.netif.gateway", inet_ntoa(gateip), 1);
+		}
+	}
+	if (err != 0)
+		return (err);
+
+	oh = calloc(1, sizeof(struct open_efihttp));
+	if (oh == NULL)
+		return (ENOMEM);
+	oh->dev_handle = handle;
+	dev = (struct devdesc *)f->f_devdata;
+	dev->d_opendata = oh;
+
+	status = BS->OpenProtocol(handle, &httpsb_guid, (void **)&sb, IH, NULL,
+	    EFI_OPEN_PROTOCOL_GET_PROTOCOL);
+	if (EFI_ERROR(status)) {
+		err = efi_status_to_errno(status);
+		goto end;
+	}
+
+	status = sb->CreateChild(sb, &oh->http_handle);
+	if (EFI_ERROR(status)) {
+		err = efi_status_to_errno(status);
+		goto end;
+	}
+
+	status = BS->OpenProtocol(oh->http_handle, &http_guid,
+	    (void **)&oh->http, IH, NULL, EFI_OPEN_PROTOCOL_GET_PROTOCOL);
+	if (EFI_ERROR(status)) {
+		sb->DestroyChild(sb, oh->http_handle);
+		err = efi_status_to_errno(status);
+		goto end;
+	}
+
+	config.HttpVersion = HttpVersion11;
+	config.TimeOutMillisec = 0;
+	config.LocalAddressIsIPv6 = FALSE;
+	config.AccessPoint.IPv4Node = &config_access;
+	config_access.UseDefaultAddress = TRUE;
+	config_access.LocalPort = 0;
+	status = oh->http->Configure(oh->http, &config);
+	if (EFI_ERROR(status)) {
+		sb->DestroyChild(sb, oh->http_handle);
+		err = efi_status_to_errno(status);
+		goto end;
+	}
+
+	/*
+	 * No trailing slash: every path here is already absolute, and a
+	 * double slash makes EDK2's HttpDxe reject the request outright
+	 * (EFI_HTTP_ERROR, not a clean 404).
+	 */
+	if (hd->port != 0)
+		asprintf(&oh->uri_base, "http://%s:%d", hd->host, hd->port);
+	else
+		asprintf(&oh->uri_base, "http://%s", hd->host);
+	if (oh->uri_base == NULL) {
+		sb->DestroyChild(sb, oh->http_handle);
+		err = ENOMEM;
+		goto end;
+	}
+
+	err = 0;
+end:
+	if (err != 0) {
+		free(dev->d_opendata);
+		dev->d_opendata = NULL;
+	}
+	return (err);
+}
+
 static int
 efihttp_dev_close(struct open_file *f)
 {
@@ -379,6 +666,56 @@ efihttp_dev_close(struct open_file *f)
 	return (0);
 }
 
+/*
+ * Recognizes both "httpN:[/path]" (legacy EFI HTTP Boot) and
+ * "httpN://host[:port][/path]" (see efihttp_dev_open_url()). Falls back
+ * to default_parsedev() for the legacy form so existing "http0:"
+ * currdev strings keep parsing the same way.
+ */
+static int
+efihttp_parsedev(struct devdesc **idev, const char *devspec,
+    const char **path)
+{
+	struct http_devdesc *dev;
+	struct devdesc *ldev;
+	const char *np, *p;
+	char *host;
+	int unit, port, err;
+
+	np = devspec + strlen(efihttp_dev.dv_name);
+	err = parse_uri(np, &unit, &host, &port, &p);
+	if (err == EINVAL) {
+		err = default_parsedev(&ldev, np, path);
+		if (err != 0)
+			return (err);
+		dev = malloc(sizeof(*dev));
+		if (dev == NULL) {
+			free(ldev);
+			return (ENOMEM);
+		}
+		dev->dd = *ldev;
+		free(ldev);
+		dev->host = NULL;
+		dev->port = 0;
+	} else if (err != 0) {
+		return (err);
+	} else {
+		dev = malloc(sizeof(*dev));
+		if (dev == NULL) {
+			free(host);
+			return (ENOMEM);
+		}
+		dev->dd.d_unit = unit;
+		dev->host = host;
+		dev->port = port;
+		if (path != NULL)
+			*path = p;
+	}
+	dev->dd.d_dev = &efihttp_dev;
+	*idev = &dev->dd;
+	return (0);
+}
+
 static int
 _efihttp_fs_open(const char *path, struct open_file *f)
 {
@@ -562,8 +899,11 @@ efihttp_fs_open(const char *path, struct open_file *f)
 	char *path_slash;
 	int err;
 
-	if (!efihttp_init_done)
-		return (ENXIO);
+	/*
+	 * efihttp_init_done is irrelevant here: efihttp_dev_open_legacy()
+	 * already gates on it before a legacy-form open can reach this
+	 * point, and the URL form never touches it at all.
+	 */
 	if (f->f_dev != &efihttp_dev)
 		return (EINVAL);
 	/*