Tracking trust with Rust in the kernel
The Linux kernel has to handle many different sources of data that should not be trusted: user space, network connections, and removable storage, to name a few. The kernel has to remain secure even if one of these sends garbled (or malicious) data. Benno Lossin has been working on an API for kernel Rust code that makes it harder to accidentally make decisions based on data from user space. That work is now on its fourth revision, and Lossin has asked kernel developers to experiment with it and see where problems remain, making this a good time to look at the proposed API.
The core approach, as with so many things in Rust, centers on the type system. Lossin's patch set introduces a new type, Untrusted, that marks data as originating from an untrusted source, and therefore requiring special caution. Trying to access a value wrapped by Untrusted is forbidden by Rust. The type is a "transparent" structure, meaning that it will be laid out in memory exactly like the type that it wraps. An Untrusted<u8> is a single byte, for example. The type therefore has no run-time overhead, so it can be used as a kind of marker in the type system for data that has come from user space without being validated. That makes it impossible to pass into functions that expect normal kernel data by accident.
The bulk of Lossin's patch set is documentation for Untrusted, and some utility functions to manipulate untrusted values. There is also special support for common data structures, specifically slices (arrays with a run-time-known length) and vectors (growable arrays on the heap) of untrusted values. When dealing with an existing buffer that should be filled with untrusted data, the documentation recommends writing the interface like this:
pub fn read_from_userspace(buf: &mut [Untrusted<u8>])
That function takes a mutable reference to a slice of untrusted bytes, and will fill it with user-space data. That data can later be copied back to user space without having to unwrap it. Trying to actually use the value of an untrusted object within the kernel, however, will cause a compiler error. Lossin recommends that form of API because converting an &mut Untrusted<[u8]> (a mutable reference to an untrusted slice of bytes) into an &mut [Untrusted<u8>] (a mutable reference to a slice of individually untrusted bytes) can be done automatically — conversions are inserted where needed by the compiler due to the DerefMut implementation for Untrusted — but converting things the other way around requires an explicit function call. If Rust developers get in the habit of writing APIs involving Untrusted in this way, they'll be less of a hassle to use.
Sometimes, the kernel does need to read user-space data, not merely copy it around. The third patch of the set contains functions to help with that. It introduces a new trait called Validate that encapsulates the logic for validating user-space data before use. For a custom type T, an implementation of Validate<S> contains the logic needed to turn an Untrusted<S> into a plain T. Lossin isn't totally happy with that API, though, and wants to find time to improve it.
Greg Kroah-Hartman, who has been enthusiastic about the idea of marking input from user space in the past, asked for Lossin to add an example of a driver using Untrusted.
ioctl() is the callback that is taking untrusted data from userspace. That's one place we have had more kernel buffer overflows then I can count and ALWAYS needs to be properly verified before anything can be done with the data there.
Lossin hasn't written a complete example driver, but did share a rough sketch of what using the new API to implement a driver ioctl() function might look like. ioctl() is a tricky interface because it takes two parameters: cmd, which specifies which command to run, and arg, the meaning of which depends on cmd. How arg should be validated depends, therefore, on which command user space has sent. Lossin suggested representing this in Rust with an enumeration:
enum MyIoctlArgs { WriteFoo(UserPtr<Foo>), VerifyBar(UserPtr<Bar>), // ... }
With some suitable tweaks to the Rust driver API, the generic MiscDevice::ioctl() function could take a structure that bundles cmd and arg together:
pub struct IoctlArgs { pub cmd: u32, pub arg: usize, } fn ioctl( _device: <Self::Ptr as ForeignOwnable>::Borrowed<'_>, _file: &File, _args: Untrusted<IoctlArgs>, ) -> Result<isize>
The Untrusted<IoctlArgs> could then be validated into a normal MyIoctlArgs value. This would not prevent the programmer from botching the validation logic, of course, but it would force the conversion to happen in one specific place, and enforce that it is called. Hopefully, that makes it easier to spot missing checks.
Kroah-Hartman raised the problem of bugs where the kernel "validates" a particular piece of data while it is still accessible to user space, and only then copies it with copy_from_user(). He wondered how this interface could protect against that. The UserPtr types in MyIoctlArgs in his example help there, Lossin explained. These represent pointers to user-space memory; when combined with a length, they can become a UserSlice. The UserSlice::read_all() method being the Rust equivalent of copy_from_user(). Right now, read_all() just reads bytes directly, but if his patch set is accepted, he would want to change it to mark the read bytes as untrusted:
pub fn read_all<A: Allocator>( self, buf: &mut Untrusted<Vec<u8, A>>, flags: Flags ) -> Result
With the types Lossin sketched out, the type system would theoretically enforce the progression from Untrusted<IoctlArgs>, to MyIoctlArgs, to UserPtr, to Untrusted<Vec<u8>>, to whatever user-defined type is appropriate for the argument of ioctl(). Since he did not provide a full, working example, there are still some questions his response leaves unanswered, but Kroah-Hartman was happy with it. At the time of writing no other comments on the patch set have been made. If the API is adopted in its current form, it will necessitate pervasive changes to the interfaces for Rust drivers, so the patch set seems likely to be a topic of discussion at the upcoming Kangrejos conference about the Rust for Linux project in September.
Posted Sep 3, 2025 19:53 UTC (Wed)
by Sesse (subscriber, #53779)
[Link]
Posted Sep 3, 2025 22:23 UTC (Wed)
by iabervon (subscriber, #722)
[Link] (2 responses)
I think it might be wise for UserPtr to lack the Copy trait, however, which (I think) would mean that the example bug wouldn't compile without using a clone() method call that shows where the data you check diverges from the data you use, while not affecting any code that only sets up the user/kernel data transfer once.
Posted Sep 4, 2025 15:57 UTC (Thu)
by farnz (subscriber, #17727)
[Link] (1 responses)
That would prevent the TOCTOU bug, since UserSliceReader::read_all would work in terms of Untrusted<KVec<u8>> instead of KVec<u8>, and thus to return KVec<u8> as per the method signature in the TOCTOU bug example, you'd have to call validate and get back a validated buffer.
Posted Sep 4, 2025 17:12 UTC (Thu)
by iabervon (subscriber, #722)
[Link]
Posted Sep 4, 2025 6:57 UTC (Thu)
by Wol (subscriber, #4433)
[Link] (1 responses)
Cheers,
Posted Sep 6, 2025 8:04 UTC (Sat)
by lossin (subscriber, #177724)
[Link]
Posted Sep 4, 2025 9:53 UTC (Thu)
by taladar (subscriber, #68407)
[Link] (10 responses)
I would assume similar concerns could be present in the kernel where e.g. an integer needs to be validated differently if it is used as, say, a month number in a date, than if it was used as a plausible size for a TCP packet.
Posted Sep 4, 2025 17:51 UTC (Thu)
by epa (subscriber, #39769)
[Link] (2 responses)
Getting back to the kernel and Rust, I would love it if more languages adopted bounded integers as provided by Ada. You can declare your month number variable as range 1..12. I'm not an Ada programmer and I am sure those more experienced with it will point out practical shortcomings in the way Ada does it. But I'm sure it could be nicer in Rust. (You can even do it in C++ templates.) There is a bounded_integer crate, I see.
Posted Sep 4, 2025 19:48 UTC (Thu)
by Cyberax (✭ supporter ✭, #52523)
[Link] (1 responses)
If you want to start using numeric types for things like packet length, you quickly get bogged down with casts when arithmetic results in possible out-of-bounds. In the end, it's just easier to use safe arrays with safe indexing.
Posted Sep 6, 2025 10:40 UTC (Sat)
by tialaramex (subscriber, #21167)
[Link]
Firstly, bounded integers give us a niche and Rust knows how to use the niche, with built-in types such as Option<T> as well as any user types being allowed to consume a niche - so now our data structures are smaller, yet our software is more correct, that's a win-win deal.
But also - this isn't a thing Rust is expected to do in the foreseeable future but it's certainly reasonable for Linux to be thinking about it in this context, bounded integers mean you can use mechanical proofs to ensure you can't write certain crucial types of bug. This is why WUFFS gets to have no bound misses despite not explicitly emitting bounds checks. It has verified that your code doesn't use any values which would cause a miss, ensuring that you meet these mathematical criteria is your problem and so you might need to write bounds checks, but often your algorithm can prove it doesn't miss anyway and WUFFS checks the proof.
Posted Sep 5, 2025 14:07 UTC (Fri)
by daroc (editor, #160859)
[Link]
Posted Sep 20, 2025 19:24 UTC (Sat)
by mrugiero (guest, #153040)
[Link] (5 responses)
The easy solution to that (both in Rust and many other strongly typed languages) is the newtype pattern.
Rather than using `Untrusted<String>` for both your HTML and SQL sanitization, you would do something like:
I wonder if there's a way to ban implementing `Validate` for types that are considered too generic to force one into the newtype pattern when that should be a reasonable requirement (such as strings that may be literally anything).
Posted Sep 22, 2025 8:52 UTC (Mon)
by taladar (subscriber, #68407)
[Link] (4 responses)
Posted Sep 22, 2025 9:34 UTC (Mon)
by farnz (subscriber, #17727)
[Link] (3 responses)
Solutions to this tend to either feel un-Rusty (since you have your own special UntrustedFrom, instead of using From like everyone else), or fall foul of the orphan rule, or make it hard to verify that Untrusted<DisplayName>::validate really does validate against all of the required validation types cheaply.
Posted Sep 23, 2025 8:03 UTC (Tue)
by taladar (subscriber, #68407)
[Link] (2 responses)
Posted Sep 23, 2025 9:20 UTC (Tue)
by farnz (subscriber, #17727)
[Link] (1 responses)
Untrusted<T> is closer to the way &str relates to &[u8]; a string slice is a byte slice with the additional promise on top that it's valid UTF-8. In this analogy, Untrusted<T> is to T as &[u8] is to &str; you may "know" that this "should" be a safe string, but you have to actually validate it to use it (like you would with str::from_utf8 if you were dealing with byte slices).
Posted Sep 24, 2025 7:46 UTC (Wed)
by taladar (subscriber, #68407)
[Link]
Posted Sep 4, 2025 12:28 UTC (Thu)
by excors (subscriber, #95769)
[Link] (5 responses)
My initial thought was: doesn't the first type imply that the length of the slice is untrusted, while the second implies it is trusted? That sounds like a dangerous conversion to do automatically. But from reading the patch set, Untrusted<[T]> is documented as having a trusted length ("as it would otherwise violate normal Rust rules") and only the elements are untrusted, so the conversion is fine.
I don't entirely see how that would work, though. I guess low-level code that's constructing an Untrusted<[T]> from e.g. core::slice::from_raw_parts(ptr, len) will be responsible for ensuring the length is safe (not extending outside the appropriate address space or backing buffer, etc). But 'safe' does not mean 'trusted' (that distinction is fundamental to this patch set), and the Untrusted<[T]> / [Untrusted<T>] will come into existence before we reach the higher-level code that knows how to validate the length properly, so I'm not sure how it can be claimed that the slice length is trusted.
With APIs like copy_from_user (in C) and iov::IovIterSource::copy_from_iter_raw (in Rust), the caller has to provide a (trusted) maximum length to copy - but the actual length copied might be smaller than that, since it'll truncate the copy if e.g. it hits an unmapped page (I think), meaning the length returned is a user-controlled value and I believe it should be considered untrusted. Otherwise there's a risk of attackers causing out-of-bounds panics, when a driver makes invalid assumptions about the slice length because it's documented as being trusted and they don't realise it's still attacker-controlled and needs validating.
Posted Sep 4, 2025 15:09 UTC (Thu)
by matthias (subscriber, #94967)
[Link] (4 responses)
Actually, the length of a slice is stored in the reference and not stored as part of the slice. &mut Untrusted<[u8]> is a wide pointer that is a pair (ptr,len) that says at position ptr, there is data of type Untrusted<[u8]> that has size len. The len is always part of the reference, not part of the type T inside Untrusted<T>.
This is how all dynamically sized types work in rust. The length is always stored in the reference which therefore needs twice as much space as ordinary references. The difference between &mut Untrusted<[u8]> and &mut [<Untrusted<u8>] is really just the difference between a slice of size len that is untrusted and len many bytes that are individually untrusted.
If you want to imply that the length is not trusted, this would be sth. like Untrusted<&mut [u8]>. This would be indeed very strange as not only the length would now be untrusted but also the pointer and the associated lifetime.
Posted Sep 4, 2025 17:46 UTC (Thu)
by excors (subscriber, #95769)
[Link] (2 responses)
On the other hand Untrusted<Vec<T>> derefs to Vec<Untrusted<T>>, and in that case I don't think there's any DST cleverness happening - it's simply moving the Vec's length field from inside to outside the Untrusted, which looks like a change in the trustedness of the length field, without any validation. I don't think this is a big problem, it just seems like an unfortunate hole in the trust boundary.
Posted Sep 4, 2025 22:56 UTC (Thu)
by NYKevin (subscriber, #129325)
[Link] (1 responses)
* A pointer to some heap allocation, which can be dynamically resized.
If the pointer points to userspace, then it is very questionable to call this thing a Vec, because you can't dynamically reallocate userspace memory - it's better to characterize that as a slice, or something[1] resembling a slice. But if the pointer points to kernelspace, then that means you called kmalloc or one of its equivalents, and you had darned well better know that it's still a valid allocation, as well as how big that allocation is. That implies the capacity is trusted completely, and the pointer is trusted to point to a live allocation of the correct size and alignment. Which just leaves size. Size tells us how many elements (T objects) in the allocation are initialized.
Technically, uninitialized values are instant UB unless protected by MaybeUninit<T> (or behind a raw pointer). If size is wrong, Vec will produce uninitialized values when we index it, and that's UB. So we do need to validate size at some point in this whole procedure, or else we lose soundness. But by assumption, we are in kernelspace, so "initialized" just means that we called something resembling copy_from_user(), and it wrote some sort of data into that array slot. When we did that, we should have set size to the appropriate value for whatever copying we did (because we know what arguments we passed to copy_from_user()).
In short, if you really have a Vec, and not an elaborately disguised UserSlice or the like, then you should already trust enough of it to make this transformation valid.
[1]: https://rust.docs.kernel.org/next/kernel/uaccess/struct.U...
Posted Sep 6, 2025 11:22 UTC (Sat)
by tialaramex (subscriber, #21167)
[Link]
Two things are going on here for anybody fascinated but not wanting to go read the Rust internals
1. Runtime performance optimizations. Vec<T> is Rust's growable array type so it must have excellent performance. This is less crucial in Linux or some embedded code but in most normal userspace code you're using growable arrays a LOT and so the performance of this type is crucial to overall Rust performance.
An example of this would be do_reserve_and_handle which is an inner cold function, so that the compiler will emit all the code to actually go do a bunch of work in a separate function, whereas the code around it is warm, and will be inlined aggressively but it will usually do something very cheap, such as checking n <= capacity, only calling do_reserve_and_handle when that's not true.
2. Separation of concerns. RawVecInner literally doesn't know about T. It just makes growable arrays of bytes. The people working on RawVecInner do need to fret about Allocators (you might not use the default one) and efficiency but not about the type T. On the other hand RawVec<T> can pass the allocator problem to RawVecInner and cope only with the difference between bytes and T if T isn't a byte. Cap is a type dedicated solely to the problem of efficiently improving upon the obvious but not quite adequate "Capacity is just an integer" idea. People working on Cap don't need to think about any other part of the Vec<T> problem.
Posted Sep 4, 2025 20:14 UTC (Thu)
by NYKevin (subscriber, #129325)
[Link]
The lifetime parameter is indeed trusted, because the borrow checker "lifts" it out of the type parameter and treats it roughly the same as a top-level lifetime parameter (i.e. if you have Untrusted<&'a T>, then Rust automatically infers that the Untrusted object must not outlive 'a, because &'a T must not outlive 'a).
Technically, it's more complicated than that, and the precise behavior depends on the fields of Untrusted, see [1] for details. But in short, Rust will automatically infer relationships between Untrusted<&'a T> and &'a T as appropriate to prevent you from using a reference whose lifetime has ended, and at no point does it consider the possibility that &'a T points to a T object that fails to live for at least 'a. Even if &'a T only appears in PhantomData, a relationship is still inferred (Rust interprets this as "I logically own some &'a T even though you can't see it listed in my fields").
But it gets even worse. If you ever expose a real &'a T to safe code (i.e. code that does not live inside the implementation of Untrusted or its owning crate), and it points into userspace-controlled memory, then you have probably violated soundness in multiple different ways:
* If userspace modifies the T while the reference exists (e.g. from another thread), and T is not UnsafeCell<U>, then the behavior is undefined. If T is UnsafeCell, then you have to turn it into a raw pointer (*mut U) via UnsafeCell::get() to do anything anyway. We can't have e.g. T = Mutex<U>, because then the whole Mutex object lives in userspace, and userspace is not beholden to Rust's safety rules (so it could modify the inner value without taking the lock, or even stomp on the futex and break everything).
On the other side of things, once you have all of the necessary guardrails around a userspace pointer, there is not much point in giving it a lifetime parameter. You're already (by assumption) calling fallible functions like copy_from_user() or get_user() (or their Rust equivalents) instead of directly dereferencing an address. You already have runtime error checking, and cannot violate soundness by holding onto the pointer for too long. Static borrow checking would provide no benefit, and might as well be disabled for ergonomic reasons.
This is one of (probably) several reasons why UserPtr<T> has to exist and can't be spelled as Untrusted<&'a T> or any similar signature (note the lack of a lifetime).
[1]: https://doc.rust-lang.org/reference/subtyping.html#variance
Posted Sep 7, 2025 17:13 UTC (Sun)
by adobriyan (subscriber, #30858)
[Link]
I suspect this will be as useful as Ada's integers for months from 1 to 12 mentioned above,
Here is the simplest example where "validation" happens in wider context (of a block device).
Finally
Rust could prohibit reuse of a value
It strikes me, reading UserSlice's documentation that the fix isn't so much making UserPtr not implement Copy (since UserPtr is equivalent to a raw pointer), but making UserSlice's, UserSliceReader's and UserSliceWriter's methods always handle Untrusted<T>, and not T directly.
Rust could prohibit reuse of a value
Rust could prohibit reuse of a value
Validate & Copy?
Wol
I think we definitely can add functions to Validate & Copy?
UserSliceReader
that copy and validate in the same step. But we'll have to do that for every single way you can read untrusted data. Having a generic API that you can just plug untrusted values into should still exist for APIs that don't provide such a function themselves.
There are also other use-cases for untrusted data, for example with UserSliceReader
you might also just want to copy some bytes from one place in userspace to another and you wouldn't want to validate anything in between. So if we only had the copy & validate function, you would have to write an empty validation function for [u8]
.
Different kinds of validation?
Different kinds of validation?
Different kinds of validation?
Bounded integers
Different kinds of validation?
Different kinds of validation?
struct SQLText(pub String);
struct HTMLText(pub String);
impl Validate for Untrusted<HTMLText> {
fn validate(self) -> Result<HTMLText> {
// HTML validation logic here
}
}
impl Validate for Untrusted<SQLText> {
fn validate(self) -> Result<SQLText> {
// HTML validation logic here
}
}
And the functions receiving each trusted type would look like:
fn operate_on_html(HTMLText(text): HTMLText) -> Something {
// Stuff
}
fn operate_on_sql(SQLText(text): SQLText) -> SomethingElse {
// Other stuff
}
Different kinds of validation?
The challenge is that you want to model "convert Untrusted<&str> to Untrusted<HTML> or Untrusted<SQL>", so that what you're passing round is always an Untrusted<T>, the conversions are zero-cost, the validations are cheap, and using a newtype like DisplayName that requires that something validates as SQL, HTML, shell, and whatever other places you want to use it in.
Different kinds of validation?
Different kinds of validation?
That's a related problem, solved already (as you've shown) by the newtype pattern.
Different kinds of validation?
Different kinds of validation?
Trusted slice lengths
Trusted slice lengths
> My initial thought was: doesn't the first type imply that the length of the slice is untrusted, while the second implies it is trusted? That sounds like a dangerous conversion to do automatically. But from reading the patch set, Untrusted<[T]> is documented as having a trusted length ("as it would otherwise violate normal Rust rules") and only the elements are untrusted, so the conversion is fine.
Trusted slice lengths
Trusted slice lengths
* A size
* A capacity
Trusted slice lengths
Trusted slice lengths
* Safe Rust assumes that you can dereference &'a T at any time during 'a, and that it will always succeed and produce a T instance. In particular, the dereference operation is not allowed to fail with EFAULT or the like, so that would have to panic (obviously an unacceptable response to "userspace gave me a bad pointer").
* The T instance must be initialized unless T is MaybeUninit<U> (or some equivalent, since unlike UnsafeCell, MaybeUninit is not magic and can be implemented by hand). I have no idea how that rule even applies when you're pointing into shared memory, but I think it doesn't have a good answer because...
* Safe references are never volatile, so it's pretty much always incorrect to produce or dereference a safe reference pointing into shared memory. This is the case even if you fully serialize all reads and writes with some external locking, because LLVM (or GCC etc.) is permitted to optimize under the assumption that nobody else is reaching into your memory and modifying it behind your back (if the pointer escapes, then the compiler probably has to pessimistically assume that foreign code might fiddle with it, but that does not mean it is valid to do this in general). The "correct" way to do it is with functions like read_volatile[2], but those all take raw pointers, not safe references.
[2]: https://doc.rust-lang.org/std/ptr/fn.read_volatile.html
Parse not validate!
> + V::validate(&self.0)
> + }
covering only the most simplest examples (which are the least interesting).
https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/...