TLS SNI: names with more than 10 labels are added to the SNI tree but never found or removed
What happens
The native SNI tree in packages/bun-usockets/src/crypto/sni_tree.cpp has an asymmetric label limit. sni_add walks every label of a hostname. sni_remove and sni_find stop at 10 labels and return nullptr (MAX_LABELS is defined but unused). A name with more than 10 labels can be added, but it never matches a handshake and cannot be removed.
Visible effects:
tls.Server#addContext(name, ctx)with a name of more than 10 labels is accepted, but a client that sends thatservernamegets the default certificate. Node selectsctx.Listener::add_server_name(src/runtime/socket/Listener.rs) removes, then adds. For such a name the remove does nothing and the add reports a duplicate. So twoaddContext()calls that land on one tree node (for examplenameandname + ".") makelisten()throwFailed to register SNI for '...'. #43082 covers the half-open server that throw left behind, but not the throw itself.Bun.serve({ tls: [{ serverName }] })uses the same tree and has the same limit.
Repro
Run from the repo root. The fixtures are in test/js/node/tls/fixtures/.
import tls from "node:tls";
import fs from "node:fs";
import { once } from "node:events";
const dir = "test/js/node/tls/fixtures/";
const pem = n => ({ key: fs.readFileSync(dir + n + "-key.pem", "utf8"), cert: fs.readFileSync(dir + n + "-cert.pem", "utf8") });
for (const name of ["a.b.c.d.e.f.g.h.i.example", "a.b.c.d.e.f.g.h.i.j.k.example"]) {
const server = tls.createServer(pem("agent1"), s => s.end());
server.addContext(name, pem("agent2"));
server.listen(0, "127.0.0.1");
await once(server, "listening");
const c = tls.connect({ port: server.address().port, host: "127.0.0.1", servername: name, rejectUnauthorized: false });
await once(c, "secureConnect");
console.log(name.split(".").length, "labels ->", c.getPeerCertificate().subject.CN);
c.end(); server.close();
}
bun 1.4.3, Linux x64:
10 labels -> agent2
12 labels -> agent1
node v26.3.0:
10 labels -> agent2
12 labels -> agent2
Expected
sni_remove and sni_find accept the same names as sni_add. A name of any label count that addContext() accepted is selected at the handshake and can be replaced.
Found during the work on #43082, where the duplicate-add throw is the only public input that reaches the failing listen() path.
Source: oven-sh/bun