Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 13 additions & 3 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ jobs:
with:
lfs: true

- name: 'Install rust-toolchain.toml'
- name: "Install rust-toolchain.toml"
run: rustup toolchain install
# We use Swatinem/rust-cache to cache cargo registry, index and target in this job
- uses: Swatinem/rust-cache@v2
Expand Down Expand Up @@ -58,16 +58,26 @@ jobs:
- uses: moonrepo/setup-rust@v1
- name: Install dependencies required for libbpf-sys (vendored feature)
run: sudo apt-get update && sudo apt-get install -y autopoint bison flex

- name: Install additional allocators
run: sudo apt-get install -y libmimalloc-dev libjemalloc-dev

- name: Run tests
run: sudo -E $(which cargo) test
env:
RUST_LOG: debug
run: sudo -E $(which cargo) test -- --test-threads 1 --nocapture
working-directory: crates/memtrack

# Since we ran the tests with sudo, the build artifacts will have root ownership
- name: Clean up
run: sudo chown -R $USER:$USER .

benchmarks:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3

- name: 'Install rust-toolchain.toml'
- name: "Install rust-toolchain.toml"
run: rustup toolchain install
- uses: Swatinem/rust-cache@v2
- name: Install cargo codspeed
Expand Down
6 changes: 4 additions & 2 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions crates/memtrack/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ bindgen = "0.71"
tempfile = { workspace = true }
rstest = "0.21"
test-log = "0.2"
insta = { version = "1.46.1", default-features = false }

[package.metadata.dist]
dist = true
Expand Down
14 changes: 12 additions & 2 deletions crates/memtrack/src/allocators/dynamic.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
use std::path::PathBuf;

use crate::{AllocatorKind, AllocatorLib};
use std::path::PathBuf;

/// Returns the glob patterns used to find this allocator's shared libraries.
fn get_allocator_paths(lib: &AllocatorKind) -> &'static [&'static str] {
Expand All @@ -15,6 +14,16 @@ fn get_allocator_paths(lib: &AllocatorKind) -> &'static [&'static str] {
// NixOS: find all glibc versions in the Nix store
"/nix/store/*glibc*/lib/libc.so.6",
],
AllocatorKind::LibCpp => &[
// Standard Linux multiarch paths
"/lib/*-linux-gnu/libstdc++.so*",
"/usr/lib/*-linux-gnu/libstdc++.so*",
// RHEL, Fedora, CentOS, Arch
"/lib*/libstdc++.so*",
"/usr/lib*/libstdc++.so*",
// NixOS: find all gcc lib versions in the Nix store
"/nix/store/*gcc*/lib/libstdc++.so*",
],
AllocatorKind::Jemalloc => &[
// Debian, Ubuntu: Standard Linux multiarch paths
"/lib/*-linux-gnu/libjemalloc.so*",
Expand Down Expand Up @@ -62,6 +71,7 @@ pub fn find_all() -> anyhow::Result<Vec<AllocatorLib>> {
.map(|m| m.is_file())
.unwrap_or(false)
})
.filter(|path| super::is_elf(path))
.collect::<Vec<_>>();

