Skip to content

outbound: a disarmed DNS door writes no reply produced after the disarm - #556

Merged
CMGS merged 1 commit into
mainfrom
fix/dns-disarm-answer
Nov 10, 2026
Merged

CMGS merged 1 commit into
mainfrom
fix/dns-disarm-answer

Conversation

@CMGS

@CMGS CMGS commented Nov 10, 2026

Copy link
Copy Markdown
Contributor

Root-causes the third flake on the v0.1.31 round's open list, TestDNSDoorDisarmEndsAnOpenConnection, which failed with "a disarmed door answered 29 bytes on a connection opened while armed". It was a real, benign race.

Mechanism. Disarm cancels the resolver, and context.AfterFunc(r.ctx, conn.Close) closes each open stream connection from another goroutine. A query read before that close is still served:

  • Cancel before the exchange: the canceled exchange fails, so the door writes a SERVFAIL, records a DNSFailed event, and sends no upstream query. That SERVFAIL is 12 + 13 + 4 = 29 bytes, the flake's exact size.
  • Cancel after the upstream answered: the real reply is written after the disarm. A mutant of this fix lets the 90-byte answer out.

Fix (outbound/dns.go):

  • serveConn returns without answering a query it reads once the resolver is canceled.
  • It drops any reply when the resolver is canceled before the write, whichever branch the inflight select took.
  • answer records no DNSFailed for an exchange the cancel ended.

Tests:

  • TestADisarmedResolverDropsAQueryItReadsAfterTheDisarm: the resolver is canceled before the read, over an unclosable pipe so AfterFunc cannot win, and repeated 20 times against the select's randomness.
  • TestADisarmLandingWhileAQueryIsAnsweredWritesNoReplyAndRecordsNoFailure:
    • the cancel is injected before the exchange, through Nameservers;
    • and after the answer, through the event recorder;
    • each case expects no reply and no DNSFailed.
  • Mutants, all killed:
    • no early check (the 20-run test answers 29 bytes);
    • no drop after the answer (90 bytes out);
    • DNSFailed recorded for a dropped query.

Cost. One ctx.Err() check per DNS query on the door, and none on the claim path.

/simplify: a 3-line production diff with no helper to reuse.

Gates (fail-fast ship script):

  • lint 0 issues. on darwin and linux; fmt clean; asl clean;
  • race green on darwin and linux/arm64, except the container-only utils test;
  • no new comments.

Disarm cancels the resolver, and context.AfterFunc closes each open
stream connection from another goroutine. A query read before that
close was still answered: a cancel before its exchange wrote a 29-byte
SERVFAIL and recorded a DNSFailed event, and a cancel after the upstream
answered wrote the real reply. serveConn now drops a query read after
the cancel and drops any reply once the resolver is canceled before the
write, and answer records no failure for an exchange the cancel ended.
@CMGS
CMGS merged commit 44ec565 into main Nov 10, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant