//! Regression test for issue #7563 - Memory leak when fd closed before AsyncFd drop //! //! This test uses a custom global allocator to track actual memory usage, //! avoiding false positives from RSS measurements which include freed-but-retained memory.
#[tokio::test] asyncfn memory_leak_when_fd_closed_before_drop() { use nix::sys::socket::{self, AddressFamily, SockFlag, SockType}; use std::os::fd::OwnedFd; use std::os::unix::io::{AsRawFd, RawFd}; use std::sync::Arc; use tokio::io::unix::AsyncFd;
// Warm up - let runtime and allocator stabilize for _ in0..100 {
tokio::task::yield_now().await;
}
const ITERATIONS: usize = 1000;
// Phase 1: Warm up allocations for _ in0..ITERATIONS { let (fd_a, _fd_b) = socket::socketpair(
AddressFamily::Unix,
SockType::Stream,
None,
SockFlag::empty(),
)
.unwrap();
let raw_fd = fd_a.as_raw_fd();
set_nonblocking(&fd_a);
std::mem::forget(fd_a);
let wrapper = Arc::new(RawFdWrapper { fd: raw_fd }); let async_fd = AsyncFd::new(ArcFd(wrapper)).unwrap();
// Close fd before dropping AsyncFd - this triggers the bug unsafe {
libc::close(raw_fd);
}
drop(async_fd);
}
// Let things settle
tokio::task::yield_now().await; let baseline = allocated_bytes();
// Phase 2: Run more iterations and check for growth for _ in0..ITERATIONS { let (fd_a, _fd_b) = socket::socketpair(
AddressFamily::Unix,
SockType::Stream,
None,
SockFlag::empty(),
)
.unwrap();
let raw_fd = fd_a.as_raw_fd();
set_nonblocking(&fd_a);
std::mem::forget(fd_a);
let wrapper = Arc::new(RawFdWrapper { fd: raw_fd }); let async_fd = AsyncFd::new(ArcFd(wrapper)).unwrap();
unsafe {
libc::close(raw_fd);
}
drop(async_fd);
}
tokio::task::yield_now().await; let after_phase2 = allocated_bytes();
// Phase 3: Run even more iterations for _ in0..ITERATIONS { let (fd_a, _fd_b) = socket::socketpair(
AddressFamily::Unix,
SockType::Stream,
None,
SockFlag::empty(),
)
.unwrap();
let raw_fd = fd_a.as_raw_fd();
set_nonblocking(&fd_a);
std::mem::forget(fd_a);
let wrapper = Arc::new(RawFdWrapper { fd: raw_fd }); let async_fd = AsyncFd::new(ArcFd(wrapper)).unwrap();
unsafe {
libc::close(raw_fd);
}
drop(async_fd);
}
tokio::task::yield_now().await; let after_phase3 = allocated_bytes();
let growth_phase2 = after_phase2.saturating_sub(baseline); let growth_phase3 = after_phase3.saturating_sub(after_phase2);
// If there's a leak, each phase adds ~250KB (1000 * ~256 bytes per ScheduledIo) // If fixed, memory should stabilize (minimal growth between phases) // Allow 64KB tolerance for normal allocation variance let threshold = 64 * 1024; // 64KB
assert!(
growth_phase2 < threshold || growth_phase3 < threshold, "Memory leak detected: allocations keep growing without stabilizing. \
Phase 1->2: +{growth_phase2} bytes, Phase 2->3: +{growth_phase3} bytes. \
(baseline: {baseline} bytes, phase2: {after_phase2} bytes, phase3: {after_phase3} bytes). \
Expected at least one phase with <{threshold} bytes growth.",
);
}
Die Informationen auf dieser Webseite wurden
nach bestem Wissen sorgfältig zusammengestellt. Es wird jedoch weder Vollständigkeit, noch Richtigkeit,
noch Qualität der bereit gestellten Informationen zugesichert.
Bemerkung:
Die farbliche Syntaxdarstellung und die Messung sind noch experimentell.