git: 6788e42d53c6 - main - stand/efihttp: Support arbitrary http[N]://host[:port]/path devspecs
- Go to: [ bottom of page ] [ top of archives ] [ this month ]
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);
/*