Proposal to organize bounty for Linux kernel patch to restrict access to process's own memory via file system

It is well known that Rust's File::open is unsafe on Linux, because it can open /proc/self/mem and write to it to bypass memory safety, see https://github.com/ben0x539/totally-safe-transmute.

The only honest thing to do is to mark File::open unsafe, but it's difficult in practice. An alternative is to make file system access memory safe on Linux, or at least to have an option for file system access to be memory safe. The default probably can't be fixed due to backward compatibility.

I want to organize bounty for Linux kernel patch to restrict access to /proc/self/mem. It probably should use Linux's capability system. That is, file system access to process's own memory is CAP_MEMORY, which processes have by default. You can drop the capability by calling prctl system call with PR_CAPBSET_DROP. Once the patch is in mainline Linux kernel and Debian stable is released with the kernel, estimate 2025, Rust's runtime initialization should drop the capability by default.

If you are interested please reply. Let's make Linux memory safe! Thanks.

3 Likes

First, this doesn't need kernel changes at all. You could, instead, apply such restrictions using seccomp filters: look for any file accesses, and prevent access to /proc/self/mem and equivalent.

Second, there are many, many other ways to access the outside world. The outside world is not a safe construct, and we cannot mark all access to non-Rust things as unsafe, even if the outside world provides mechanisms that could be used to commit unsoundness in Rust. We also cannot enumerate a complete list of mechanisms provided by the outside world that can commit such unsoundness; such a list would always be incomplete.

If you want to go that route, you'd want to do the reverse: list safe operations, and disallow all others. Down that path lies sandbox systems, such as virtual machines and WebAssembly.