for path in paths {
Expand Down
30 changes: 28 additions & 2 deletions crates/memtrack/src/allocators/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ mod static_linked;
pub enum AllocatorKind {
/// Standard C library (glibc, musl, etc.)
Libc,
/// C++ standard library (libstdc++, libc++) - provides operator new/delete
LibCpp,
/// jemalloc - used by FreeBSD, Firefox, many Rust projects
Jemalloc,
/// mimalloc - Microsoft's allocator
Expand All @@ -26,17 +28,21 @@ pub enum AllocatorKind {
impl AllocatorKind {
/// Returns all supported allocator kinds.
pub fn all() -> &'static [AllocatorKind] {
// IMPORTANT: Check non-default allocators first, because they will contain compatibility
// layers for the default allocators.
&[
AllocatorKind::Libc,
AllocatorKind::Jemalloc,
AllocatorKind::Mimalloc,
AllocatorKind::LibCpp,
AllocatorKind::Libc,
]
}

/// Returns a human-readable name for the allocator.
pub fn name(&self) -> &'static str {
match self {
AllocatorKind::Libc => "libc",
AllocatorKind::LibCpp => "libc++",
AllocatorKind::Jemalloc => "jemalloc",
AllocatorKind::Mimalloc => "mimalloc",
}
Expand All @@ -51,7 +57,8 @@ impl AllocatorKind {
pub fn symbols(&self) -> &'static [&'static str] {
match self {
AllocatorKind::Libc => &["malloc", "free"],
AllocatorKind::Jemalloc => &["_rjem_malloc", "_rjem_free"],
AllocatorKind::LibCpp => &["_Znwm", "_Znam", "_ZdlPv", "_ZdaPv"],
AllocatorKind::Jemalloc => &["_rjem_malloc", "je_malloc", "je_malloc_default"],
AllocatorKind::Mimalloc => &["mi_malloc_aligned", "mi_malloc", "mi_free"],
}
}
Expand All @@ -71,3 +78,22 @@ impl AllocatorLib {
Ok(allocators)
}
}

/// Check if a file is an ELF binary by reading its magic bytes.
fn is_elf(path: &std::path::Path) -> bool {
use std::fs;
use std::io::Read;

let mut file = match fs::File::open(path) {
Ok(f) => f,
Err(_) => return false,
};

let mut magic = [0u8; 4];
if file.read_exact(&mut magic).is_err() {
return false;
}

// ELF magic: 0x7F 'E' 'L' 'F'
magic == [0x7F, b'E', b'L', b'F']
}
29 changes: 11 additions & 18 deletions crates/memtrack/src/allocators/static_linked.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,23 +4,6 @@ use std::path::{Path, PathBuf};

use crate::allocators::{AllocatorKind, AllocatorLib};

/// Check if a file is an ELF binary by reading its magic bytes.
fn is_elf(path: &Path) -> bool {
let mut file = match fs::File::open(path) {
Ok(f) => f,
Err(_) => return false,
};

let mut magic = [0u8; 4];
use std::io::Read;
if file.read_exact(&mut magic).is_err() {
return false;
}

// ELF magic: 0x7F 'E' 'L' 'F'
magic == [0x7F, b'E', b'L', b'F']
}

/// Walk upward from current directory to find build directories.
/// Returns all found build directories in order of preference.
fn find_build_dirs() -> Vec<PathBuf> {
Expand Down Expand Up @@ -61,7 +44,7 @@ fn find_binaries_in_dir(dir: &Path) -> Vec<PathBuf> {
.into_iter()
.flatten()
.filter_map(Result::ok)
.filter(|p| p.is_file() && is_elf(p))
.filter(|p| p.is_file() && super::is_elf(p))
.collect::<Vec<_>>()
}

Expand Down Expand Up @@ -107,3 +90,13 @@ pub fn find_all() -> anyhow::Result<Vec<AllocatorLib>> {

Ok(allocators)
}

impl AllocatorLib {
pub fn from_path_static(path: &Path) -> Result<Self, Box<dyn std::error::Error>> {
let kind = find_statically_linked_allocator(path).ok_or("No allocator found")?;
Ok(Self {
kind,
path: path.to_path_buf(),
})
}
}
51 changes: 50 additions & 1 deletion crates/memtrack/src/ebpf/memtrack.rs
Original file line number Diff line number Diff line change
Expand Up @@ -271,10 +271,31 @@ impl MemtrackBpf {
self.try_attach_realloc(lib_path, "realloc");
self.try_attach_free(lib_path, "free");
self.try_attach_aligned_alloc(lib_path, "aligned_alloc");
self.try_attach_memalign(lib_path, "posix_memalign");
self.try_attach_memalign(lib_path, "memalign");
Ok(())
}

/// Attach C++ operator new/delete probes.
/// These are mangled C++ symbols that wrap the underlying allocator.
/// C++ operators have identical signatures to malloc/free, so we reuse those handlers.
pub fn attach_libcpp_probes(&mut self, lib_path: &Path) -> Result<()> {
self.try_attach_malloc(lib_path, "_Znwm"); // operator new(size_t)
self.try_attach_malloc(lib_path, "_Znam"); // operator new[](size_t)
self.try_attach_malloc(lib_path, "_ZnwmSt11align_val_t"); // operator new(size_t, std::align_val_t)
self.try_attach_malloc(lib_path, "_ZnamSt11align_val_t"); // operator new[](size_t, std::align_val_t)
self.try_attach_free(lib_path, "_ZdlPv"); // operator delete(void*)
self.try_attach_free(lib_path, "_ZdaPv"); // operator delete[](void*)
self.try_attach_free(lib_path, "_ZdlPvm"); // operator delete(void*, size_t) - C++14 sized delete
self.try_attach_free(lib_path, "_ZdaPvm"); // operator delete[](void*, size_t) - C++14 sized delete
self.try_attach_free(lib_path, "_ZdlPvSt11align_val_t"); // operator delete(void*, std::align_val_t)
self.try_attach_free(lib_path, "_ZdaPvSt11align_val_t"); // operator delete[](void*, std::align_val_t)
self.try_attach_free(lib_path, "_ZdlPvmSt11align_val_t"); // operator delete(void*, size_t, std::align_val_t)
self.try_attach_free(lib_path, "_ZdaPvmSt11align_val_t"); // operator delete[](void*, size_t, std::align_val_t)

Ok(())
}

/// Attach probes for a specific allocator kind.
/// This attaches both standard probes (if the allocator exports them) and
/// allocator-specific prefixed probes.
Expand All @@ -290,14 +311,22 @@ impl MemtrackBpf {
// Libc only has standard probes, and they must succeed
self.attach_libc_probes(lib_path)
}
AllocatorKind::LibCpp => {
// libc++ exports C++ operator new/delete symbols
self.attach_libcpp_probes(lib_path)
}
AllocatorKind::Jemalloc => {
// Try standard names (jemalloc may export these as drop-in replacements)
let _ = self.attach_libc_probes(lib_path);
// Try C++ operators (jemalloc exports these for C++ programs)
let _ = self.attach_libcpp_probes(lib_path);
self.attach_jemalloc_probes(lib_path)
}
AllocatorKind::Mimalloc => {
// Try standard names (mimalloc may export these as drop-in replacements)
let _ = self.attach_libc_probes(lib_path);
// Try C++ operators (mimalloc exports these for C++ programs)
let _ = self.attach_libcpp_probes(lib_path);
self.attach_mimalloc_probes(lib_path)
}
}
Expand All @@ -311,7 +340,27 @@ impl MemtrackBpf {
// - rust_dealloc: _rjem_sdallocx
// - rust_realloc: _rjem_realloc / _rjem_rallocx

// Prefixed standard API
// je_*_default API (C++ with static linking)
self.try_attach_malloc(lib_path, "je_malloc_default");
self.try_attach_malloc(lib_path, "je_mallocx_default");
self.try_attach_free(lib_path, "je_free_default");
self.try_attach_free(lib_path, "je_sdallocx_default");
self.try_attach_realloc(lib_path, "je_realloc_default");
self.try_attach_realloc(lib_path, "je_rallocx_default");
self.try_attach_calloc(lib_path, "je_calloc_default");

// je_* API (internal jemalloc functions, static linking)
self.try_attach_malloc(lib_path, "je_malloc");
self.try_attach_malloc(lib_path, "je_mallocx");
self.try_attach_calloc(lib_path, "je_calloc");
self.try_attach_realloc(lib_path, "je_realloc");
self.try_attach_realloc(lib_path, "je_rallocx");
self.try_attach_aligned_alloc(lib_path, "je_aligned_alloc");
self.try_attach_memalign(lib_path, "je_memalign");
self.try_attach_free(lib_path, "je_free");
self.try_attach_free(lib_path, "je_sdallocx");

// _rjem_* API (Rust jemalloc crate, dynamic linking)
self.try_attach_malloc(lib_path, "_rjem_malloc");
self.try_attach_malloc(lib_path, "_rjem_mallocx"); // Also used for `calloc`
self.try_attach_calloc(lib_path, "_rjem_calloc");
Expand Down
28 changes: 21 additions & 7 deletions crates/memtrack/src/ebpf/tracker.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,22 +18,36 @@ impl Tracker {
/// - Attach uprobes to all libc instances
/// - Attach tracepoints for fork tracking
pub fn new() -> Result<Self> {
let mut instance = Self::new_without_allocators()?;

let allocators = AllocatorLib::find_all()?;
debug!("Found {} allocator instance(s)", allocators.len());
instance.attach_allocators(&allocators)?;

Ok(instance)
}

pub fn new_without_allocators() -> Result<Self> {
// Bump memlock limits
Self::bump_memlock_rlimit()?;

let mut bpf = MemtrackBpf::new()?;
bpf.attach_tracepoints()?;

// Find and attach to all allocators
let allocators = AllocatorLib::find_all()?;
debug!("Found {} allocator instance(s)", allocators.len());
Ok(Self { bpf })
}

for allocator in &allocators {
debug!("Attaching uprobes to: {}", allocator.path.display());
bpf.attach_allocator_probes(allocator.kind, &allocator.path)?;
pub fn attach_allocators(&mut self, libs: &[AllocatorLib]) -> Result<()> {
for allocator in libs {
self.bpf
.attach_allocator_probes(allocator.kind, &allocator.path)?;
}

Ok(Self { bpf })
Ok(())
}

pub fn attach_allocator(&mut self, lib: &AllocatorLib) -> Result<()> {
self.bpf.attach_allocator_probes(lib.kind, &lib.path)
}

/// Start tracking allocations for a specific PID
Expand Down
1 change: 1 addition & 0 deletions crates/memtrack/testdata/alloc_cpp/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
build/
Loading
Loading