Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions ChangeLog.md
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,10 @@ See docs/process.md for more on how version tagging works.
process, or pthreads required. Supports incoming and outgoing TCP, UDP, IPv6,
and `-pthread` with `PROXY_TO_PTHREAD`. Uses the public node APIs where
available, falling back to `tcp_wrap`/`udp_wrap` on older Node.js. (#27080)
- Under `-sNODERAWSOCKETS`, `getaddrinfo` now performs real name resolution
via `node:dns`, blocking the caller where its stack can wait (a proxied
pthread, `ASYNCIFY`/`JSPI`) and returning `EAI_AGAIN` otherwise. Results may
now be a linked list, which `freeaddrinfo` frees in full. (#27693)
- The following symbols are no longer included in `INCOMING_MODULE_JS_API`
by default:
- GL_MAX_TEXTURE_IMAGE_UNITS
Expand Down
56 changes: 55 additions & 1 deletion src/lib/libcore.js
Original file line number Diff line number Diff line change
Expand Up @@ -1007,8 +1007,21 @@ addToLibrary({
return inetPton4(DNS.lookup_name(nameString));
},

getaddrinfo__deps: ['$DNS', '$inetPton4', '$inetNtop4', '$inetPton6', '$inetNtop6', '$writeSockaddr', 'malloc', 'htonl'],
getaddrinfo__deps: ['$DNS', '$inetPton4', '$inetNtop4', '$inetPton6', '$inetNtop6', '$writeSockaddr', 'malloc', 'htonl',
#if NODERAWSOCKETS
'$nodeSockHelpers',
#endif
#if NODERAWSOCKETS && ASYNCIFY
'$Asyncify',
#endif
],
getaddrinfo__proxy: 'sync',
#if NODERAWSOCKETS && (PTHREADS || ASYNCIFY)
// Returns an EAI_* code synchronously, or - for a hostname needing a real
// DNS lookup - a Promise of one, which a sync-proxied pthread awaits and
// ASYNCIFY/JSPI suspend on.
getaddrinfo__async: true,
#endif
getaddrinfo: (node, service, hint, out) => {
// Note getaddrinfo currently only returns a single addrinfo with ai_next defaulting to NULL. When NULL
// hints are specified or ai_family set to AF_UNSPEC or ai_socktype or ai_protocol set to 0 then we
Expand Down Expand Up @@ -1055,6 +1068,27 @@ addToLibrary({
return ai;
}

#if NODERAWSOCKETS
// Resolve via node:dns (which honors the host's /etc/hosts), chaining one
// addrinfo per {family, addr} result into *out.
async function lookupHostname() {
var entries = await nodeSockHelpers.lookupHost(node, family);
if (typeof entries == 'number') return entries;
var head = 0, prev = 0;
for (var entry of entries) {
var ai = allocaddrinfo(entry.family, type, proto, null, entry.addr, port);
if (prev) {
{{{ makeSetValue('prev', C_STRUCTS.addrinfo.ai_next, 'ai', '*') }}};
} else {
head = ai;
}
prev = ai;
}
{{{ makeSetValue('out', '0', 'head', '*') }}};
return 0;
}
#endif

if (hint) {
flags = {{{ makeGetValue('hint', C_STRUCTS.addrinfo.ai_flags, 'i32') }}};
family = {{{ makeGetValue('hint', C_STRUCTS.addrinfo.ai_family, 'i32') }}};
Expand Down Expand Up @@ -1167,6 +1201,25 @@ addToLibrary({
//
// try as a hostname
//
#if NODERAWSOCKETS
// The lookup is asynchronous, so only start it where the calling stack can
// wait on the Promise: a sync-proxied pthread (PROXY_SYNC_ASYNC) awaits it,
// ASYNCIFY/JSPI suspend on it. Otherwise (the event-loop thread itself) it
// must not start at all, since it would write to *out after we have
// returned.
#if PTHREADS
if (PThread.currentProxiedOperationCallerThread) return lookupHostname();
#endif
#if ASYNCIFY
// handleAsync holds a runtime keepalive across the suspension, so a user
// callback completing meanwhile does not exit the runtime under main().
// Everything above this point is pure, so the ASYNCIFY rewind re-running
// this body reaches handleAsync again and takes the stored result.
return Asyncify.handleAsync(lookupHostname);
#else
return {{{ cDefs.EAI_AGAIN }}};
#endif
#else
// resolve the hostname to a temporary fake address
node = DNS.lookup_name(node);
addr = inetPton4(node);
Expand All @@ -1178,6 +1231,7 @@ addToLibrary({
ai = allocaddrinfo(family, type, proto, null, addr, port);
{{{ makeSetValue('out', '0', 'ai', '*') }}};
return 0;
#endif
},

getnameinfo__deps: ['$DNS', '$readSockaddr', '$stringToUTF8'],
Expand Down
3 changes: 2 additions & 1 deletion src/lib/libpthread.js
Original file line number Diff line number Diff line change
Expand Up @@ -1086,7 +1086,8 @@ var LibraryPThread = {
var rtn = func(...proxiedJSCallArgs);
PThread.currentProxiedOperationCallerThread = 0;
if (ctx) {
rtn.then((rtn) => __emscripten_run_js_on_main_thread_done(ctx, ctxArgs, rtn));
// A PROXY_SYNC_ASYNC function may complete synchronously with a plain value.
Promise.resolve(rtn).then((rtn) => __emscripten_run_js_on_main_thread_done(ctx, ctxArgs, rtn));
return;
}

Expand Down
33 changes: 32 additions & 1 deletion src/lib/libsockfs_node.js
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ null;

var NodeSockFSLibrary = {
// Node plumbing shared by the interface methods below.
$nodeSockHelpers__deps: ['$SOCKFS', '$ERRNO_CODES',
$nodeSockHelpers__deps: ['$SOCKFS', '$ERRNO_CODES', '$inetPton4', '$inetPton6',
#if ASSERTIONS
'$warnOnce',
#endif
Expand All @@ -73,6 +73,37 @@ var NodeSockFSLibrary = {
getDgram() {
return nodeSockHelpers.dgramModule ??= (process.getBuiltinModule || require)('dgram');
},
getDns() {
return nodeSockHelpers.dnsModule ??= (process.getBuiltinModule || require)('dns');
},
// Resolve a hostname via node:dns for `family` (AF_UNSPEC for both).
// Resolves to a list of {family, addr} entries, or an EAI_* code: node:dns
// surfaces either getaddrinfo EAI_* names or libuv codes, of which the
// transient ones map to EAI_AGAIN and the rest to "name not found".
lookupHost(name, family) {
var opts = { all: true };
if (family === {{{ cDefs.AF_INET }}}) opts.family = 4;
else if (family === {{{ cDefs.AF_INET6 }}}) opts.family = 6;
return new Promise((resolve) => {
nodeSockHelpers.getDns().lookup(name, opts, (err, addresses) => {
if (err) {
switch (err.code) {
case 'EAI_AGAIN':
case 'ETIMEDOUT':
case 'ESERVFAIL':
case 'EREFUSED':
return resolve({{{ cDefs.EAI_AGAIN }}});
default:
return resolve({{{ cDefs.EAI_NONAME }}});
}
}
if (!addresses.length) return resolve({{{ cDefs.EAI_NONAME }}});
resolve(addresses.map((a) => a.family === 6 ?
{ family: {{{ cDefs.AF_INET6 }}}, addr: inetPton6(a.address) } :
{ family: {{{ cDefs.AF_INET }}}, addr: inetPton4(a.address) }));
});
});
},
// True when node:dgram exposes both synchronous bindSync and connectSync
// (a recent addition), letting UDP run entirely on the public API. A runtime
// missing either falls back to the private udp_wrap handle, which provides
Expand Down
3 changes: 2 additions & 1 deletion src/struct_info.json
Original file line number Diff line number Diff line change
Expand Up @@ -206,7 +206,8 @@
"NI_NAMEREQD",
"EAI_NONAME",
"EAI_SOCKTYPE",
"EAI_BADFLAGS"
"EAI_BADFLAGS",
"EAI_AGAIN"
],
"structs": {
"addrinfo": [
Expand Down
1 change: 1 addition & 0 deletions src/struct_info_generated.json
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@
"EADV": 122,
"EAFNOSUPPORT": 5,
"EAGAIN": 6,
"EAI_AGAIN": -3,
"EAI_BADFLAGS": -1,
"EAI_FAMILY": -6,
"EAI_NONAME": -2,
Expand Down
1 change: 1 addition & 0 deletions src/struct_info_generated_wasm64.json
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@
"EADV": 122,
"EAFNOSUPPORT": 5,
"EAGAIN": 6,
"EAI_AGAIN": -3,
"EAI_BADFLAGS": -1,
"EAI_FAMILY": -6,
"EAI_NONAME": -2,
Expand Down
13 changes: 8 additions & 5 deletions system/lib/libc/musl/src/network/freeaddrinfo.c
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,14 @@
void freeaddrinfo(struct addrinfo *p)
{
#if __EMSCRIPTEN__
// Emscripten's usage of this structure is very simple: we always allocate
// ai_addr, and do not use the linked list aspect at all. There is also no
// aliasing with aibuf.
free(p->ai_addr);
free(p);
// Emscripten allocates each node and its ai_addr separately (no aibuf
// block, no aliasing), so walk the list freeing both.
while (p) {
struct addrinfo *next = p->ai_next;
free(p->ai_addr);
free(p);
p = next;
}
#else
size_t cnt;
for (cnt=1; p->ai_next; cnt++, p=p->ai_next);
Expand Down
4 changes: 2 additions & 2 deletions test/codesize/test_codesize_hello_dylink_all.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"a.out.js": 270520,
"a.out.nodebug.wasm": 588266,
"total": 858786,
"a.out.nodebug.wasm": 588289,
"total": 858809,
"sent": [
"IMG_Init",
"IMG_Load",
Expand Down
8 changes: 4 additions & 4 deletions test/codesize/test_codesize_minimal_pthreads.json
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
{
"a.out.js": 6883,
"a.out.js.gz": 3422,
"a.out.js": 6900,
"a.out.js.gz": 3432,
"a.out.nodebug.wasm": 19147,
"a.out.nodebug.wasm.gz": 8834,
"total": 26030,
"total_gz": 12256,
"total": 26047,
"total_gz": 12266,
"sent": [
"a (memory)",
"b (exit)",
Expand Down
8 changes: 4 additions & 4 deletions test/codesize/test_codesize_minimal_pthreads_memgrowth.json
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
{
"a.out.js": 7341,
"a.out.js.gz": 3639,
"a.out.js": 7358,
"a.out.js.gz": 3648,
"a.out.nodebug.wasm": 19148,
"a.out.nodebug.wasm.gz": 8835,
"total": 26489,
"total_gz": 12474,
"total": 26506,
"total_gz": 12483,
"sent": [
"a (memory)",
"b (exit)",
Expand Down
102 changes: 102 additions & 0 deletions test/sockets/test_dns.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
/*
* Copyright 2026 The Emscripten Authors. All rights reserved.
* Emscripten is available under two separate licenses, the MIT license and the
* University of Illinois/NCSA Open Source License. Both these licenses can be
* found in the LICENSE file.
*
* getaddrinfo() under -sNODERAWSOCKETS: numeric addresses resolve
* synchronously, and any hostname goes to node:dns, returning every address as
* a linked list. That lookup is asynchronous, so it blocks where the calling
* stack can wait (a proxied pthread, JSPI) and is EAI_AGAIN where it cannot
* (built with -DNO_WAIT).
*/

#include <arpa/inet.h>
#include <assert.h>
#include <emscripten/eventloop.h>
#include <netdb.h>
#include <netinet/in.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>

static struct addrinfo* lookup(const char* name, int family, int expect) {
struct addrinfo hints = {0};
hints.ai_family = family;
hints.ai_socktype = SOCK_STREAM;
struct addrinfo* res = NULL;
int err = getaddrinfo(name, "80", &hints, &res);
if (err != expect) {
printf("getaddrinfo(%s) = %d, expected %d\n", name, err, expect);
exit(1);
}
return res;
}

static int count_v4(struct addrinfo* res, const char* addr) {
int n = 0;
for (struct addrinfo* ai = res; ai; ai = ai->ai_next) {
assert(ai->ai_socktype == SOCK_STREAM);
assert(ai->ai_protocol == IPPROTO_TCP);
if (ai->ai_family != AF_INET) continue;
struct sockaddr_in* sin = (struct sockaddr_in*)ai->ai_addr;
assert(ai->ai_addrlen == sizeof(*sin));
assert(ntohs(sin->sin_port) == 80);
if (sin->sin_addr.s_addr == inet_addr(addr)) n++;
}
return n;
}

int ticked = 0;
void tick(void* arg) { ticked = 1; }

int main(void) {
struct addrinfo* res = lookup("10.9.8.7", AF_UNSPEC, 0);
assert(count_v4(res, "10.9.8.7") == 1 && !res->ai_next);
freeaddrinfo(res);

// A hostname is a real node:dns lookup.
#ifdef NO_WAIT
lookup("localhost", AF_INET, EAI_AGAIN);
#else
// A user callback completing while main() is suspended in the lookup must
// not exit the runtime (EXIT_RUNTIME). Under PROXY_TO_PTHREAD the calling
// thread is parked, so the timer only runs once the lookup has returned.
emscripten_set_timeout(tick, 0, NULL);
res = lookup("localhost", AF_INET, 0);
#ifndef __EMSCRIPTEN_PTHREADS__
assert(ticked);
#endif
assert(count_v4(res, "127.0.0.1") == 1);
for (struct addrinfo* ai = res; ai; ai = ai->ai_next) {
assert(ai->ai_family == AF_INET);
}
freeaddrinfo(res);

// AF_UNSPEC returns every address the resolver has, each in its own family.
res = lookup("localhost", AF_UNSPEC, 0);
assert(count_v4(res, "127.0.0.1") == 1);
for (struct addrinfo* ai = res; ai; ai = ai->ai_next) {
if (ai->ai_family == AF_INET6) {
struct sockaddr_in6* sin6 = (struct sockaddr_in6*)ai->ai_addr;
assert(ai->ai_addrlen == sizeof(*sin6));
assert(ntohs(sin6->sin6_port) == 80);
assert(IN6_IS_ADDR_LOOPBACK(&sin6->sin6_addr));
} else {
assert(ai->ai_family == AF_INET);
}
}
freeaddrinfo(res);

struct addrinfo hints = {0};
hints.ai_family = AF_INET;
res = NULL;
int err = getaddrinfo("nonexistent.invalid", NULL, &hints, &res);
assert(err == EAI_NONAME || err == EAI_AGAIN);
assert(!res);
#endif

printf("done\n");
return 0;
}
21 changes: 21 additions & 0 deletions test/test_sockets_node.py
Original file line number Diff line number Diff line change
Expand Up @@ -212,6 +212,27 @@ def test_noderawsockets_udp_ipv6(self):
self.skipTest('no IPv6 loopback available')
self.do_runf('sockets/test_udp_ipv6.c', 'done\n', cflags=['-sNODERAWSOCKETS'])

def test_noderawsockets_dns(self):
# getaddrinfo() resolves numeric addresses synchronously. A hostname needs
# a node:dns lookup, and with no stack able to wait on it is EAI_AGAIN.
self.do_runf('sockets/test_dns.c', 'done\n', cflags=['-sNODERAWSOCKETS', '-DNO_WAIT'])

def test_noderawsockets_dns_blocking(self):
# A hostname blocks on the node:dns lookup, returning every address as a
# linked list: main() is proxied to a worker, which awaits the resolution
# through the sync proxy.
self.do_runf('sockets/test_dns.c', 'done\n',
cflags=['-sNODERAWSOCKETS', '-pthread', '-sPROXY_TO_PTHREAD', '-sEXIT_RUNTIME'])

@requires_jspi_node
def test_noderawsockets_dns_blocking_jspi(self):
# Same, but getaddrinfo() suspends the wasm stack under JSPI.
self.do_runf('sockets/test_dns.c', 'done\n', cflags=['-sNODERAWSOCKETS', '-sEXIT_RUNTIME'])

def test_noderawsockets_dns_blocking_asyncify(self):
# Same, unwinding the wasm stack under ASYNCIFY.
self.do_runf('sockets/test_dns.c', 'done\n', cflags=['-sNODERAWSOCKETS', '-sASYNCIFY', '-sEXIT_RUNTIME'])

def test_noderawsockets_epoll_socket_blocking(self):
# A blocking epoll_wait() on a socket is woken by an incoming datagram
# through the unified readiness wait-queue (the SOCKFS.emit bridge), with
Expand Down
Loading