In other words: this won't detect memory unsafety that doesn't result in an abnormal exit or other detectable fault. If I'm writing an exploit, my entire goal is to perform memory corruption without causing a fault; that's why Rust's safety property is much stronger than crash-freeness.
In order for the fork() it calls to be safe, it needs to guarantee a bunch of properties of the program that it simply cannot. If this gets used in a multithreaded program that calls malloc, you've got UB. There's a long list of caveats with fork mentioned in some other comments here.
In my view, this is not serious code and should be regarded as a joke. There's no actual value in this type of isolation.
And they are very much more common than in most C codebases. C codebases are generally often overly permissive in what they accept (hence to security bugs). Rust made a different trade.
In this context, I'm using "crash" to mean something like a program fault, i.e. an uncontrolled termination orchestrated by the kernel rather than the program itself. Rust programs generally terminate in a controlled manner, even if that manner is analogous to an unchecked exception.
It's also not my experience that Rust code, on average, panics on abnormal inputs. I've seen it happen, but the presence of e.g. safe iterators and access APIs means that you see a lot less of the "crash from invalid offset or index" behavior you see in C codebases.
(However, as pointed out in the adjacent thread, none of this really has anything to do with what "safe" means in Rust; controlled termination is one way to preserve safety, but idiomatic Rust codebases tend to lean much more heavily in the "error and result types for everything" direction. This in and of itself is arguably non-ideal in some cases.)
That's kind of up to you as the developer though. I generally avoid writing functions that can panic -- I'd even argue any non-test code that panics is simply poorly written, because you can't "catch" a panic like you can in a high-level language. Better to return an error result and let the calling code decide how to handle it. Which often means showing an error to the user, but that's better than an unexpected crash.
It is entirely up to you as the developer to write memory-safe code in C, and it's possible to do so. Most programmers don't because it's hard to do that once you're doing anything nontrivial. It's also possible to write panic-free rust, but it's hard.
If there were a Quick Fix for safety, we'd probably have discovered it by now.
> use SECCOMP_SET_MODE_STRICT to isolate the child process. But at that
> point, what are you even doing? Probably nothing useful.
The classic example of a fully-seccomp'd subprocess is decoding / decompression. If you want to execute ffmpeg on untrusted user input then seccomp is a sandbox that allows full-power SIMD, and the code has no reason to perform syscalls other than read/write to its input/output stream.On the client side there's font shaping, PDF rendering, image decoding -- historically rich hunting grounds for browser CVEs.
Yes. I've run JPEG 2000 decoders in a subprocess for that reason.
1. The forked process has a copy of the program state. If I'm trying to steal in-process secrets, I can do it from the forked process.
2. The forked process is just as privileged as the original process. If I'm trying to obtain code execution, I don't care which process I'm in.
This is why Chrome at al. have full-fledged sandboxes that communicate over restricted IPC; they don't fork the same process and call it a day.
https://hacks.mozilla.org/2020/02/securing-firefox-with-weba...
Firefox employs processes for sandboxing but for small components they are not worth the overhead. For those they employed this curious idea: first compile the potentially unsafe code to wasm (any other VM would work), then compile the wasm code to C (using the wasm2c tool). Then use this new C source normally in your program.
All UB in the original code becomes logical bugs in the wasm, that can output incorrect values but not corrupt memory or do things that UB can do. Firefox does this to encapsulate C code, but it can be done with Rust too
Pseudo sandboxing on the fly is an old idea and with its own issues, as proven by classical UNIX approach to launching daemons.
Windows: https://chromium.googlesource.com/chromium/src/+/main/docs/d...
Linux: https://chromium.googlesource.com/chromium/src/+/main/sandbo...
OS X: https://chromium.googlesource.com/chromium/src/+/main/sandbo...
With the caveat that I wouldn't necessairly assume this is the cutting edge at this point, and there might be other resources to invest in for server-side sandboxing involving containers or hypervisors, and that I've only actually engaged with the Windows APIs based on that reading.
I wrote `firehazard` ( https://docs.rs/firehazard/ , https://github.com/MaulingMonkey/firehazard/tree/master/exam... ) to experiment with wrapping the Windows APIs, document edge cases, etc. - although if the long list of warnings in the readme doesn't scare you away, it'll hopefully at least confirm I hesitate to recommend my own code ;)
For Windows, you probably want WSB[2] or AppContainer isolation[3].
For Linux, the low-level primitives for sandboxing are seccomp and namespaces. You can use tools like Firejail and bubblewrap to wrap individual tool invocations, similar to sandbox-exec on macOS.
[1]: https://developer.apple.com/documentation/xcode/configuring-...
[2]: https://learn.microsoft.com/en-us/windows/security/applicati...
[3]: https://learn.microsoft.com/en-us/windows/win32/secauthz/app...
(However, the technique here is itself not sound, per the other threads.)
I'm sure there's still value in this project, but I'm not sure I'm versed enough in Rust to know what that is.
[1] https://man7.org/linux/man-pages/man7/signal-safety.7.html
I'm adding a few more limitations in this PR: https://github.com/brannondorsey/mem-isolate/pull/44
I know async-signal-safety is particularly important for, you know, signal handlers. But aside from those, and the multi-threading use case you describe, is there another use case where calling non async-signal-safe code from inside this module would lead to issues (that isn't covered in the new limitations)?
I can add another limitation is issues can transpire if the code you run in `callable()` isn't async-signal-safe, but I'd like to offer a few additional examples of gotchas or surprises to point out there.
A function being marked unsafe in Rust indicates that there are required preconditions for safely invoking the function that the compiler cannot check. Your “safe” function provided by this crate sadly meets that definition. Unless you take great care to uphold the requirements of async-signal-safety, calling your function can result in some nasty bugs. You haven’t made a “safe” wrapper for unsafe code like the crate claims, so much as you’ve really just traded one form of unsafety for another (and one that’s arguably harder to get right at that).
But I wouldn't use this often. I'd be willing to bet that you'd lose all performance benefits of using Rust versus something like Python or Ruby that uses forking extensively for parallelism.
Yeah, this is really the main use case. Its a relatively simple solution when you can't do any better.
I think that's particularly helpful when you're invoking code you don't control, like calling into a some arbitrary C library.
Tools like valgrind do not work on windows, and I am nowhere near smart enough to know the entire layout of memory that should exist.
When using Windows and calling system system functions, there’s a lot of casting involved; to convert wide characters and DWORDS to rust primitives for example. And given that I don’t have a good debugging situation, I’m terrified that I’m corrupting or leaking memory.
does anyone know any good tools that work on windows to help me out here?
• Work out what you want to do, conceptually.
• Design a safe abstraction that would allow you to do that. (Consult the Win32 API documentation for concepts to use.)
• Implement that abstraction using the Win32 API.
That last step is way easier than trying to use the Win32 API throughout your program, you'll end up with significantly less unsafe code, and if anything does go wrong, it's much easier to fix.
In order to call the win32 API one must create structs and pass pointers to them into the function call.
sending data is actually quite easy. But reading back data is quite difficult, in some cases you may have a list of something.
Regardless, Rust is not helping me anymore in those bits, and since all of the tools that find memory issues target primarily C++, and rust mangles certain things for C+ + toolchains - I find myself a little bit stuck, I’m not a genius and I’ll take all the help I can get.
Starts with Visual C++ analysers, SAL annotations, hardned runtime.
Then commercial tooling like PVS Studio, Parasoft for example.
Check out windows-rs instead.
In my case I am looking for NetUserAdd and associated functionality, which doesn’t exist in any wrapper crate I could find- since that would have been significantly easier than what I ended up needing to do.
But, how do they test their unsafe bits?
Rust allows memory-impure things, like interior mutability of arguments, so you can get different (i.e. incorrect) results when using this to run otherwise fine rust code.
For example:
fn some_fn(x: &mut i32) {
*x = 2;
}
fn main() {
let mux x = 1;
mem_isolate::execute_in_isolated_process(|| {
some_fn(&mut x);
}).unwrap();
println!("{x}"); // prints '1' even though without 'mem_isolate' this would be 2
}
What if the unsafe code is not supposed to be pure but mutates some memory? For example, does this allow implementing a doubly-linked list?
Let me know if you think this wording could be improved or I'm missing any details.
Awesome work!
It should be called "fork and see" pattern instead :D