Skip to content

id(directory_mail): SmtpSink's reply reader has no total deadline and no line bound — a trickling or never-terminating relay reply hangs the directory request forever #1899

Closed

Description

@dezren39

id(directory_mail): SmtpSink's reply reader has no total deadline and no line bound — a trickling or never-terminating relay reply hangs the directory request forever and buffers without limit

Evidence (verified this run, main d2cb60fe)

pkgs/id/src/directory_mail.rs:

  • :115 RELAY_TIMEOUT = 10s is applied at :164-165 as the socket read/write timeouts — per-read inactivity timeouts, not a deadline for a reply or for the exchange.
  • :233-252 expect(reader, code) loops on read_line (:238): every line whose first 3 chars parse to the expected code and whose 4th byte is - continues the loop. There is no cap on the number of continuation lines, no cap on one line's length, and no overall deadline. read_line buffers a whole line before expect can inspect any of it.
  • The caller awaits the send: pkgs/id/src/world_session/mod.rs:108 runs sink.send(&mail) in spawn_blocking and .awaits it before the directory action returns, so a hung reply hangs the directory request (and permanently occupies a blocking-pool thread per attempt).

This is the sibling of #1788 (CommandSink has no timeout at all). #1788's framing called SmtpSink "10s-bounded" — it is only inactivity-bounded, as the probe shows.

Probe (laptop, verbatim logic)

Probe crate ~/git/fleet/probe-2026-10-28-laneA-smtpexpect/ (laptop): expect() copied verbatim from directory_mail.rs at main d2cb60fe (file sha256 dc6f7935…, identical on laptop and sandbox), same 10s socket timeouts, fake relays on loopback:

  • CONTROL: ordinary multiline reply (250-fake / 250 OK) → Ok(()) in ~0.3 ms.
  • TRICKLE: relay sends a valid 250-… continuation line every 2 s forever → expect still blocked after 25 s; the 10s timeout never fires because every read completes in-window.
  • GIANT: relay sends 250- then floods bytes with no CRLF → expect still blocked after the relay sent up to 32 MiB; read_line keeps buffering the unterminated line without limit.

Why it matters

The relay is loopback and operator-chosen, but it is still a separate process that can be buggy, wedged, or compromised — and the failure mode is the worst kind: not an error, but a directory request (e.g. an email-code sign-in) that never returns, plus unbounded memory growth in the server for the giant-line case. A stalled local SMTP daemon or a misbehaving spam-filter milter in front of it turns into a silent sign-in outage.

Acceptance criteria

  • A reply (and/or the whole send) has a total deadline: a relay that keeps every read in-window but never finishes a reply causes send to fail with a clear timeout error in bounded time.
  • One reply line's buffered length is capped (a named constant); a line past the cap fails the send instead of buffering without limit.
  • The number of continuation lines per reply is capped, or the deadline demonstrably bounds them.
  • Regression tests with a fake relay: trickling continuation lines time out; an over-long line is refused; the existing plain/multiline/dot-stuffing tests still pass.
  • The fix states the chosen bounds where RELAY_TIMEOUT is defined, so the "10s" a reader sees is the bound that actually applies.

Acceptance criteria

  • SmtpSink's reply reader has a total deadline and a maximum line length; a trickling or never-terminating relay reply fails the directory request with a bounded error instead of hanging it.
  • A regression test drives a slow/never-terminating reply and asserts the request completes (with an error) inside the bound; existing directory_mail tests stay green in CI.

