--- id: "bug-1442" status: "fixed" status_updated: "2026-09-24" status_note: "" component: "apiserver" severity: "crash" timestamp: "2026-07-15T09:50:00Z" last_seen: "2026-09-19T09:00:00Z" occurrences: 1 files: ["src/frontend/apiserver/backend/wire_dispatch_support.go"] tags: ["cgo", "wire_dispatch", "asan", "double-free", "use-after-free", "job-array-single-wal-record", "test-backend", "go_push_handler", "free_Dataset", "free_Dataset_ptr"] related: ["bug-1441"] legacy: true --- # bug-1442: AddressSanitizer: attempting double-free on 0x...1b0 in thread T0 -- fires immediately on… ## Description AddressSanitizer: attempting double-free on 0x...1b0 in thread T0 -- fires immediately on TestWireDispatch_OnePushProducesNRecordsAndLogsOnce, aborts the whole backend.test binary before it can print PASS/FAIL, so TestWireDispatch_RedeliveryCreatesNoNewRecordsAndLogsNothing never runs at all ## Environment _Not recorded (migrated from buglog.json)._ ## Reproduction _Not recorded (migrated from buglog.json)._ ## Root cause NOT ROOT-CAUSED (build/run/report only, no fix attempted per user instruction). bug-1441 (C.go_push_handler cross-file cgo resolution) is now fixed, and a separate missing `import "apiserver/store"` in wire_dispatch_test.go was fixed as a trivial compile error, so the build now succeeds and the test actually runs -- and immediately hits a real ASan double-free at runtime, not a build failure. Leading hypothesis (unverified): wireDispatchDeliver() (lines 122-180) calls go_push_handler(&decoded, nil) at line 164, whose defer C.free_Request(req) (clib.go) frees *decoded including its Request_body_datamsg payload -- the comment at lines 166-172 asserts free_Dataset() is 'a documented no-op for every table' (citing task 5.3b's residual-gap note) and therefore safe for wireDispatchDeliver to ALSO free ds.rows via free_JobSubmit_ptr() and ds itself at lines 173-177. A 16-byte double-free immediately after go_push_handler returns is consistent with that no-op assumption being wrong for the Data_data_jobArray-derived dataset table specifically (i.e. free_Dataset() actually does free ds->rows or ds itself for this table, and wire_dispatch_support.go's explicit cleanup then frees the same memory again). NOT CONFIRMED -- needs a symbolized ASan backtrace (frames are bare hex addresses in this build) or manual trace of free_Dataset()/free_Request() to confirm which free is the duplicate. ## Fix FIXED and VERIFIED 2026-07-15T09:55:00Z. Root cause confirmed: go_push_handler's internal `defer C.free_Request(req)` (clib.go) calls free_Request(&decoded), whose Request_body_datamsg case calls free_Dataset_ptr(&decoded.body.datamsg) -- free_Dataset_ptr() (controller.capnp.c, generated) unconditionally free()s the dataset_t container pointer it is given, even though free_Dataset() itself is an empty no-op (never touches ->rows). So `ds` becomes a dangling pointer the instant go_push_handler returns -- the earlier NOT-ROOT-CAUSED hypothesis was correct in spirit but had the wrong culprit function (free_Dataset_ptr, not free_Dataset). wireDispatchDeliver() (wire_dispatch_support.go) had two bugs: (1) read ds.rows AFTER calling go_push_handler = use-after-free, (2) separately called C.free(ds) again = double-free on the same 16-byte region ASan caught. Fix: capture `rows := ds.rows` BEFORE calling go_push_handler (line 175), use that captured value afterward instead of re-reading ds.rows, and removed the redundant C.free(unsafe.Pointer(ds)) call entirely since go_push_handler's internal free_Request already frees it. Verified via full ASan CGO rebuild (make test-backend under nix develop ./src with CGO_CFLAGS/CGO_LDFLAGS = -fsanitize=address -g -DWITH_DEBUG=1 + pkg-config librtti/c-capnproto/openssl/tinylog): TestWireDispatch_OnePushProducesNRecordsAndLogsOnce and TestWireDispatch_RedeliveryCreatesNoNewRecordsAndLogsNothing both PASS with zero ASan errors; full ./backend/... suite (91 subtests, convert/handler/heartbeat/peers/wire_dispatch/auth) all PASS, zero regressions; make test (CGO_ENABLED=0) also fully green. ## Verification _Not recorded (migrated from buglog.json)._ ## References - Related: bug-1441