Skip to content

build: define HAVE_CONNECTX for c-ares on macOS - #46246

Open
robobun wants to merge 3 commits into
mainfrom
robobun/6408e388/cares-darwin-connectx
Open

robobun wants to merge 3 commits into
mainfrom
robobun/6408e388/cares-darwin-connectx

Conversation

@robobun

@robobun robobun commented Oct 20, 2026 •

Copy link
Copy Markdown
Collaborator

Problem

  • macOS builds of c-ares have no TCP Fast Open since v1.3.14. llvm-nm -u finds _connectx in the macOS binaries of v1.1.38 to v1.3.13, not in later ones.
  • build: convert 12 vendored deps from nested-cmake to DirectBuild #29584 replaced upstream's CMake probe with hand-written lists. The DARWIN list (scripts/build/deps/cares.ts:199) has no HAVE_CONNECTX, which ares_set_socket_functions.c:70 needs on macOS.

Fix

  • Add HAVE_CONNECTX to the DARWIN list. The macOS ares_config.h gains one line. No other target's header changes.
  • This is parity: upstream's probe defines the name (CMakeLists.txt:434), and bun's Linux build has Fast Open. Nobody asked, and no gain is measured.
  • Verified: test/internal/macos-cross-config.test.ts fails without the name. Two new macOS cases in test/js/node/dns/dns-tcp-bidirectional-poll.test.ts run first in CI.
  • Self-reviewed: 20 concerns raised, 18 addressed (see Notes).

Background

  • Fast Open can put the first write in the SYN, but only when the resolver granted a cookie earlier. On macOS it needs connectx(2).
  • c-ares uses TCP when a UDP answer is truncated. On macOS its main user is node:dns resolve*().
  • The DARWIN list is the only source of the name. Left out: a check of all hand-written names against upstream's probe, and zlib-ng's ARM_AUXV_HAS_CRC32 on Linux arm64, which build: convert 12 vendored deps from nested-cmake to DirectBuild #29584 also dropped (scripts/build/deps/zlib.ts:201, no issue yet).

Downsides

  • A path that drops SYNs with the Fast Open option costs 1 s per new TCP connection, as on Linux today. { timeout: 1000, tries: 1 } then fails with ETIMEOUT.
  • A path that drops SYN data costs one try: tries: 1 gets ECONNREFUSED (source reading).
  • macOS binary: one more import, +132 bytes of code (arm64), +128 (x86_64).
Notes

Release binaries

llvm-nm -u <bun> | grep -x _connectx, on the GitHub release zips and the @oven/bun-darwin-* npm packages:

