| 34 | } // namespace |
| 35 | |
| 36 | TEST(PayloadAnchorKeepalive, ReleaseRunsWhileDsoStillMapped) { |
| 37 | bool dso_mapped = true; |
| 38 | // The plugin DSO token: when its last copy drops, the ".so" unloads. |
| 39 | auto library_keepalive = |
| 40 | std::shared_ptr<void>(reinterpret_cast<void*>(0x1), [&dso_mapped](void*) { dso_mapped = false; }); |
| 41 | |
| 42 | AnchorProbe probe{&dso_mapped}; |
| 43 | PJ_payload_anchor_t anchor{}; |
| 44 | anchor.ctx = &probe; |
| 45 | anchor.release = &probeRelease; |
| 46 | |
| 47 | PJ::sdk::BufferAnchor wrapped = PJ::detail::wrapPayloadAnchor(anchor, library_keepalive); |
| 48 | |
| 49 | // The extension catalog tears down and drops its DSO token. The wrapped anchor |
| 50 | // must hold its OWN copy, so the ".so" stays mapped. |
| 51 | library_keepalive.reset(); |
| 52 | EXPECT_TRUE(dso_mapped) << "wrapped anchor must keep the producing plugin DSO token alive"; |
| 53 | EXPECT_FALSE(probe.release_ran); |
| 54 | |
| 55 | // Destroying the anchor runs release — and it must run BEFORE the DSO unloads. |
| 56 | wrapped.reset(); |
| 57 | EXPECT_TRUE(probe.release_ran); |
| 58 | EXPECT_FALSE(probe.released_after_unload) << "plugin release fn was called after the DSO unloaded (UAF)"; |
| 59 | EXPECT_FALSE(dso_mapped) << "the captured keepalive drops once the anchor is destroyed"; |
| 60 | } |
| 61 | |
| 62 | TEST(PayloadAnchorKeepalive, NullReleaseYieldsEmptyAnchor) { |
| 63 | PJ_payload_anchor_t anchor{}; // release == nullptr → no ownership to track. |
nothing calls this directly
no test coverage detected