mem-isolate
runs your function via a fork()
, waits for the result, and returns it.
This grants your code access to an exact copy of memory and state at the time just before the call, but guarantees that the function will not affect the parent process’s memory footprint in any way.
It forces functions to be memory pure (pure with respect to memory), even if they aren’t.
use mem_isolate::execute_in_isolated_process; // No heap, stack, or program memory out here... let result = mem_isolate::execute_in_isolated_process(|| { // ...Can be affected by anything in here unsafe { gnarly_cpp_bindings::potentially_leaking_function(); unstable_ffi::segfault_prone_function(); heap_fragmenting_operation(); something_that_panics_in_a_way_you_could_recover_from(); } });
Example use cases:
- Run code with a known memory leak
- Run code that fragments the heap
- Run
unsafe
code - Run your code 1ms slower (har har 😉, see limitations)
NOTE: Because of its heavy use of POSIX system calls, this crate only supports Unix-like operating systems (e.g., Linux, macOS, BSD). Windows and wasm support are not planned at this time.
See the examples/ for more uses, especially the basic error handling example.
POSIX systems use the fork()
system call to create a new child process that is a copy of the parent. On modern systems, this is relatively cheap (~1ms) even if the parent process is using a lot of memory at the time of the call. This is because the OS uses copy-on-write memory techniques to avoid duplicating the entire memory of the parent process. At the time fork()
is called, the parent and child all share the same physical pages in memory. Only when one of them modifies a page is it copied to a new location.
mem-isolate
uses this implementation detail as a nifty hack to provide a callable
function with a temporary and isolated memory space. You can think of this isolation almost like a snapshot is taken of your program’s memory at the time execute_in_isolated_process()
is called, which will be restored once the user-supplied callable
function has finished executing.
When execute_in_isolated_process()
is called, the process will:
- Create a
pipe()
for inter-process communication between the process it has been invoked in (the “parent”) and the new child process that will be created to isolate and run yourcallable
fork
15 Comments
djha-skin
This is cool from a theoretical perspective, but `fork()` can be prohibitively expensive, at least on the hot path. This is a cool tool that should be used with care.
null_investor
Forking and this package can be useful if you know that the unsafe code is really unsafe and have no hope of making it better.
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.
dijit
this seems like a good place to ask, I don’t write very much unsafe Rust code… but when I do, it’s because I’m calling the Win32 API.
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?
teknopaul
Hammer, nut.
Clever trick tho if you are in a bind.
pjmlp
Rather design the application from the start to use multiple processes, OS IPC and actual OS sandboxing APIs.
Pseudo sandboxing on the fly is an old idea and with its own issues, as proven by classical UNIX approach to launching daemons.
woodruffw
I don't think this meets the definition of "safe" in "safe" Rust: "safe" doesn't just mean "won't crash due to spatial memory errors," it means that the code is in fact spatially and temporally memory safe.
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.
m00dy
>>We call this trick the "fork and free" pattern. It's pretty nifty.
It should be called "fork and see" pattern instead :D
slashdev
I'd love to know what horrible library / code the author was using where sandboxing it like this seemed like the best alternative.
destroycom
This isn't mentioned anywhere on the page, but fork is generally not a great API for these kinds of things. In a multi-threaded application, any code between the fork and exec syscalls should be async-signal-safe. Since the memory is replicated in full at the time of the call, the current state of mutexes is also replicated and if some thread was holding them at the time, there is a risk of a deadlock. A simple print! or anything that allocates memory can lead to a freeze. There's also an issue of user-space buffers, again printing something may write to a user-space buffer that, if not flushed, will be lost after the callback completes.
corank
> It forces functions to be memory pure (pure with respect to memory), even if they aren't.
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?
wavemode
If you can afford to sacrifice that much performance just to run some potentially unsafe code, then you can probably afford to not be writing Rust in the first place and instead use a garbage-collected language.
jesprenj
Why use a pipe to communicate instead of shared memory?
nextaccountic
There is a way to sandbox native code without forking to a new process, and it looks like this
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
loeg
As a joke, it's funny. Obviously you would not want to actually deploy this. I feel like most comments are too quick to criticize using this in prod (don't!) and missing the point.
Svetlitski
This is likely to violate async-signal-safety [1] in any non-trivial program, unless used with extreme care. Running code in between a fork() and an exec() is fraught with peril; it's not hard to end up in a situation where you deadlock because you forked a multi-threaded process where one of the existing threads held a lock at the time of forking, among other hazards.
[1] https://man7.org/linux/man-pages/man7/signal-safety.7.html