Activity

  1. developing-today commented on Oct 28, 2026

    @developing-today
    Contributor
  2. added
    kind/bugSomething isn't working (prefixed form of the classic bug label)
    source/creatorFiled by the new-issue-creator job
    task/triageQueue packet kind: verify, reproduce, and route this item
    priority/normalOrdinary priority (the default)
    queue/queuedHas a queued task in the Supabase agent queue
    app/idPackage/app: id (pkgs/ or apps/ in this repo)
    effort/sSmall: expected under 2 hours of work
    on Oct 28, 2026
  3. dezren39 commented on Oct 28, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · filed

    • Agent: new-issue-creator-code-a (new-issue-creator, lane A)
    • Session / ID: cron new-issue-creator-code-a, 2026-10-28 17:10 CDT run
    • Found via: filed this run
    • Task: SmtpSink expect() reply reader has no total deadline / line bound — verified by verbatim-copy probe on the laptop (control OK; trickle blocked >25s; 32MiB unterminated line still buffering)
    • Expectation: effort/s — triage pins the deadline and line/continuation caps, then a fix with fake-relay regression tests
    • Links: probe ~/git/fleet/probe-2026-10-28-laneA-smtpexpect/ (laptop); sibling id(directory_mail): CommandSink has no timeout — a mail program that never exits hangs the directory request forever #1788 (CommandSink timeout)
  4. added
    meta/enrichedIntake review done: acceptance criteria, links, and normalized labels added
    on Oct 28, 2026
  5. dezren39 commented on Oct 28, 2026

    @dezren39
    MemberAuthor

    Intake review

    🤖 Agent note · filed

    Notes: Filed in the large mcpx filing batch of 2026-10-28 21:59–22:24Z and already carries a queued triage task — it is not orphaned. Verified live at intake: no merged PR references this issue and it was filed under an hour ago, so nothing can have resolved it yet. Acceptance criteria appended to the body from the issue's own stated scope; original body preserved verbatim above it.

    Risks: Tracker-level scope overlaps its sibling batch issues — the triage pass must dedupe against them before any implementation packet is written, and split rather than implement if the scope is multi-PR.

    Suggested approach: Triage first (task 2794): confirm the boundary against the sibling issues, then enqueue one scoped packet per deliverable slice.

  6. dezren39 commented on Oct 28, 2026

    @dezren39
    MemberAuthor

    Intake review

    🤖 Agent note · filed

    Notes: Filed in the large mcpx filing batch of 2026-10-28 21:59–22:24Z and already carries a queued triage task — it is not orphaned. Verified live at intake: no merged PR references this issue and it was filed under an hour ago, so nothing can have resolved it yet. Acceptance criteria appended to the body from the issue's own stated scope; original body preserved verbatim above it.

    Risks: Tracker-level scope overlaps its sibling batch issues — the triage pass must dedupe against them before any implementation packet is written, and split rather than implement if the scope is multi-PR.

    Suggested approach: Triage first (task 2794): confirm the boundary against the sibling issues, then enqueue one scoped packet per deliverable slice.

  7. 178 remaining items

  8. added
    queue/claimedQueue worker lifecycle state
    and removed
    queue/parkedQueue task is parked (waiting on CI, time, or an event)
    on Nov 2, 2026
  9. dezren39 commented on Nov 2, 2026

    @dezren39
    MemberAuthor

    agent-note:v1 — holder: repo-worker-code (OpenChamber scheduled lane), at 2026-11-02T14:57:09Z. event: picked-up; task: 5054; queue: repo-health-code-pilot. Fresh metadata-only revalidation: #1899 OPEN; #4267 remains OPEN/non-draft and #3708 OPEN/draft, both authored and assigned to dezren39. #3708 remains the recorded target-file ownership conflict. No PR contents/checks/reviews, branch, worktree, or source inspected.

  10. dezren39 commented on Nov 2, 2026

    @dezren39
    MemberAuthor

    agent-note:v1 — holder: repo-worker-code (OpenChamber scheduled lane), at 2026-11-02T14:57:13Z. event: parked; task: 5054; queue: repo-health-code-pilot. Parked on the Drew-owned implementation PR and overlapping file ownership. Preserve existing progress; resume only after disposition/handoff, then revalidate.

  11. added
    queue/parkedQueue task is parked (waiting on CI, time, or an event)
    status/parkedDeliberately paused with a recorded resume condition
    and removed
    queue/claimedQueue worker lifecycle state
    on Nov 2, 2026
  12. added a commit that references this issue on Nov 2, 2026
  13. dezren39 commented on Nov 2, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · done

  14. removed
    status/parkedDeliberately paused with a recorded resume condition
    queue/parkedQueue task is parked (waiting on CI, time, or an event)
    work/assignedQueue worker lifecycle state
    on Nov 2, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

app/idPackage/app: id (pkgs/ or apps/ in this repo)effort/sSmall: expected under 2 hours of workkind/bugSomething isn't working (prefixed form of the classic bug label)meta/enrichedIntake review done: acceptance criteria, links, and normalized labels addedpriority/normalOrdinary priority (the default)source/creatorFiled by the new-issue-creator jobtask/small-fixQueue packet kind: bounded fix with reproduction and targeted tests

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions