WinMMF - Sharing the braincell
Riven Skaye / May 2024
Sharing memory on Windows
And why you should (not) forget about it
So the initial implementation was almost quickly written. I reasonably quickly decided that, since the goal is to share many bytes of data, I wasn’t going to use the W
-APIs (16-bit Unicode) as all we need here is a unique (user-provided) name for the mapping to be created. There are common conventions, most of which include using your application (or lib/namespace/crate/etc) name as a prefix to ensure uniqueness. The OS API wants a PCSTR
which turns out to just be a pointer to an ANSI string, which it uses to map clients requesting the same named memory into the same address space. Oh and if that memory needs to be accessible to processes that are not your children, you should prefix it with GLOBAL\
and be sure to have the right permissions. Which for globally accessible shared memory means running as a service, an admin, or any other account with SeCreateGlobal/SE_CREATE_GLOBAL. This is where the funny stuff started happening. Turns out the OS internals hold no copy of the data or anything, they just store a pointer to the string you hold. This is problematic for a few reasons, and we’ll get there in a little bit to go over the problems this causes. First up, however, is how I found this out. And why.
The minimal test application was about as unsafe as use after free
meets thread panicked while holding a lock
and passing null pointers and casting them
combined. You see, I thought creating the PCSTR
and keeping that struct around would be fine. There was some ambiguity with regards to what the PCSTR
was built up of (this ambiguity has since been fixed) that made it seem like I was getting a proper struct back. Alas, it’s a wrapper around a (*const u8)
. So that was really just taking a pointer to the first u8
in my string and rolling with it. I only found out because trying to dump the names of the mappings I opened but couldn’t access elsewhere got me garbage data after a few tries. So then I started playing around, and lo and behold it was properly functional when I ignored the PCSTR
and instead just kept the string around that I’d used to open it. For this purpose I used fixedstr::zstr
, a small and simple, [u8;N]
-based String-like type that ensures NUL
terminators are present. Literally perfect for what the Windows API wants, and it even provides a safe method to acquire that pointer! But this does bring us to what’s so potentially dangerous about this API. You don’t assume the OS holds a pointer to your string, when the official documentation makes several mentions of how this shared memory is usable until all handles are closed. That means they expect you to leak the string! After all, if you don’t leak it, other processes will just need to guess what is now behind that pointer.
Unsafely accessing a gabrage string
Some more explanation for that garbage data as the name is in order. If the process that first opened the file frees the memory (e.g. cleanup on exit), no other processes can open that mapping unless they manage to get data matching what lies behind that pointer (freed and mangled, by now). You can leak it, and hope that Windows doesn’t clean it up for you, but there’s nothing in the win32 docs that gives any security about this being the case. That’s right, I can’t promise you that an MMF will be accessible to new processes if the memory has been overwritten! For now, I’ve decided to not leak the memory. You had better keep your initializing process open, which should be logical anyway from any point of view I can imagine. And for the project I’m writing this lib for in the first place, the idea is to:
- Claim any cameras connected to the system in a startup service,
- Open MMFs big enough to hold one full frame for all of them,
- In a callback, keep overwriting the MMF’s contents with the next frame,
- We’ll tune the framerate to be reasonable
- on perf hits, tuning can be done by instead dropping N frames (a topic for another time)
- Find a good balance between skipping frames when the lock is held elsewhere and waiting for it to free up
- Make this as performant as I can because not much can be slower than the current C# solution.
Of course, that is all part of the plan, but at the point in time I was figuring out that my name was being freed, I was working on a bare implementation to see if I could even get this to work from Rust. There were no locks, I hadn’t even settled on an interface or anything. It was just a plain old sequentially opened and read file. Because of the testing nature of it all, I was manually ensuring nothing was trying to interfere with each other. Function one would write, function two would read, and I’d panic if the results were unexpected. And you know what? It worked! So I could start on implementing it in a bit more of a safe way, and then also write an FFI interface for it to make sure this is usable from C# as well. I could consider using the dotnet exposed methods of opening memory-mapped files, but that is just not cursed enough for my tastes not using the lock I created, or Rust’s speed in manipulating the data behind those pointers. Not to mention that I can’t find anything about opening global MMFs in dotnet. Only about opening them and sharing them to children.
I could also consider instead using the pointer to the non-lock part of the MMF to have the service update the frames realtime, but with the current design of WinMMF, this is explicitly a blocked option. You should not be wanting to pass raw pointers around that are behind a lock unless you’re using an external locking mechanism. Sure, it would be faster, and there’s nothing stopping me from making a private implementation that allows me to do that, but it’s not my intent. It would, quite frankly, require a very different approach to designing the lock in a way that it isn’t implemented as a bunch of Rust internal types. And that, dear reader, is up to someone else implementing the traits exposed.
Ramblings of an Alchemist by Riven Skaye is licensed under Creative Commons Attribution-ShareAlike 4.0 International