Feb 22, 2026·6 min read·3 visits
The Rust `mnl` crate (Netlink bindings) exposed a memory safety bug because it didn't validate input before passing it to a buggy C library (`libmnl`). By crafting a specific Netlink message with a 'negative' length (integer overflow), an attacker can crash the application (DoS) via a Segmentation Fault.
A classic C-style signedness error in the underlying `libmnl` library allows attackers to crash Rust applications using the `mnl` crate. Despite Rust's safety guarantees, the `mnl` wrapper blindly trusted a C function that incorrectly casts unsigned 32-bit integers to signed integers, leading to out-of-bounds reads and segmentation faults.
We all love Rust. It's the memory-safe superhero that promised to save us from the sins of the 90s—buffer overflows, use-after-frees, and dangling pointers. But every superhero has a weakness, and for Rust, that weakness is often the unsafe block, or worse: Foreign Function Interfaces (FFI) that claim to be safe but rely on C libraries that are very much not.
Enter mnl, a Rust crate providing high-level abstractions for libmnl, a minimalistic user-space library for Netlink developers. Netlink is the Linux kernel's preferred way to talk to user space (and vice versa). It's powerful, complex, and historically full of parsing bugs.
The mnl crate exposes functions like cb_run, which processes a buffer of Netlink messages. It marked these functions as Safe Rust (fn cb_run(...)). That signature is a contract: "I promise you cannot crash memory by calling this, no matter what garbage data you feed me." spoiler alert: The contract was a lie.
The root cause isn't actually in the Rust code itself—initially. It lies deep within the bowels of the C library libmnl. Specifically, a function called mnl_nlmsg_ok, which is supposed to validate that a Netlink message header is sane.
Here is the comedy of errors:
nlmsg_len) is a uint32_t (unsigned 32-bit integer).libmnl (versions <= 1.0.5) decides to cast this length to a signed int during validation checks.0xFFFFFFF5 (which is 4,294,967,285 as an unsigned int), the C compiler goes "Ah, yes, -11."Because the length is interpreted as negative, it bypasses the standard "is the message bigger than the remaining buffer?" check. A negative number is technically smaller than the positive buffer size, so the check passes. The library then proceeds to read memory it shouldn't, assuming the message is valid. The Rust wrapper simply forwarded the raw byte slice to this C function without checking it first, acting like a bouncer who lets a guy with a bomb in because "he looked confident."
Let's look at what changed. The vulnerability was patched not by fixing the C library (though that should happen too), but by implementing a "Trust but Verify" approach in the Rust wrapper.
Before the fix, cb_run was a reckless conduit:
// OLD VULNERABLE CODE
pub fn cb_run(buf: &[u8], seq: u32, portid: u32) -> io::Result<CbResult> {
// Yolo. Pass the pointer directly to C.
let ret = unsafe {
mnl_sys::mnl_cb_run(
buf.as_ptr() as *const _,
buf.len(),
seq,
portid,
Some(cb_handler),
// ...
)
};
// ...
}The fix introduces a validation step. It forces the data to be parsed by Rust's own iterators (NlMessages) before the C library ever sees it. If Rust's iterator chokes on the malformed length, the function returns an error instead of crashing.
// NEW PATCHED CODE
fn validate_messages(buffer: &[u8]) -> io::Result<()> {
// Iterate through the buffer using Rust's safe parsing logic first
for msg in NlMessages::new(buffer) {
msg?; // This will error if the header length is garbage
}
Ok(())
}
pub fn cb_run(buf: &[u8], seq: u32, portid: u32) -> io::Result<CbResult> {
// "I don't trust you, C library."
validate_messages(buf)?;
// Now it's safe(r) to pass to C
let ret = unsafe { ... };
}You don't need a complex heap grooming strategy to trigger this. You just need 28 bytes. The PoC released with the advisory is brutally simple. It constructs a byte array that mimics a Netlink message header but inserts a massive value where the length should be.
Here is the layout of the attack buffer:
let data = [
0, 0, 0, 0, 0, 0, 0, 0, // Padding / ignored
0, 0, 0, 0, 0, 0, // More filler
245, 255, 255, 255, // <--- THE PAYLOAD: 0xFFFFFFF5
0, 0, 255, 255, // More garbage
5, 224, 0, 0, 0, 0 // Trailing bytes
];
// This line crashes the program
mnl::cb_run(&data, 0, 0);When libmnl reads 0xFFFFFFF5, it treats it as a negative integer. It assumes the message is valid but weirdly sized, and eventually tries to access memory based on that offset or iterates past the bounds of data. AddressSanitizer (ASan) lights up like a Christmas tree with a SEGV on unknown address.
While this is "only" a crash (Denial of Service) in most contexts, memory corruption is a sliding scale. If an attacker can control the precise layout of memory around the buffer, a "read" access violation could potentially be massaged into an information leak or worse, though the DoS is the guaranteed outcome.
The immediate impact is application stability. Any Rust application listening for Netlink messages (e.g., network managers, VPN clients, system monitors) using this crate can be crashed by a local user or potentially a remote attacker if the Netlink messages are bridged or processed from untrusted sources.
But the philosophical impact is higher. This vulnerability is a Soundness Hole. In Rust, safe functions must uphold memory safety invariants under all inputs. cb_run failed this test. It allowed a safe function to trigger undefined behavior (UB) in the C layer.
This serves as a grim reminder: If you wrap a C library, you inherit its CVEs. You cannot simply wrap unsafe C calls in a safe Rust function and call it a day without validating that the inputs respect the C library's hidden assumptions (like "please don't give me negative lengths").
The fix is straightforward for consumers: update mnl to version 0.3.1. This version includes the validate_messages logic that sanitizes inputs before they reach the C library.
There is a catch, though. The fix enforces stricter alignment. The input buffer must now be properly aligned to size_of::<nlmsghdr>(). If you were previously passing unaligned slices (which C might have tolerated or handled with undefined behavior), the new Rust code will return an error.
cargo update -p mnlLong term? Check your system libraries. The root cause is in libmnl. If you are running an ancient Linux distro, you might still have the buggy C library installed. Even with the Rust fix, it's good hygiene to update libmnl to a version newer than 1.0.5 (patched circa Nov 2023).
CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:N/I:N/A:H| Product | Affected Versions | Fixed Version |
|---|---|---|
mnl mullvad | < 0.3.1 | 0.3.1 |
| Attribute | Detail |
|---|---|
| CWE ID | CWE-196 |
| Attack Vector | Local (typically) |
| CVSS | 6.5 (Medium) |
| Impact | Denial of Service (Crash) |
| Exploit Status | PoC Available |
| Library | libmnl (C) / mnl (Rust) |