Here's a short list of outside-world operations that don't necessarily require unsafe code, and that could do arbitrary things (assuming your process has the requisite permissions). (I'm sure that people could easily generate a list many times this long.)

  • Write out a new executable and run it
  • Modify a library that the dynamic linker has mmapped into your program, and then let new bits of it be faulted in.
  • Arrange for some of your code to be swapped out, then do the above.
  • Run another process, which modifies /proc/$YOURPID/mem
  • Run another process which ptraces you (e.g. a scriptable debugger)
  • Run two more processes: one that uses checkpoint-restart functionality to save an image of your process, and the other that waits for the first, then modifies the image, then re-runs it, so that your program picks up where it left off but with something changed out from under it.
  • Write to a swap file or partition (as root)
  • Write to a hibernation image and then reboot into it
  • Send a network packet to another process that will perform an action on your behalf
  • Exploit a bug in some other program or library.
  • On a system without an IOMMU, use a hardware device to write over the physical memory containing your process.
  • Evaluate an ACPI method (on systems where that isn't locked down and inaccessible even to root).
23 Likes

You could, instead, apply such restrictions using seccomp filters: look for any file accesses, and prevent access to /proc/self/mem and equivalent.

I believe this is not correct, because seccomp filters do not resolve path names. You could symbolic link /proc/self/mem. I agree pathname resolving seccomp filters are acceptable solutions too.

For other stuffs, I will have a look and reply.

Also, aren't seccomp filters non-stackable?

I seem to remember something about having to choose between the Chromium sandbox's seccomp filters and the outer tool's seccomp filters when running Electron apps under things like Firejail or Bubblewrap.

If that's the case, it would certainly not be suitable as a default for something like Rust.

If you want to do more complex filtering, you can use a separate userspace sandboxing process: Deferring seccomp decisions to user space [LWN.net]

My point is that once you go from "trying to protect against mistakes" to "trying to protect against malice", you need a whole new level of protection, and you likely need a full sandboxing scheme.

I'm not suggesting that it would be. I don't think Rust should even attempt do anything about this by default, because there's no hope of completeness. If a program wants to sandbox itself, there are many secure mechanisms of doing so, and I'd recommend using one of those.

10 Likes

Instead of modifying Linux kernel or using seccomp, wouldn't it be simpler to add necessary checks to Rust standard library?

But is this really a part of Rust safety guarantees? Rust allows to perform IO; IO allows to perform actions in the outside world; since your program, OS, computer, etc is part of this world, this may allow you to do something with your program to cause unsafety. By this logic any IO should be unsafe. For example, if I build a robot, should API to control this robot be unsafe because I can use it to make this robot drill a hole in the memory chip?

On the other hand, one can argue that access to /proc/self/mem is more similar to using APIs such as map and unmap which are definitely unsafe.

1 Like

I'm on board with the idea that in principle unsafe refers to whether or not the code satisfies a particular model, or is known to risk empowering users to violate that model. Things like the risk of people holding a heat gun near their RAM to flip bits is "outside" the model. Similarly a bunch of things you are listing feel outside the model, like debuggers.

However, I am a little sympathetic because /proc/self/mem feels like it's right on the edge:

  • It doesn't require a debugger
  • It doesn't require any escalated privileges
  • It doesn't require an external process
  • It doesn't require any unsafe code
  • It doesn't require modifying running the executable
  • It doesn't require external hardware writing directly to memory

Certainly I think there would be value for some users to be able to exclude /proc/self/mem and overwriting of dynamic libraries as causes of segfaults. In practice the second has definitely caused me headaches in the past. I had a past employer with a script that wrapped their main executable and copied the executable and all of its libraries to a temporary location before invoking it just to protect against this issue.

But this seems like more of a problem with the OS's perspective. /proc/self/mem probably isn't considered to be a security issue because from the point of view of coding in unsandboxed C (the traditional language of Linux development) the file isn't giving the process access to anything that it didn't already have. However as languages like Rust that try to provide stronger compile time guarantees become more common, and as projects like WebAssembly that run in user space but consider themselves responsible for sandboxing become more prevalent, maybe this will get revisited.

2 Likes

Apparently there was already a patch to control access to /proc/pid/mem back in 2012. Proposed by someone from the chrome team it seems.

https://lwn.net/Articles/476832/

No idea how far that went.

1 Like

I think this is misguided. Rust's safety/unsafety is for programmer's convenience only — to prevent programmer from accidentally writing wrong code.

It's not a sandbox or a security boundary. You don't need kernel changes to stop yourself from writing to /proc/self/mem. It's not something you'd do by accident.

If you need to run untrusted code that could deliberately do hacks like that, then patching just this one exploit is not nearly enough. It's an example of using a blocklist where allowlist is necessary.

Rust is not intended for, and never even tried to provide that kind of safety. Rust's trust model is fundamentally unsuitable for this. If you have untrusted Rust code to run then you need to run it in something like WASI where you can control memory and system access on basis of an allowlist.

19 Likes

If you want to protect your process from any other user space process you should use LSMs. And the simplest (so secure) approach is certainly SMACK:Smack — The Linux Kernel documentation.

That was my first thought, and then it occurred to me that you could do it by accident via a temp file vulnerability with a symlink attack. So it could be a mechanism for an attacker to get your program to execute arbitrary code, and that is one of the things rust is supposed to protect you from.

It is a security issue, but I'm not convinced that's a Rust's security issue. Rust tries to prevent bugs and mistakes, but it can't prevent them all. It's not inifinite-monkeys-with-typewriters-proof. It has to draw the line somewhere.

If an attacker can create arbitrary symlinks and precisely control what you write to them — your system is compromised already. Doing that with /proc/self/mem and safe Rust only scores extra points for style, but is not even necessary in such scenario, e.g. attacker could as well symlink ~/.profile or some startup script and make you write exploit code there. I bet there's a ton of other dangerous stuff in /proc.

std::fs and std::process are marked as "safe" in Rust, even though there are countless ways in which a program can do unsafe things with them. Policing of all filesystem access and system interaction is hard, and may be better left to other solutions, like chroot/jails/containers/VMs/sandboxes.

7 Likes

That is not my understanding. Rust strives to protect the programmer from silly mistakes with mis-matched types, memory use errors etc. All things that are expressible within the language.

To my mind people writing into /proc/pid/mem or /proc/self/mem are outside that scope. Not a Rust concern.

That leaves the question as to whether a process should be able to modify it's own memory through that means on Linux. I'm not sure but I suspect it is already possible as suggested above.

Is it really necessary? Why would one do that in ones code? Or if it's happening by some code hidden in a crate one is using one is compromised already.

A long time ago, Google had a project called Caja, Spanish for box, that had a goal of making JavaScript more secure. It translated any existing JavaScript program into one that was more secure. You can look up the papers on the project to find out what that means.

Their job was not complete until they "tamed" the library, a process that marked some functions as "unsafe" for use, and provided "safe" versions of others. It was a long, expensive process. I know because a friend of mine was hired to do it.

Protecting against such things as /proc/self/mem requires a similar process. It is not a job for the faint of heart or the inexperienced, but it can be done.

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.