Component: src/mailbox/BoundedMailbox.ts
Severity (assessment): LOW
The drop-head arm captures the evicted envelope solely to decide whether to increment a counter, then discards it; the drop-new arm never even sees the envelope again. Neither reaches system.deadLetters.
Exploit walkthrough
Not directly exploitable; it is the observability gap that makes several of the higher findings undetectable in production. The dropped envelope carries sender, message, the MDC context and the tracing trace context (src/internal/Mailbox.ts:5-31) — everything needed to attribute the loss — and all of it is thrown away. actor_mailbox_dropped_total{class,path,reason} records that a drop happened but not what was lost, so an operator cannot tell a shed telemetry sample from a dropped Terminated (the HIGH finding above) or a dropped ProducerSend carrying an unsettled confirmation callback (the MEDIUM finding above). An attacker exploiting either of those leaves no forensic trace beyond a counter that looks like normal load shedding.
The same applies to BackoffSupervisor's stash overflow, which drops the oldest buffered message with only a log line and no dead letter (src/pattern/BackoffSupervisor.ts:328-333) — and emits one log.warn per dropped message, so a sustained flood against a supervisor in a backoff window is also a log-amplification vector.
Evidence — src/mailbox/BoundedMailbox.ts
src/mailbox/BoundedMailbox.ts:43-59 —
.with('drop-head', () => {
const dropped = super.removeOldest();
if (dropped !== undefined) {
this.droppedCount++;
this.onDrop?.('drop-head');
}
super.enqueue(env);
})
.with('drop-new', () => {
this.droppedCount++;
this.onDrop?.('drop-new');
})
Contrast with every other loss path, which does dead-letter:
src/internal/ActorCell.ts:441-448 (deadLetterStash), 749-751 (for (const env of this.mailbox.drainUser()) this.system.deadLetters.tell(new DeadLetter(...))), 529-531 and 546-548 (tells to a terminated cell).
The framework's own rationale for why silent loss is unacceptable — src/internal/ActorCell.ts:432-437: "That is the worst shape a lost message can take … and 'I told an actor and nothing happened, anywhere' is unfalsifiable from the outside. Dead-lettering costs nothing and makes it visible."
Why the existing guard does not cover it
droppedCount, the onDrop hook and the actor_mailbox_dropped_total metric with {class, path, reason} labels (src/internal/ActorCell.ts:827-834) are real observability and are documented in docs/operations/tuning/mailbox-sizing.mdx:131-138. The docs are also honest that drop-head means "silent message loss" (mailbox-sizing.mdx:30). The gap is that the framework's own stated standard — dead-letter it so the loss is falsifiable — is applied to the stash and to termination drains but not to overflow.
Suggested fix
Give BoundedMailbox a reference to (or a callback into) the dead-letter path and route every dropped envelope there, preserving sender/context/trace. Do the same for BackoffSupervisor's stash eviction, and rate-limit or aggregate its per-drop log.warn so an overflow cannot amplify into a log flood.
Verification status
Found in the second, independent whole-framework security re-audit of 2026-08-02 (v0.12.0) — a fresh pass run without reference to the first wave's findings, then triaged against the existing tracker and adjudicated by verifiers instructed to refute it.
Verifier note
Confirmed at src/mailbox/BoundedMailbox.ts:43-59. The drop-head arm binds super.removeOldest() to dropped and uses it solely for the !== undefined check that gates droppedCount++ / onDrop; the envelope is then unreferenced. The drop-new arm never sees the envelope at all. Neither arm reaches system.deadLetters, and I verified the contrast the finding draws: src/internal/ActorCell.ts:441-448 dead-letters the stash, :749-751 dead-letters the drained mailbox on termination, and :529-531 / :546-548 dead-letter any tell to a terminated cell — so overflow is the one loss path in the framework that does not. The comment at :427-437 does state the project's rationale for why silent loss is unacceptable. The lost fields are real: src/internal/Mailbox.ts:5-31 shows sender, context (MDC), trace and enqueuedAtMs all travelling on the envelope. src/pattern/BackoffSupervisor.ts:328-333 confirmed verbatim — stash.shift() with a log.warn and no dead letter.
Holding LOW rather than raising it: this is an observability gap, not an exploitable defect, and drop-head being the default mailbox policy (src/util/Constants.ts:96) is what keeps it above INFORMATIONAL — every actor in a default deployment loses envelopes with no recoverable record. Corrections applied above about the existing metric/hook, the cost of the proposed fix on the sender's stack, and the overstated log-amplification framing.
Correction applied: (1) "Only a counter remains" understates the existing signal: both actor_mailbox_dropped_total{class,path,reason} (src/internal/ActorCell.ts:827-834) and the onDrop hook fire on every drop. What is destroyed is the payload and its attribution (sender / MDC / trace), not the fact of the drop — the issue should be framed as "no forensic record", not "no signal". (2) The suggested fix needs a caveat the issue must carry: BoundedMailbox.enqueue runs on the sender's stack, and DeadLetterRef.tell publishes synchronously to every event-stream subscriber (src/internal/DeadLetterRef.ts:22-36). Routing every shed envelope there converts load-shedding into per-message work under exactly the pressure the bound exists to absorb; a sampled, rate-limited or opt-in dead-letter path is the defensible shape. (3) The BackoffSupervisor "log-amplification vector" framing is overstated — it is one log.warn per message, and only while the stash is already full inside a backoff window; that is the same exposure as any per-message log line, not a distinct amplifier.
Component:
src/mailbox/BoundedMailbox.tsSeverity (assessment): LOW
The
drop-headarm captures the evicted envelope solely to decide whether to increment a counter, then discards it; thedrop-newarm never even sees the envelope again. Neither reachessystem.deadLetters.Exploit walkthrough
Not directly exploitable; it is the observability gap that makes several of the higher findings undetectable in production. The dropped envelope carries
sender,message, the MDCcontextand the tracingtracecontext (src/internal/Mailbox.ts:5-31) — everything needed to attribute the loss — and all of it is thrown away.actor_mailbox_dropped_total{class,path,reason}records that a drop happened but not what was lost, so an operator cannot tell a shed telemetry sample from a droppedTerminated(the HIGH finding above) or a droppedProducerSendcarrying an unsettled confirmation callback (the MEDIUM finding above). An attacker exploiting either of those leaves no forensic trace beyond a counter that looks like normal load shedding.The same applies to
BackoffSupervisor's stash overflow, which drops the oldest buffered message with only a log line and no dead letter (src/pattern/BackoffSupervisor.ts:328-333) — and emits onelog.warnper dropped message, so a sustained flood against a supervisor in a backoff window is also a log-amplification vector.Evidence —
src/mailbox/BoundedMailbox.tssrc/mailbox/BoundedMailbox.ts:43-59 —
Contrast with every other loss path, which does dead-letter:
src/internal/ActorCell.ts:441-448 (
deadLetterStash), 749-751 (for (const env of this.mailbox.drainUser()) this.system.deadLetters.tell(new DeadLetter(...))), 529-531 and 546-548 (tells to a terminated cell).The framework's own rationale for why silent loss is unacceptable — src/internal/ActorCell.ts:432-437: "That is the worst shape a lost message can take … and 'I told an actor and nothing happened, anywhere' is unfalsifiable from the outside. Dead-lettering costs nothing and makes it visible."
Why the existing guard does not cover it
droppedCount, theonDrophook and theactor_mailbox_dropped_totalmetric with{class, path, reason}labels (src/internal/ActorCell.ts:827-834) are real observability and are documented in docs/operations/tuning/mailbox-sizing.mdx:131-138. The docs are also honest thatdrop-headmeans "silent message loss" (mailbox-sizing.mdx:30). The gap is that the framework's own stated standard — dead-letter it so the loss is falsifiable — is applied to the stash and to termination drains but not to overflow.Suggested fix
Give
BoundedMailboxa reference to (or a callback into) the dead-letter path and route every dropped envelope there, preservingsender/context/trace. Do the same forBackoffSupervisor's stash eviction, and rate-limit or aggregate its per-droplog.warnso an overflow cannot amplify into a log flood.Verification status
Found in the second, independent whole-framework security re-audit of 2026-08-02 (
v0.12.0) — a fresh pass run without reference to the first wave's findings, then triaged against the existing tracker and adjudicated by verifiers instructed to refute it.Verifier note
Confirmed at src/mailbox/BoundedMailbox.ts:43-59. The
drop-headarm bindssuper.removeOldest()todroppedand uses it solely for the!== undefinedcheck that gatesdroppedCount++/onDrop; the envelope is then unreferenced. Thedrop-newarm never sees the envelope at all. Neither arm reachessystem.deadLetters, and I verified the contrast the finding draws: src/internal/ActorCell.ts:441-448 dead-letters the stash, :749-751 dead-letters the drained mailbox on termination, and :529-531 / :546-548 dead-letter any tell to a terminated cell — so overflow is the one loss path in the framework that does not. The comment at :427-437 does state the project's rationale for why silent loss is unacceptable. The lost fields are real: src/internal/Mailbox.ts:5-31 showssender,context(MDC),traceandenqueuedAtMsall travelling on the envelope. src/pattern/BackoffSupervisor.ts:328-333 confirmed verbatim —stash.shift()with alog.warnand no dead letter.Holding LOW rather than raising it: this is an observability gap, not an exploitable defect, and
drop-headbeing the default mailbox policy (src/util/Constants.ts:96) is what keeps it above INFORMATIONAL — every actor in a default deployment loses envelopes with no recoverable record. Corrections applied above about the existing metric/hook, the cost of the proposed fix on the sender's stack, and the overstated log-amplification framing.Correction applied: (1) "Only a counter remains" understates the existing signal: both
actor_mailbox_dropped_total{class,path,reason}(src/internal/ActorCell.ts:827-834) and theonDrophook fire on every drop. What is destroyed is the payload and its attribution (sender / MDC / trace), not the fact of the drop — the issue should be framed as "no forensic record", not "no signal". (2) The suggested fix needs a caveat the issue must carry:BoundedMailbox.enqueueruns on the sender's stack, andDeadLetterRef.tellpublishes synchronously to every event-stream subscriber (src/internal/DeadLetterRef.ts:22-36). Routing every shed envelope there converts load-shedding into per-message work under exactly the pressure the bound exists to absorb; a sampled, rate-limited or opt-in dead-letter path is the defensible shape. (3) The BackoffSupervisor "log-amplification vector" framing is overstated — it is onelog.warnper message, and only while the stash is already full inside a backoff window; that is the same exposure as any per-message log line, not a distinct amplifier.