ci: add code coverage, add core crate tests, and fix mem-ring fd/heap safety issues - #173
Conversation
Add a coverage job to CI: - Collect Rust coverage with cargo-llvm-cov (lcov format) - Run Go tests in test/go with coverage (previously never run in CI) - Upload both reports to Codecov - Disable sccache for this job since it does not work well with coverage instrumentation
rust2go-convert/src/convert.rs: tests for MemType, ToRef/FromRef roundtrips of primitives/String/Vec/Option/tuples, CopyStruct and Writer. rust2go/src/slot.rs: tests for atomic slot read/write, attachments, waker wakeup, ptr roundtrip and various drop orders. rust2go/src/future.rs: tests for ResponseFuture poll lifecycle (init -> executed -> ready/fused) and drop behavior. mem-ring/src/queue.rs: sync tests for Queue push/pop/wrap-around and new_from_meta, WakerSlot transitions; async tests for pending tasks, notify_manually and push_with_awaiter. mem-ring/src/eventfd.rs: tests for socketpair notify and dup.
…borrow clippy warning
The monoio version of Awaiter::wait read into a thread_local buffer via a raw pointer (RawBuf). The spawned unstuck_handler task can leak at runtime drop (detached JoinHandle + waker/Op Rc cycle), leaving an in-flight kernel Recv holding a dangling pointer to freed TLS memory. A later notify() then completes the read and the kernel writes into reused heap, corrupting the allocator (observed as 'tcache_thread_shutdown(): unaligned tcache chunk detected' on Linux). Pass an owned Vec to the read op so the buffer lives as long as the op itself. Also drop the temporary CI diagnostic steps and restore the plain test/coverage commands.
…eardown heap corruption
Queue::read and run_handler used to hand the queue's own fd to the peer-side Notifier/Awaiter while Queue::drop also closes it, so one fd was closed twice. With parallel tests the fd number can be reused in between by an unrelated owner, which then gets its descriptor closed from under it. ci: TEMP loop mem-ring tests to measure crash rate
The Go tests constructed an empty Demo (nil user maps), so every state-dependent case failed with 'user not exist'. They were never exercised in CI before. Use the global TestCallImpl initialized in init() instead.
Welcome to Codecov 🎉Once you merge this PR into your default branch, you're all set! Codecov will compare coverage reports and display results in all future pull requests. Thanks for integrating Codecov - We've got you covered ☂️ |
ihciah
left a comment
There was a problem hiding this comment.
The overall direction looks good. In particular, the fix for the Awaiter::wait buffer lifetime and the additional core tests are valuable. However, there are still several fd ownership issues that should be addressed before merging.
Must fix
- queue.rs:532-533 should not fall back to the original fd when
dup()fails.
If the process hits EMFILE/ENFILE, dup() returns an error. The fallback then gives the same fd to both Notifier and Queue, so the fd is double-closed during destruction. If the descriptor number has been reused, this may close an unrelated fd.
The same pattern appears at queue.rs:559-561, queue.rs:593-595, and queue.rs:620-622.
Please propagate the dup() error or implement an explicit ownership transfer instead of falling back to the original fd.
Queueinstances created bynew_from_meta()leak fds.
new_from_meta() sets do_drop to false, and Queue::drop therefore does not close the original fds. After this change, read()/write() only close the duplicated fds, leaving the original peer fds open indefinitely.
This leaks fds every time new_from_meta().read() or .write() is used and can eventually exhaust the process fd limit. Memory ownership and fd ownership should be tracked separately.
Suggestion
- dup() drops
FD_CLOEXEC.
new_pair() creates sockets with SOCK_CLOEXEC on Linux, but a regular dup() creates a descriptor without the close-on-exec flag. These internal fds may therefore be inherited by child processes.
Please use F_DUPFD_CLOEXEC or another duplication method that preserves close-on-exec semantics.
One additional note: --all-features still only exercises the monoio branch, so the tokio branch is not covered by the new CI tests. This appears to be an existing CI gap, but it would be good to add a separate tokio-feature test.
Local validation passed for the workspace tests, mem-ring tests, Clippy, rustfmt, and Go tests.
mem-ring/src/queue.rs: replace dup-and-fallback in read()/write()/ run_handler() with explicit fd ownership transfer: the fd is moved to the Notifier/Awaiter and marked as -1 in the queue, so it is closed exactly once. Queue::drop now closes fds it still owns regardless of do_drop, fixing the fd leak of queues created by new_from_meta(); do_drop only controls shared memory freeing. Add tests verifying both owner and from-meta queues close their fds on drop (checked via POLLHUP on the peer end to stay robust against fd number reuse in parallel tests). mem-ring/src/eventfd.rs: remove the now-unused dup() helper and its test; no descriptor duplication remains, so the FD_CLOEXEC loss on dup() is gone as well. mem-ring/Cargo.toml: enable tokio "io-util" feature; the tokio branch of mem-ring did not compile standalone without it (AsyncReadExt). .github/workflows/ci.yml: add a mem-ring test run with --no-default-features --features tokio; --all-features only exercises the monoio branch since the tokio code is cfg'd out when monoio is on. Addresses review feedback on #173.
|
Thanks for the careful review! All points are addressed in 8433bd2. 1.
|
|
Re-review result: the main issues from the previous review appear to be fixed:
One remaining concern: P2: Queue::read() and Queue::write() set transferred FDs to As a result:
If these methods are only intended to be called before Other than this API concern, the changes look good. |
mem-ring/src/queue.rs: document on ReadQueue::meta() and WriteQueue::meta() that only the memory fields of the returned QueueMeta are meaningful: fds whose ownership was transferred to the internal Notifier/Awaiter by read()/write() are reported as -1, and a shareable meta for a peer should be obtained from Queue::new. Returning the live fds instead would let a new_from_meta() peer close descriptors owned by these queues. Extend drop_after_read_write_transfers to assert the documented behavior. Addresses the remaining P2 review comment on #173.
|
Thanks for the re-review! The P2 concern is addressed in 63bd3fa by documenting the restriction on both methods:
I chose documenting over returning the live fds deliberately: these two methods have no callers in the repo, and a The mem-ring tests pass under both monoio (default + tpc) and tokio feature sets; clippy |
ihciah
left a comment
There was a problem hiding this comment.
LGTM! Thanks @lirenjie95 ! It's a very good work.
Summary
This PR adds code coverage collection to CI, significantly expands the test suite of core crates, and fixes two real memory/fd safety issues in mem-ring that were exposed by the new tests.
Current coverage (Codecov, this branch): 82.6% lines (2873 / 3478, 605 missed) across 18 files. Branch coverage is not tracked yet (cargo-llvm-cov lcov without
--branch). Previously CI had no coverage measurement at all.Changes
CI: coverage collection (
.github/workflows/ci.yml)coveragejob: collects Rust coverage withcargo-llvm-cov(lcov) and Go coverage fortest/go(Go tests were never run in CI before), and uploads both to Codecov.-p mem-ring) — see the note below.New tests (+~650 lines)
rust2go-convert/src/convert.rs:MemType,ToRef/FromRefroundtrips for primitives/String/Vec/Option/nested Vec/tuples,CopyStructbuffer layout,Writer.rust2go/src/slot.rs: atomic slot read/write, attachments, waker wakeup, ptr roundtrip, drop orders.rust2go/src/future.rs:ResponseFuturepoll lifecycle (sync/async completion,new_without_req, poll-after-ready panic, drop paths).mem-ring/src/queue.rs: sync tests for push/pop/wrap-around/new_from_meta/WakerSlot; async tests for pending tasks,notify_manually,push_with_awaiter.mem-ring/src/eventfd.rs: socketpair notify,dup.test/go/impl_test.go: fixed to use the initializedTestCallImpl— these tests were previously broken (constructed an emptyDemo, so all state-dependent cases failed) and never ran in CI.mem-ring fixes
1. Use-after-free in
Awaiter::wait(mem-ring/src/eventfd.rs)The monoio version read into a thread_local buffer via a raw pointer (
RawBuf). A spawnedunstuck_handlertask can leak at runtime drop (detached JoinHandle + waker/Op Rc cycle), leaving an in-flight kernelRecvholding a dangling pointer to freed TLS memory. A laternotify()completes that read and the kernel writes into reused heap. Now an ownedVecis passed to the read op so the buffer lives as long as the op itself.2. fd double close (
mem-ring/src/queue.rs)Queue::read,run_handlerandQueue::writehanded the queue's own fd to the peer-sideNotifier/Awaiter, whileQueue::dropalso closes it — one fd was closed twice. With parallel tests the fd number can be reused in between by an unrelated owner (another test thread's socketpair / epoll / uring fd), which then gets its descriptor closed from under it. The fds are nowdup()'ed so each owner closes an independent descriptor.Testing
cargo test --all-features --all-targets,cargo clippy --all-features --all-targets -- --deny warnings,cargo fmt --check— all green (also verified locally on macOS + go1.18).Note for reviewers (the heap corruption saga) — @ihciah
The new mem-ring tests initially crashed CI almost 100% with
tcache_thread_shutdown(): unaligned tcache chunk detected(glibc heap metadata corruption) — only on Linux, only in the full-suite binary. Investigation (valgrind, ASan, gdb, single-threaded, test-elimination bisect — rr was unavailable on the runner) showed:-p mem-ringbinary (15/15 loop passes, both drivers).cargo test --all-features --all-targets) still crashes, while the-p mem-ringbuild of the same tests never does. The discriminant is monoio'ssyncfeature: workspace feature unification pullsmonoio/sync(required by thetestcrate) into mem-ring's test binary. With sync enabled, the combination of monoio 0.2.4's sync machinery and mem-ring's "spawned task that only exits with the runtime" pattern still corrupts the heap on test teardown (both io_uring and legacy drivers). mem-ring itself does not usemonoio/sync, so CI now tests mem-ring with the feature set declared by its ownCargo.toml.Related Issue
N/A — CI/test infrastructure improvement; no tracking issue.