4 ms·
This is something I’ve always wanted from a type system, or a way to make it if you can easily make custom types. Especially for strings, bags of bytes are easi
by MBCook 1y ago
This is something I’ve always wanted from a type system, or a way to make it if you can easily make custom types. Especially for strings, bags of bytes are easier. Seems like it could help in a lot of circumstances with security issues.
Writing a web app? All user input is untrusted until you process it. And if Untrusted<String> can’t be converted to String accidentally then it forces the programmer to think about it.
Unfortunately in Java (my everyday language) this isn’t feasible. I’d want to be able to join or process Untrusted<String> the same as normal. Really it would need to be built into the stars library.
Back to the article it sounds like this could work really well for the kernel. I hope this kind of idea catches on outside of that.
- lock1 1y agoWhy is that not feasible? You could define `Untrusted<T>` container and `.map()` in Java just fine.
- menaerus 1y agoYes, it can be implemented basically in any language that can hide the data members so I also see nothing special about it.
- surajrmal 1y agoIt's only special when you are coming from C. Kernels implemented in c++ have done this sort of thing for a long time.
- kimixa 1y agoThere's plenty of C string libraries with opaque types. No reason why a similar thing can't be done there.
- menaerus 1y agoOpaque types cannot be stack- or statically allocated.
- kimixa 1y agoNo, but many languages already have limitations like "strings live on the heap", even many discussed in this thread. Dynamically sized objects on the stack has always been difficult. You could argue that C special casing fixed length strings to allow that is the odd one out.
- ViewTrick1002 1y agoNot really. Hiding the data is the easy part. What makes Rust special is that you need to acknowledge the potential errors when unwrapping the type. With a standard library and culture built on exposing the edge cases at compile time. That is where the guarantees and feeling of certainty comes from.
- lock1 1y agoVisibility modifier as a native language feature is not the easy part. It's more like "comptime safety feeling" => "language w/ visibility modifier" but the converse is not necessarily true. Without language support, it's back to C convention or workaround again.
- menaerus 1y agoShow me an example. I don't understand this marketing lingo.
- ViewTrick1002 1y agoTake for example working with file names and paths. In for example Linux paths are a collection and bytes and does not need to be valide unicode. Which some programming languages hides from you and subtly introduces errors. Or even worse, accepting that strings may contain invalid unicode poisoning the entire language with uncertainty. If you are iterating over the files in a directory the Rust standard library gives you paths. Not strings. To convert a path that to a regular string type which is valid unicode you need to acknowledge that the path may be invalid. Either unwrapping and panicking or handling the error. To do this the standard library gives two options: /// Yields a [`&str`] slice if the `Path` is valid unicode. /// /// This conversion may entail doing a check for UTF-8 validity. /// Note that validation is performed because non-UTF-8 strings are /// perfectly valid for some OS. pub fn to_str(&self) -> Option<&str> https://doc.rust-lang.org/std/path/struct.PathBuf.html#method.to_str https://doc.rust-lang.org/std/path/struct.PathBuf.html#metho... Or accept that the conversion may be lossy: /// Converts a Path to a Cow<str>. /// /// Any non-UTF-8 sequences are replaced with U+FFFD REPLACEMENT CHARACTER. pub fn to_string_lossy(&self) -> Cow<'_, str> https://doc.rust-lang.org/std/path/struct.PathBuf.html#method.to_string_lossy https://doc.rust-lang.org/std/path/struct.PathBuf.html#metho... You get to make a choice, and panicking/erroring is perfectly valid if you only expect valid unicode paths. As the world and your software changes if you suddenly encounter a non-unicode path then you will immediately know where the error comes from and can fix the issue. Instead of trying to pinpoint the root source of an error far exposing itself far down stream.
- taktoa 1y agoRust and C++ implement generics with monomorphization rather than boxing, so there is a potential performance hit associated with a type like this in Java that is guaranteed not to exist in Rust. In practice, the JVM may still monomorphize it, but it is not guaranteed to, and this would be a good reason to avoid unnecessary uses of generics in a high performance codebase like a kernel, if you chose to write one in Java.
- lock1 1y agoSure, I guess that's worth mentioning in the context of the original post. It's true that Java implementation of parametric polymorphism has a performance drawback compared to Rust or C++. And it's certainly a bad idea to use Java generics without considering the drawback in hot code paths. But GP described something they wanted from a type system and basically said container with `Functor`-like behavior is not possible to do in Java. It's possible, albeit with a performance drawback and a bit more clunky to work with compared to Rust, Haskell, or a language with native HKT support.
- wavemode 1y agothe parent commenter states that Java is their everyday language, so in this context I don't think we're talking about performance, nor about the needs of the kernel.
- lmm 1y agoJava lacks higher-kinded types, so there is no library of helpful functions that work on anything with a .map() method. E.g. if you want to do a tree traversal you'll have to implement it by hand.
- MBCook 1y agoSince I do web apps Strings are my bread and butter. If you make UntrustedString a subclass of String (trusted) then you lose type safety. So TrustedString has to be the subclass. Easy enough. But now string literals are “untrusted”. So you have to do new String(“this is trusted content”) everywhere you need it, which is a pain. And you can’t add trusted strings. The operator will return a normal String (untrusted) so you have to cast it. Same with any function you call like substring. So you have to live with that, or make overrides for every single string function that fix the types where necessary. It’s just really non-ergonomic. I think having it built in would likely make it far better.
- lock1 1y ago> But now string literals are “untrusted”. So you have to do new String(“this is trusted content”) everywhere you need it, which is a pain. Eh? Isn't that the main point of doing all of this? Being explicit on the boundaries but still providing a way to manipulate them like a String? I don't see anything wrong with `new Validated("literal")` (or functional friendly `Validated.of("literal")`). If you intend to create a `Validated`, then create it via constructor / static factory method that enforces necessary validations to create `Validated`. > So you have to live with that, or make overrides for every single string function that fix the types where necessary. Like `Optional<T>` and `Stream<T>`, you could define `Validated::map(Function<? super String,String>)` if you want. With `map()`, you could operate `Validated` with anything that accepts `String` like usual. With that said, I don't recommend using `Validated` in your actual Spring project though, use (OOP) value objects instead. I used value objects quite a lot on my legacy Spring project. It plays nicely with functional-style, cover "validation" stuff, and avoiding primitive obsession. Putting `String` on `UserId` will result in a loud compiler error.
- bjackman 1y agoIt's worth noting that in the kernel there's more to "user data" than just the fact that its content might be crafted maliciously: - it might get swapped out or migrated, meaning your thread goes to sleep if you touch it. - it can always change concurrently. - the pointer is only valid within the current context (coz it's a pointer into the user address space). - I've actually never thought about this before but also the user could free it concurrently I think? So there's actually already C APIs in the kernel that force you to be aware of this, at least for strings (stuff where the user passes a pointer to their memory). There's also fancy compiler stuff for marking struct fields as pointing to user data, which IIUC can be used to detect if you're forgetting to use the right conversion APIs. This isn't the case for values passed in registers though so I think Trusted<u64> would be totally new. ANYWAY, overall: this is cool and good but it's actually one of the lower-impact things from Rust in the kernel IMO. I don't think the bugs that this prevents are actually all that common in practice. TOCTOU would be the biggest one by far but they are rare compared to the daily deluge of incredibly basic UAF bugs that Rust also prevents.
- tux3 1y ago>I've actually never thought about this before but also the user could free it concurrently I think? Yup. But if you only send it back to the malloc heap, the kernel doesn't notice, it will happily read data that has been freed. Unless the data is unmapped, then the kernel is going to get a page fault while trying to read your input. What goes up must come down, so that gets translated to SIGSEGV, and the program stops.
- cyphar 1y agoThe kernel doesn't give you SIGSEGV in that case -- you will usually just get -EFAULT. All user pointer accesses are scoped with user_access_begin() and user_access_end() (or something equivalent) which stops the block of data from being unmapped by another thread -- of course, if this mechanism didn't exist then you would get a kernel oops if there was a concurrent munmap(2).
- cyphar 1y ago
- blibble 1y agoof all languages, perl supported this as a first class feature, "tainting"