
Cloud Vulnerability DB
A community-led vulnerabilities database
rustix 1.1.5's generic Buffer-based xattr getters can treat a successful Linux zero-size xattr query as if the returned byte count represented initialized output bytes. On Linux, getxattr/lgetxattr/fgetxattr with a buffer size of zero are documented to return the required attribute size without writing to the buffer. listxattr/llistxattr/flistxattr similarly support zero-size size queries. The Rustix wrappers pass every successful returned count directly to Buffer::assume_init. For a zero-extent buffer and a positive query result, that count does not represent initialized elements. This can produce a panic for MaybeUninit buffers and, more importantly, can cause undefined behavior through the entirely safe spare_capacity(&mut Vec<T>) API by calling Vec::set_len beyond the vector's capacity and treating uninitialized elements as initialized. I found this in rustix 1.1.5. The same pattern also appears to be present on current main. This report was identified and drafted with AI assistance during a dependency source review; I reviewed the reasoning before submitting it.
In rustix 1.1.5, tag v1.1.5 / commit 287214b889865d8e1406a0ee71cc409b6f6191c8, src/fs/xattr.rs contains the following pattern for getxattr:
let len = unsafe { backend::fs::syscalls::getxattr(path, name, value.parts_mut())? };
unsafe { Ok(value.assume_init(len)) }
The same pattern occurs in:
getxattr
lgetxattr
fgetxattr
listxattr
llistxattr
flistxattr
The safety contract of Buffer::assume_init(len) in src/buffer.rs says:
At least len elements of the buffer must now be initialized.
However, Linux deliberately has a successful case where this is false.
For the getxattr family, if the supplied size is zero, Linux returns the size of the attribute and leaves the output buffer unchanged. Thus an attribute containing three bytes can cause a successful return value of 3 even though zero bytes of the buffer were initialized.
The listxattr family has the analogous zero-size size-query behavior.
SpareCapacity consequence
For SpareCapacity, parts_mut exposes the vector's current spare capacity:
let spare = self.0.spare_capacity_mut();
(spare.as_mut_ptr().cast(), spare.len())
and assume_init does:
self.0.set_len(self.0.len() + len);
Consider a Vec<u8> with nonzero capacity but len capacity. It has zero spare capacity.
spare_capacity(&mut vec) is a safe API. Its debug assertion checks that v.capacity() != 0; it does not check that v.len() < v.capacity(). Therefore a nonzero-capacity full vector passes this assertion.
Passing this SpareCapacity to getxattr supplies a zero-size buffer. If the named xattr contains three bytes, Linux legitimately returns 3 without initializing the buffer.
Rustix then invokes:
set_len(old_len + 3)
even though old_len capacity, violating Vec::set_len's safety requirements: the resulting length exceeds capacity and the added elements have not been initialized.
This is reachable using only safe Rust APIs.
Although the spare_capacity documentation says the vector "should have some non-empty spare capacity", the API is safe, so violating that recommendation cannot soundly permit undefined behavior.
MaybeUninit consequence
For &mut [MaybeUninit<T>], assume_init(len) performs:
let (init, uninit) = self.split_at_mut(len);
A zero-length slice used for an xattr size query can therefore receive a positive returned count and panic because len > self.len().
That provides a simple non-UB demonstration of the underlying incorrect assumption: a successful positive xattr return count does not necessarily mean that many buffer elements were initialized.
This should be run on Linux on a filesystem supporting user.* extended attributes. Cargo.toml: [package] name = "rustix-xattr-buffer-poc" version = "0.0.0" edition = "2021" [dependencies] rustix = { version = "=1.1.5", features = ["fs"] } A non-UB panic witness: use rustix::fs::{getxattr, setxattr, XattrFlags}; use std::mem::MaybeUninit; fn main() -> rustix::io::Result<()> { let mut path = std::env::temp_dir(); path.push(format!("rustix-xattr-buffer-poc-{}", std::process::id())); std::fs::write(&path, b"x").unwrap(); let name = "user.rustix_xattr_buffer_poc"; setxattr(&path, name, &[0x11, 0x22, 0x33], XattrFlags::empty())?; // Linux interprets the zero-size buffer as a size query and returns 3 // without initializing output bytes. Rustix passes 3 to assume_init, // which attempts split_at_mut(3) on this zero-length slice. let mut empty: [MaybeUninit<u8>; 0] = []; let _ = getxattr(&path, name, &mut empty[..])?; Ok(()) } The corresponding safe-Rust UB path is: use rustix::buffer::spare_capacity; // Nonzero capacity, but no spare capacity. let mut buf = Vec::with_capacity(8); let capacity = buf.capacity(); buf.resize(capacity, 0); let _ = getxattr(&path, name, spare_capacity(&mut buf))?; parts_mut() reports an extent of zero, so Linux performs a size query and returns 3 without writing output. SpareCapacity::assume_init(3) then calls set_len(capacity + 3). I have not intentionally executed the UB case; the source-level violation of Vec::set_len's unsafe preconditions appears sufficient to demonstrate the soundness issue.
This appears to be a soundness bug because entirely safe Rust can cause Rustix's internal unsafe code to violate Vec's length/capacity and initialization invariants. At minimum: an empty MaybeUninit buffer can cause an unexpected panic during a valid xattr size query; a full, nonzero-capacity Vec passed through safe spare_capacity can cause Rustix to set the vector length beyond capacity and mark nonexistent/uninitialized elements as initialized; subsequent safe operations on the corrupted vector can therefore operate on invalid memory. The problematic syscall behavior is normal documented Linux behavior; it does not require a faulty or malicious kernel. I have not demonstrated practical exploitation, so I leave final severity assessment to the maintainers. The same vulnerable pattern is present in rustix 1.0.0, so released versions 1.0.0 through 1.1.5 are affected. I also observed the pattern on current main.. The older 0.38.x xattr API returned native counts directly and did not perform this Buffer::assume_init conversion. Possible direction for a fix Before calling Buffer::assume_init(n), the xattr wrappers need to distinguish bytes actually written from a successful zero-extent size-query result. A simple bounds check preventing assume_init(n) whenever n exceeds the supplied extent would prevent the memory-safety problem, but it would also need an API/semantics decision for legitimate xattr size queries, because some Buffer output types can naturally represent a returned count while others represent initialized memory. I therefore have not assumed that a one-line bounds check is the ideal upstream API fix.
Source: NVD
Free Vulnerability Assessment
Evaluate your cloud security practices across 9 security domains to benchmark your risk level and identify gaps in your defenses.
Get a personalized demo
"Best User Experience I have ever seen, provides full visibility to cloud workloads."
"Wiz provides a single pane of glass to see what is going on in our cloud environments."
"We know that if Wiz identifies something as critical, it actually is."