Skip to content

Fix six findings from the 0.7.0 API/lifecycle review - #77

Merged
danReynolds merged 3 commits into
mainfrom
danreynolds/0.7.1-review-fixes
Jul 21, 2026
Merged

Fix six findings from the 0.7.0 API/lifecycle review#77
danReynolds merged 3 commits into
mainfrom
danreynolds/0.7.1-review-fixes

Conversation

@danReynolds

Copy link
Copy Markdown
Owner

A fresh adversarial review of the surfaces the transport audit didn't hammer (public API, worker lifecycle, Go non-transport) surfaced these. Every finding was verified against the code before fixing.

# Sev Fix
1 High closeAllServePublications made unbounded context.Background() LocalAPI calls while holding the global mu on the down/logout/start path → a wedged tailscaled froze the whole binding. Bounded every LocalAPI call via boundedCallCtx(0). #71's invariant had only covered dial/ping — 9 wrappers were still unbounded.
2 Med Non-root funnel mount didn't strip its prefix → funnel.forward(path:'/api') sent the backend /api/foo not /foo. Added funnelMountHandler (http.StripPrefix), matching Serve/tailscaled.
3 Med send()request.finalize() sat outside the try/catch; a throw (re-sent/custom request) leaked the request fd (no finalizer) + pinned the Go goroutine. Isolated finalize().
4 Med A peer's zero LastSeen parsed to a year-1 DateTime instead of the documented null. _parseTime now maps the zero-time sentinel to null.
5 Med nodes()/whois()/useById() threw synchronously instead of rejecting the Future (.catchError missed them). Made async.
6 Med exitNode.suggest() threw for the transient "not ready yet" errors (common right after up()) instead of returning null. Mapped ErrNoPreferredDERP/ErrNoNetMap (by message, like isNotFound) to a null suggestion.

Also corrected the funnel.clear() doc (it doesn't touch Serve).

Tests

Discriminating regression tests for #2 (funnel strip), #4 (lastSeen null), #5 (async rejection), #6 (transient matcher). #1 (30s deadline) and #3 (needs a live native node) aren't practically unit-testable and rely on the shared boundedCallCtx helper + review.

Verification

Darwin go vet + -race clean; dart analyze clean; unit + fd/ffi integration +117. Linux container + Headscale e2e running. Version bump to 0.7.1 + changelog to follow in this PR after review.

🤖 Generated with Claude Code

danReynolds and others added 3 commits July 21, 2026 17:09
A fresh adversarial review of the un-hammered surfaces (public API, worker
lifecycle, Go non-transport) surfaced these; all verified against the code.

1. LocalAPI freeze (high): closeAllServePublications made unbounded
   context.Background() calls while holding the global mu on the
   down/logout/start path, so a wedged tailscaled froze the whole binding.
   Bound every LocalAPI call via boundedCallCtx(0) (30s) — #71's invariant had
   only covered dial/ping; 9 wrappers were still unbounded.

2. Funnel path prefix (med): a non-root funnel mount didn't strip its prefix,
   so funnel.forward(path:'/api') sent the backend /api/foo not /foo. Added
   funnelMountHandler (http.StripPrefix), matching Serve/tailscaled.

3. HTTP client fd leak (med): in send(), request.finalize() sat outside the
   try/catch; a throw (re-sent/custom request) leaked the request fd (no
   finalizer) and pinned the Go goroutine. Isolated finalize() so a failure
   releases both fds.

4. lastSeen garbage (med): a peer's zero LastSeen ('0001-01-01T00:00:00Z')
   parsed to a year-1 DateTime instead of the documented null. _parseTime now
   treats the zero-time sentinel as null.

5. Sync-throw (med): nodes()/whois()/useById() threw synchronously instead of
   rejecting the returned Future, so .catchError missed them. Made async.

6. suggest() throws vs null (med): exitNode.suggest() threw for the transient
   'not ready yet' errors (ErrNoPreferredDERP/ErrNoNetMap) common right after
   up(). Mapped those to a null suggestion (matched by message, like
   isNotFound, since they cross the LocalAPI boundary).

Also corrected the funnel.clear() doc (it doesn't touch Serve).

Discriminating tests for 2/4/5/6 (funnel strip, lastSeen null, async
rejection, transient matcher). #1 (30s timeout) and #3 (needs a live native
node) aren't practically unit-testable and rely on the shared boundedCallCtx
helper + review.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… test-wiring gaps

The adversarial review of the fixes found them correct but surfaced two
completeness gaps:

- Context sweep was incomplete: DuneStatus/DunePeers (go/lib.go) still made
  unbounded context.Background() LocalAPI calls — the same one-shot pattern
  fixed in localapi.go. Bounded both; a full-tree sweep now confirms every
  remaining context.Background() is a WithTimeout/WithCancel base. The
  'no unbounded native call' invariant is complete.

- The funnel and suggest regression tests only exercised the helper, not the
  production wiring — reverting the wiring line left them green. Extracted
  newFunnelTarget and exitNodeSuggestResult (the exact constructors the
  exports use) and pointed the tests at them, so a wiring regression now
  fails. Verified: reverting the funnel strip fails TestFunnelTargetStrips.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@danReynolds
danReynolds merged commit de5e227 into main Jul 21, 2026
3 checks passed
@danReynolds
danReynolds deleted the danreynolds/0.7.1-review-fixes branch July 21, 2026 23:00
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