fix(conn_slab): decommit slot buffer pages inside the slab mutex - #300
Open
detail-app[bot] wants to merge 1 commit into
Open
detail-app[bot] wants to merge 1 commit into
detail-app[bot] wants to merge 1 commit into
Conversation
ApprovabilityVerdict: Approved at Macroscope's review found this PR approvable — This is a focused concurrency bug fix that keeps the existing page-decommit operation under the slab mutex, preventing reuse of a slot while its pages are being discarded. Its only broader effect is a bounded mutex hold around one connection-close syscall, with no API, schema, deployment, or sensitive-area changes. You can add or adjust custom eligibility rules. Learn more. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Detail bug report: View on Detail
Fixes ENG-701
Bug
ConnSlab.releaseinsrc/core/conn_slab.zigdecommitted a slot's buffer pages withmadviseafter the slab mutex was released. Thedefer self.mutex.unlock(io)was scoped to an inner labeledblk:block, so it fired atbreak :blk— not at function return — leaving themadviserunning lockless.The free list is LIFO, so the just-freed slot is the very next one
claimreturns. The stdio frontend spawns one OS-threaded task per accepted connection (server.zig→Lifecycle.spawn→group.concurrent), andconn.zigbinds the new connection'snet.Stream.Reader/Writertoslab.recvBuf/sendBufimmediately onclaim. A concurrently accepted connection could therefore start writing those pages while the releasing thread'smadvise(MADV_DONTNEED)(Linux) /FREE_REUSABLE(macOS) was discarding them, silently zeroing the live owner's data — surfacing as spurious400 Bad Request(zeroed HTTP head) or garbled/truncated responses and upstream bodies. There was no lock, fence, or stale-state re-check between unlock andmadvise.Fix
Moved the
madvisedecommit inside the slab mutex critical section:releasenow takes a function-scopedefer self.mutex.unlock(io)and performs the decommit before the slot becomes claimable. The decommit only ever touches pages of a slot that is still exclusively.freeand unreachable by a concurrentclaim.releasefires once per connection lifetime (not per request), so adding onemadvisesyscall to the lock hold is negligible on a path dominated by the upstream round trip, and the slab mutex is uncontended on the close path in the common case.Added a regression test (
release decommits page-aligned slot buffers before the slot is claimable) that exercises the realmadvisepath with page-sized buffers — the existing tests use sub-page buffers so thealigned_end ≤ aligned_baseguard skips the decommit entirely — and asserts the slab survives the in-lock decommit (on Linux, confirmsMADV_DONTNEEDzeroed the re-claimed buffer; on other platforms, confirms the buffer is still usable).Testing
zig build test --summary all(520 pass, 1 pre-existing skip — incl. the new madvise-path test),zig fmt --check,ziglint, andzig build otlp -Dfrontend=stdio(builds the affected stdio frontend).zig test src/core/conn_slab.zig: all 8 conn_slab/limits tests pass.std.Io.Threaded, page-aligned slot buffers, LIFO free-list, and the realConnSlab.release/claim— running 2,000,000 claim-write-verify cycles (4 thread pairs, 8 threads):0x42read back as0x00fromrecvBuf, confirming the race is real and this change is what closes it.claim/setState/release/inUseops against a shared slab understd.Io.Threadedconfirmed no deadlock, no debug-assert trip, and consistent accounting (all 64 slots returned) with the wider critical section.exhausted slab returns null, recovers after releaseunit test, which asserts a re-claimed slot carries a new generation (old handle is dead); the in-lock madvise does not perturb the generation bump.edge-otlpbinary and a high-churn client against it, but a pre-existing panic insrc/frontend/stdio/conn.zig'spipe_bufferedpath (request.iterateHeaders()assertsreader.state == .received_headafter the body reader is already created, atconn.zig:161) aborts connection tasks when forwarding certain POSTs — this is in code the fix does not touch and is out of scope. The local-only/_edge/metricspath serves 200, but exercising the slab-backedrecvBufforwarding path end-to-end through the live binary is blocked by that pre-existing bug. The race is instead reproduced and closed via the direct threaded slab harness above.MADV_DONTNEEDzero-page assertion, but did not measure idleVmRSSreturn on a running binary.MADV.FREE_REUSABLE) variant (not run): this environment is Linux-only; the unit test guards the non-Linux branch by asserting buffer usability rather than zeroing.Automatic Fixes PRs can be configured here.