Version arm64 x64
1.1.37 no
1.1.38, 1.2.0, 1.3.0, 1.3.12 yes
1.3.13 yes yes
1.3.14, 1.4.0, 1.4.1, 1.4.2, 1.4.3 no no
  • Update c-ares #15435 brought in the first c-ares with Fast Open (upstream added it in 1.33.0). v1.1.38 is the first release with it.
  • The only connectx( call in src/, packages/ and vendor/ is the c-ares one.
  • Bun's macOS deployment target is 13.0. macOS has connectx() since 10.11.

How the name went missing

Why parity, and not speed

  • The tracker has no issue about connectx, TCP Fast Open or HAVE_CONNECTX. Nobody reported the loss, and this PR has no measurement of a gain.
  • A round trip is saved only when both sides support Fast Open. Upstream says so in Implement TCP FastOpen (TFO) RFC7413 c-ares/c-ares#840: "when both sides support TCP FastOpen, which is unfortunately not always the case. For instance, 1.1.1.1 appears to support TFO, however 8.8.8.8 does not."
  • What the name restores is upstream's probe answer, the behavior of bun's macOS binaries up to v1.3.13, and the behavior of bun's Linux and FreeBSD builds.

What other builds do

  • Linux: the official bun-linux-x64 v1.4.3 uses Fast Open for c-ares today. With a preload shim that logs socket calls, dns-tcp-bidirectional-poll-fixture.ts shows setsockopt(IPPROTO_TCP, TCP_FASTOPEN_CONNECT, 1) = 0, then sendto() with the address on the TCP socket.
  • Linux and FreeBSD need no config name. ares_set_socket_functions.c:58-69 keys them on TCP_FASTOPEN_CONNECT and TCP_FASTOPEN from <netinet/tcp.h>.
  • Homebrew's c-ares 1.34.8 bottles (arm64 Sequoia, x64 Sonoma) import _connectx. Homebrew's node links that library (--shared-cares).
  • The official Node.js macOS builds (v22.23.3, v24.22.0, v26.3.0, v26.12.0) do not import _connectx. Node's checked-in deps/cares/config/darwin/ares_config.h is from c-ares 1.26.0, which is older than Fast Open in c-ares.

Other names, and what this PR leaves out

  • In the five c-ares lists, HAVE_CONNECTX is the only name whose absence turns a feature off.
  • macOS: HAVE_AVAILABILITYMACROS_H and HAVE_SYS_RANDOM_H only add an #include. HAVE_MSG_NOSIGNAL is redundant, because c-ares sets SO_NOSIGPIPE on macOS.
  • Windows: without HAVE_STRICMP and the related names, c-ares uses its own compare loop.
  • Linux and Android: HAVE_ARC4RANDOM_BUF is absent. getrandom is used.
  • zlib-ng, same class, not changed here: scripts/build/deps/zlib.ts:201 defines ARM_AUXV_HAS_NEON for Linux arm64 and not ARM_AUXV_HAS_CRC32. vendor/zlib/arch/arm/arm_features.c:29 then reports no CRC32 support, and functable.c:250 never selects crc32_armv8. Upstream probes the name (CMakeLists.txt:748). The fix changes the Linux arm64 binaries and needs a CRC-32 number from an arm64 machine, so it is a change of its own.
  • Remove dead code from bun_http_jsc, the node:http fallback handle, and the c-ares config #45753, Remove dead code from bun_core, bun_sys, console.table, and three dependency configs #44343 and build: define the libarchive config.h names that 3.8.9 reads on Windows and FreeBSD #44489 correct other names in these hand-written lists. A check that covers the class is a separate decision. One option is upstream's CMake, configured for each target, compared with the lists.
  • Also considered for this name alone: a link-time check that the macOS binary imports _connectx, and a patch that makes c-ares ignore the macro. binary-expectations.ts states the binary's contract with the machine it runs on (exports, libraries, symbol versions), and a required import would be a new kind of rule there. The patch diverges from upstream.

Costs on a bad network path

Measured on Linux (bun 1.4.3, loopback, a network namespace with iptables -A OUTPUT -p tcp --syn --tcp-option 34 -j DROP). Linux builds have Fast Open today, so these are costs that Linux users already have.

Case Result
dns-tcp-bidirectional-poll-fixture.ts (timeout: 1000, tries: 1) ETIMEOUT after about 1.2 s
The same, client Fast Open off answer in about 60 ms
Default options, one nameserver, three truncated answers 1049, 1014 and 1024 ms
Default options, no drop rule 12, 3 and 1 ms
  • c-ares asks for Fast Open on each new TCP connection and keeps no memory of a timeout (ares_conn.c:376-380).
  • XNU puts the option on the first SYN with or without a cookie (tcp_tfo_check and tcp_tfo_write_cookie in bsd/netinet/tcp_output.c). A SYN retransmit calls tcp_disable_tfo() for that socket (tcp_timer.c).
  • XNU's back-off is keyed by the local network, not by the destination (tcp_cache_hash_src in tcp_cache.c), and needs two losses.
  • XNU sets ENODATA on a socket that sent SYN data when it decides that a middlebox dropped the data (tcp_timer.c, tcp_input.c). c-ares has no case for that errno: ares_socket.c:58-98 maps it to a generic failure, and ares_process.c:531-537 fails the connection. This needs a resolver that granted a cookie earlier. No test reaches it.
  • Bun has no switch that turns Fast Open off.

Tests

  • macos-cross-config.test.ts: the new case checks four targets. macOS arm64 and x64 define the name, Linux and FreeBSD do not. It fails when the name is removed from DARWIN, and when it is added to the shared POSIX list.
  • dns-tcp-bidirectional-poll.test.ts, first new case (macOS with a C compiler): a dylib with a __DATA,__interpose entry for connectx is inserted into the existing fixture with DYLD_INSERT_LIBRARIES. The case expects one call with flags 3 (CONNECT_RESUME_ON_READ_WRITE | CONNECT_DATA_IDEMPOTENT). test/js/bun/dns/resolve-dns.test.ts uses the same mechanism.
  • Second new case (macOS): the fixture calls resolveTxt() with RES_OPTIONS=use-vc and then blocks its event loop. The test answers the query and only then releases the fixture. With Fast Open, c-ares writes the query inside resolveTxt(), so the answer waits at the first poll, and kqueue reports readable and writable in one batch. With connect() the query is not sent before the first poll, and the case times out.
  • On Linux the second case passes with bun 1.4.3 (Fast Open on) and times out in a network namespace with net.ipv4.tcp_fastopen=0, which is the connect() flow. The case is limited to macOS: Linux accepts the early write only when the loopback handshake is already complete, and macOS queues it in the kernel.

Local checks

  • Generated headers, merge base against this branch: macOS arm64 and x64 gain #define HAVE_CONNECTX 1. Linux (glibc, musl, Android), FreeBSD and Windows are byte-identical. ares_build.h, the -D defines and the source list are identical on every target.
  • macOS compile of the c-ares sources with bun's release flags for darwin (no LTO), against the macOS libc headers that zig 0.14.0 ships: 79 of 91 sources compile for each of arm64 and x86_64, before and after, with 0 warnings. The other 12 need arpa/nameser.h, notify.h or SystemConfiguration, which that header set does not have. None of them reads the name.
  • One object differs: ares_set_socket_functions.o. __text goes from 1524 to 1656 bytes on arm64 and from 1702 to 1830 bytes on x86_64. Its undefined symbols gain _connectx and nothing else.
  • XNU, bsd/netinet/tcp_usrreq.c: with CONNECT_RESUME_ON_READ_WRITE, connectx() sets the state to SYN_SENT and sends nothing. The first sendto() then sends the SYN. The kernel ignores the address argument because the state is already SYN_SENT.
  • tsc -p scripts/build/tsconfig.json and prettier --check pass.

macOS

  • CI compiles c-ares with the generated header for macOS arm64 and x64, with Apple's SDK, and links both binaries.
  • CI's link check counts the imported symbols of bun-profile: 714 on macOS arm64 and 716 on x64 with this change, 713 and 715 for the merge base. The check prints no names.
  • With the build of the first commit, dns-tcp-bidirectional-poll.test.ts (the case that was there before) passes on macOS 15 for arm64 and x64. It passes on main too.
  • Bun polls the c-ares sockets itself (on_dns_socket_state in src/runtime/dns_jsc/dns.rs). With and without Fast Open, c-ares asks for readable and writable, then readable only. FilePoll: support readable+writable on one poll (fixes c-ares TCP DNS assertion) #29955 reworked how Bun's poll code handles both directions on one descriptor.

History, and c-ares 1.34.9

  • v1.1.38 to v1.3.13 ran connectx() on macOS with c-ares up to 1.34.6 and with the poll code from before FilePoll: support readable+writable on one poll (fixes c-ares TCP DNS assertion) #29955. This PR is the first build that runs it with c-ares 1.34.8 and the current poll code. The old releases are not a record for that combination.
  • c-ares 1.34.9 is a security release (2026-10-19). It is not in tree, and this PR does not depend on it. The macOS lanes of that bump are the first run of connectx() with 1.34.9. If dns-tcp-bidirectional-poll.test.ts goes red there, revert the HAVE_CONNECTX line and do not hold the bump.

Self-review

  • A review of this PR raised 20 concerns. 18 are addressed: the two macOS runtime cases, the reason (parity, not speed), the costs on a bad path, the history wording, the import counts, the c-ares 1.34.9 order, and the scope of the "only name" statement.
  • Not done: an issue for the zlib-ng loss. It is named above, and no issue exists yet.
  • Not done: removing the Linux and FreeBSD keys from the config case. The name has no effect there, and the keys state a fact about those targets.

Limits

  • I ran nothing on macOS myself and linked no macOS binary. Apple's SDK is not reachable from the machine I used.
  • I could not download the macOS binaries that CI built, so llvm-nm -u on them is not done. The import count and the object-level compile are the evidence that the import returns.
  • The tests do not prove that the kernel sends data in the SYN. Bun's listeners do not enable Fast Open, so no test server grants a cookie.

c-ares turns TCP Fast Open on for macOS only when ares_config.h defines
HAVE_CONNECTX (ares_set_socket_functions.c). The hand-written DARWIN
list never had the name, so macOS builds since v1.3.14 compile Fast
Open out. Upstream's CMake defines the name when connectx() exists.

Add a config test that reads the generated header.
@robobun

robobun commented Oct 20, 2026 •

Copy link
Copy Markdown
Collaborator Author
Updated 6:04 AM PT - Oct 21st, 2026

✅ @robobun, your commit 2c0d0099c6311b4e309c114203a5137f7767f1e0 passed in Build #129960! 🎉


🧪   To try this PR locally:

bunx bun-pr 46246

That installs a local version of the PR into your bun-46246 executable, so you can run:

bun-46246 --bun

@coderabbitai

coderabbitai Bot commented Oct 20, 2026 •

Copy link
Copy Markdown
Contributor

Warning

Review limit reached

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

Next included review available in 19 seconds.

Check out review usage here.

View limit details

Limit details: You’ve used all 10 included reviews currently available.

Learn how review limits work.

Review configuration:

⚙️ Run configuration
  • Configuration used: Repository: oven-sh/bun/.coderabbit.yaml
  • Review profile: ASSERTIVE
  • Plan: Advanced
  • Run ID: 2c3823c3-d198-48d5-80df-5e37a9468307
📥 Commits

Reviewing files that changed from the base of the PR and between 9b4284f and 2c0d009.

📒 Files selected for processing: 4
  • scripts/build/deps/cares.ts
  • test/internal/macos-cross-config.test.ts
  • test/js/node/dns/dns-tcp-answer-before-poll-fixture.ts
  • test/js/node/dns/dns-tcp-bidirectional-poll.test.ts
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@robobun

robobun commented Oct 20, 2026 •

Copy link
Copy Markdown
Collaborator Author

Status: waiting for CI on the head commit. Two new macOS test cases have their first run there.

  • How to see the defect on main:
    grep -c HAVE_CONNECTX scripts/build/deps/cares.ts                # 0
    rg --no-ignore -n HAVE_CONNECTX vendor/cares/src/lib/*.c         # ares_set_socket_functions.c:70 reads it
    llvm-nm -u bun-darwin-aarch64/bun | grep -x _connectx            # release zips: v1.3.13 prints _connectx, v1.3.14 to v1.4.3 print nothing
  • Scope: one name in the DARWIN list of scripts/build/deps/cares.ts, one config case in test/internal/macos-cross-config.test.ts, and two macOS cases with one fixture in test/js/node/dns/.
  • Verified locally on 2c0d009 (Linux): the config case fails without the name and passes with it. The generated ares_config.h gains one line on macOS and is byte-identical on Linux, FreeBSD and Windows. A local compile of c-ares for macOS (arm64, x86_64) changes one object, which now imports _connectx. The second new macOS case, run on Linux without its platform gate, passes with Fast Open on and times out with Fast Open off.
  • macOS so far, from CI build 129678 on ad1aa3c (181 of 181 jobs passed): both macOS build-bun jobs compile and link c-ares with the name defined. The link check counts 714 imported symbols on arm64 and 716 on x64, against 713 and 715 for the merge base (build 129665). The DNS-over-TCP case that existed before passes on macOS 15.7.7 arm64 and on macOS 15.7.9 x64.
  • Not verified yet: the two new macOS cases have not run on a Mac. One sees the connectx() call through an inserted dylib. The other needs the early write of the query. I could not download the macOS binaries that CI built, so I did not run llvm-nm -u on them.
  • Open for a maintainer: the cost on a network path that drops Fast Open SYNs (1 s per new DNS-over-TCP connection, see Downsides), and the zlib-ng name that the same conversion dropped on Linux arm64, which this PR names and does not change.

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nothing blocking. The comments below are optional suggestions. There is no need to push a fix for them before merging.

Beyond the inline finding, I also checked whether enabling HAVE_CONNECTX could leave connectx() undeclared on the darwin target: scripts/build/deps/cares.ts:82 already passes _DARWIN_C_SOURCE for darwin, which is what Apple's sys/socket.h gates the connectx prototype on, so no implicit-declaration risk. The runtime effect (c-ares now taking the connectx() TCP Fast Open path for DNS-over-TCP on macOS) could not be exercised from this host; the vendored c-ares source is not in this checkout and nothing here ran on macOS, so the macOS CI run of test/js/node/dns/dns-tcp-bidirectional-poll.test.ts is the first real check of that path.

Extended reasoning...

Two-file, +11-line change: adds HAVE_CONNECTX to the DARWIN define table in scripts/build/deps/cares.ts (so macOS ares_config.h enables c-ares' connectx-based TCP Fast Open path) and a host-side test asserting the generated darwin header contains the define. No security-sensitive surface; it is a build-config flag change whose only runtime effect is on macOS DNS-over-TCP connects. Deferring rather than approving because the behavioral consequence runs only on macOS, which neither the author nor this review could exercise, and an inline test-coverage finding was posted.

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM — a one-line addition to the darwin define list, and the follow-up commit covers what I asked for in the earlier inline comment (both darwin arches, plus absence on linux and freebsd).

What was reviewed:

  • configH() in scripts/build/deps/cares.ts splices DARWIN only in the cfg.darwin branch, so linux/android/freebsd/windows headers are unchanged; _DARWIN_C_SOURCE is already set for darwin targets, so the connectx() prototype is visible to the compile.
  • The new test reads the literal ares_config.h string off cares.build(cfg) with a kind === "direct" / typeof === "string" guard (headers are typed string | HeaderSubst), and the linux/freebsd cases pass explicit /fake sysroots the same way the neighboring tests do, so resolveConfig never probes the filesystem.
  • Restores the _connectx import that shipped in macOS binaries from v1.1.38 through v1.3.13, so this is not a first-time code path; the author notes nothing ran on macOS locally, so the macOS CI lane (dns-tcp-bidirectional-poll test) is the on-target check.
Extended reasoning...

The diff adds HAVE_CONNECTX to the darwin-only define list in scripts/build/deps/cares.ts and a config test in test/internal/macos-cross-config.test.ts that asserts the define is present for darwin aarch64 and x64 and absent for linux and freebsd; no vendored source, patch, or commit pin changes. It touches no security-sensitive surface. Neither file is covered by .github/CODEOWNERS, the change is 22 insertions with a test that would fail without the define, and the only open feedback on the PR (my own earlier inline comment on test coverage) was addressed by commit 8b78ef3. The c-ares sources are not vendored in this checkout, so the claim that c-ares gates its macOS TCP Fast Open path on HAVE_CONNECTX with no companion define rests on the PR's own object-file evidence and prior release binaries rather than a read of the pinned upstream source here.

Two macOS cases in dns-tcp-bidirectional-poll.test.ts:

- A dylib inserted into the fixture prints the flags of each connectx()
  call. c-ares must call it once, with
  CONNECT_RESUME_ON_READ_WRITE | CONNECT_DATA_IDEMPOTENT.
- A fixture blocks its event loop right after resolveTxt(). With Fast
  Open the query is already written, so the answer waits at the first
  poll, readable and writable in one batch.

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nothing blocking. The comments below are optional suggestions. There is no need to push a fix for them before merging.

Beyond the inline findings, I checked that configH in scripts/build/deps/cares.ts splices DARWIN only under the cfg.darwin branch and that nothing else in the tree reads HAVE_CONNECTX, so the define cannot reach the Linux, FreeBSD, Android or Windows headers. The four-target matrix case added in the latest commit covers the x64 and negative-contract point raised on the earlier thread.

Extended reasoning...

The change adds one define (HAVE_CONNECTX) to the darwin list in scripts/build/deps/cares.ts, switching c-ares DNS-over-TCP on macOS from connect() to connectx() with TCP Fast Open flags, and adds a configure-time matrix test plus two macOS-only runtime tests with a new fixture. No auth, crypto or input-parsing surface is touched; the runtime effect is a networking behaviour change in release macOS binaries that the author says was not exercised locally on macOS. Three inline findings on the new tests are posted; the build-side define itself was confirmed darwin-only by reading configH, and the earlier inline thread about x64 and the negative contract is covered by the new matrix case.

Comment on lines +136 to +138
stderr: "pipe",
});

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 (optional) A maintainer whose fixture dies before the query reaches the server gets a silent test timeout with no stderr, instead of a failing assertion. At test/js/node/dns/dns-tcp-bidirectional-poll.test.ts:138 await answered only resolves from the server's data handler; nothing rejects it when the spawned fixture exits early (bad argv[2] for setServers, a thrown resolveTxt, a crash), so the test hangs until the runner timeout and proc.stderr is never read or printed. Fix: race the readiness promise against fixture exit so any early exit rejects with the collected stderr and exit code (e.g. Promise.race([answered, proc.exited.then(async code => { throw new Error(...) })])), which also keeps the existing pass path unchanged.

Why this was flagged

The fixture test/js/node/dns/dns-tcp-answer-before-poll-fixture.ts is spawned at test/js/node/dns/dns-tcp-bidirectional-poll.test.ts:129-135 with stdin, stdout and stderr all piped. The test then awaits answered at line 138, a promise resolved only inside the Bun.listen data handler at lines 117-123 once a full TCP DNS frame arrives. If the fixture exits before sending that frame (for example resolver.setServers throwing on the argv value, resolveTxt failing synchronously, or a runtime crash in a debug/ASAN build), no code path rejects answered: proc.exited is not raced and stderr is only drained at line 142, after the await. The test therefore sits until bun test's per-test timeout and reports a timeout with none of the fixture's stderr, whereas a wired failure would fail immediately with the fixture's message. The base branch has no such test, so this only affects diagnosability of the new test, but the repository review rules explicitly require every failure event (process exit included) to reject the awaited promise.

Verification: Triggers whenever the spawned fixture exits before it sends a complete TCP DNS frame to the local server. In test/js/node/dns/dns-tcp-bidirectional-poll.test.ts, the test does await answered; at line 139 with nothing racing it against proc.exited. So a fixture that dies early leaves the test pending until the runner's per-test timeout, and its stderr is never surfaced.

Comment on lines +14 to +23
let calls = 0;
resolver.resolveTxt("example.test", (err, records) => {
calls++;
// Print one turn of the event loop later, so that a second call is counted.
setImmediate(() => {
console.log(JSON.stringify({ calls, code: err ? (err as NodeJS.ErrnoException).code : null, records }));
process.exit(0);
});
});

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 nit (optional): the "delivered once" half of the new macOS test cannot fail, so a double delivery would pass unnoticed. The fixture counts invocations of a node:dns callback, but Bun invokes that callback from a promise .then (src/js/node/dns.ts:647-656), so calls is 1 whenever anything is printed at all; the setImmediate deferral at dns-tcp-answer-before-poll-fixture.ts:18 adds nothing. Fix: drop the counter and the deferral (keep the records/exit assertion) and name the test for what it checks, an answer already readable at the first poll is delivered; or count at a layer that can observe a second completion.

Why this was flagged

The test at test/js/node/dns/dns-tcp-bidirectional-poll.test.ts:115-149 asserts calls: 1 from the fixture's JSON. The fixture increments calls inside the resolveTxt callback (dns-tcp-answer-before-poll-fixture.ts:15-16) and prints one event-loop turn later (line 18-21). Resolver.prototype.resolveTxt calls the user callback from the native promise's .then handlers (src/js/node/dns.ts:647-656), and a promise settles at most once; natively, the first c-ares completion consumes the ResolveInfoRequest (src/runtime/dns_jsc/dns.rs:5443-5447), so a second c-ares callback would be a use-after-free rather than a second JS call. Therefore calls is always exactly 1 whenever the line is printed, and 0 is never printed (the only print is inside the callback); the calls field and the setImmediate deferral can never turn the test red. The useful part of the test (answer already in the socket at the first poll is delivered, process exits 0 with [["hello"]]) still works. This is a test-quality nit; the base branch has no such test.

Verification: Resolver.prototype.resolveTxt in /home/claude/bun/src/js/node/dns.ts:647-656 invokes the user callback only from .then handlers on the native promise; a promise settles at most once, so exactly one of those handlers runs, once. Therefore in /home/claude/bun/test/js/node/dns/dns-tcp-answer-before-poll-fixture.ts:14-22 calls is always exactly 1 whenever the JSON line is printed at all.

// when its config defines HAVE_CONNECTX. The answer is the same with connect(),
// so the call itself is observed: a dylib inserted into the fixture prints the
// flags of each connectx() and forwards it.
test.skipIf(!isMacOS || !cc)("c-ares opens its TCP DNS connection with connectx() on macOS", async () => {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 nit (optional): the file now runs three independent subprocess tests plus a cc compile strictly in series, while test/CLAUDE.md asks that tests which each spawn processes use test.concurrent unless that is hard. Each new test owns its own tempDir, port 0 server and fixture process, so nothing is shared. Fix: declare the three tests with test.concurrent.skipIf(...) (or wrap them in describe.concurrent) so the macOS lane does not pay the compile and three Bun startups back to back, especially on debug/ASAN builds.

Why this was flagged

test/js/node/dns/dns-tcp-bidirectional-poll.test.ts:18, :37 and :110 declare test.skipIf(...) serial tests; the second one compiles a dylib with cc at lines 56-66 and then spawns bunExe(), the third starts a Bun.listen server and spawns another bunExe(). None of the three shares state: separate tempDir, ephemeral ports, separate fixture files. test/CLAUDE.md 'Prefer concurrent tests over sequential tests' applies when multiple tests in one file spawn processes. On the base branch the file had a single test, so the serial cost is introduced by this diff; on a debug+ASAN macOS lane three sequential Bun startups plus a compile add several seconds of wall time with no stated reason for staying serial.

Verification: Triggering condition: any macOS run of the file, where all three tests now execute. test/js/node/dns/dns-tcp-bidirectional-poll.test.ts declares three serial tests at lines 18, 37 and 110, none of which shares state. test/CLAUDE.md line 22 states: "Prefer concurrent tests over sequential tests". Nothing breaks functionally; the cost is serial wall time (a C compile plus three Bun startups).

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant