Bug still unresolved since 2015 (cve-rs)?!

This carries a really key point about Rust's safety guarantees; they're not about allowing you to use untrusted code without risk, but about preventing you from making the sorts of mistakes that we know developers make and that are hard to debug.

In other words, it's not about "can I use this code from the enemy's spy agency without worrying?", it's about "can I use this code I wrote 6 months ago (and thought was bug-free then) that looks right without worrying about subtle bugs?".

2 Likes

That seems to be the state of the art.

I like to think though that if I obtain untrusted Rust source code from some enemies spy agency and if I disallow "unsafe" when compiling it. Then the checks Rust puts in place render it harmless. It can not perform I/O that I don't allow, it cannot get hold of memory I don't allow and so on. I should become as harmless as Javascript, except that it all compiled in one go rather than interpreted or JITed.

Give the topic of this thread and whatever other bugs there may be in Rust's safety system I guess we are not there yet.

However there is at least one operating system that has been built in Rust based on that premise. It uses the languages safety features rather than hardware memory isolation between processes etc. Sorry I can't recall what it is called or who developed it.

std::fs::File::create("/foo.txt") is and always will be a safe IO operation. Therefore, with std as it currently stands, even if all bugs were fixed, you cannot get this property from the things Rust offers.

However, there is a way to achieve that, which is closely related to and supported by memory safety: capability security. This is the paradigm that a program or even an individual function may not have an effect on the outside world (discounting energy consumption, wall-banging side channels, etc) without possessing a capability (a sort of handle/object/pointer) that both designates some outside resource that could be affected, and is necessary and sufficient to communicate with it.

Using a programming language with memory safety enables structuring individual functions in a program to operate in this manner, not just programs interacting with the operating system โ€” but that's not sufficient to achieve security, because you still have the possibility of global mutable state and functions which act on the outside world without taking any capability inputs. cap_std is an attempt to write a version of Rust std APIs that follows capability principles, but to actually benefit from it in the way you propose, you'd need to mechanically audit code to prohibit use of std (though not core) and many other libraries that indirectly expose std, as well as fixing all Rust soundness bugs. This is theoretically possible, but a lot of work.

All that said, certainly Rust's memory safety rules make a program's behavior easier to reason about. It's just not in the same league as โ€œcan run untrusted code without a process-level sandboxโ€, which a capability system can offer.

2 Likes

I don't see that as an issue with the Rust language. As the creator of the operating system I get to say what std::fs::File::create("/foo.txt") and other such operations do. Which by default may be to simply halt with an error at that point. Or provide an implementation that looks like a file system but is not "the file system". A sand box.

Same applies to another I/O your code may contain.

In short I see no reason why my OS could not be that capability system you are describing. No need for any hardware process isolation or other traps.

To be clear what I am describing relies on the OS only accepting Rust source code as means of getting code into it. Not any old binaries.

I don't see that as an issue with the Rust language. As the creator of the operating system I get to say what std::fs::File::create("/foo.txt") and other such operations do.

Ah, okay, I misunderstood you to be talking about using Rust on a conventional operating system.

In short I see no reason why my OS could not be that capability system you are describing. No need for any hardware process isolation or other traps.

Indeed, that's theoretically possible. You still need distinct memory regions per process in order to clean up leaked allocations and hung threads after process termination, and to ensure that each process has its own static state.

(In a true capability-language system, mutable statics of any kind, including interior mutability, would be absolutely prohibited, and at that point the distinction between โ€œa processโ€ and โ€œa part of a processโ€ starts to become less important.)

This sort of design hangs a lot of weight on โ€œthe compiler has no soundness bugsโ€, though. There are practical systems that rely on this property for virtual machines with JIT compilers (your web browser is one), but I'm not aware of any currently-in-use systems that rely on ahead-of-time source-to-machine-code compilers to have this property.

1 Like

Do you mean Redox OS ?

Sooner or later Rust will have to take this step, otherwise languages inspired by the logic of Rust with the flexibility of GCs will emerge, and it will be too late (e.g. Nokia).

I share your point of view on many points, but for me the notion of "Unsafe" must be well defined:

I'll give you some examples, here are some toxic cases of "unsafe" use :

  • Someone who doesn't have a good grasp of Static/Mutex, and to resolve compiler errors, chooses to use "Unsafe" to unblock the situation
  • The use of Unsafe for performance reasons, to avoid Box/Rc/Arc

and then there's the legitimate reason for using "Unsafe", such as :

  • Calling an external library (C)
  • Access random memory areas, but usually this is either to create a debugging program or a virus :thinking: .
  • etc..

for me, unsafe = unpredictable, (if behavior is predictable, it's no longer Unsafe)

Having a program with parts that can crash randomly without warning, without being able to manage the exception... it's scary.

Can you imagine sitting in an airplane with a system containing "unsafe" parts that, if by some misfortune the program goes into exception, the whole system shuts down, and I'm not telling you what happens next?

In my opinion, "Unsafe" should only be used for external calls and not in the program logic itself.

When you make an IO call, you have two scenarios, OK (Ok) or KO (Err), so you can handle both cases.

Unsafe sometimes produces code that can't be matched with "Result" to keep with Rust's logic.

I give examples:

Dangling pointers : even if the data it's targeting no longer exists, the memory area still contains data, so it will continue to work, but will bring back random data

Cast different types based on pointers only : Convert Type A -> Type B without the compiler's blessing :laughing:

More generally, operations that occur at runtime, not at compile time.

Unsafe is very useful, but lacks definition for my taste, perhaps it should be split in two:

  • Unsafe (unmanaged by borrowing rules , probable data inconsistency )
  • Unpredictable

it's very philosophical :laughing:

Rust should allow you to do everything you can do with GC languages without ever having to use Unsafe, and keep Unsafe only for external calls (Lib C, Interrupts).

[there are a wide variety of JAVA applications, with no pointers in the code. (I'm not talking about RNI, although it does allow you to create a point between Unsafe and Safe separate from the application's main code)]

and categorizes crates into Unsafe Crates and Safe Crates.

If memory serves, the QT framework (C++) has a technology for embedded systems that virtualizes the risky part of the code so that in the event of a crash, only the part concerned is blocked without impacting the critical parts (for example, the interface of the car's control panels).

1 Like

Having worked at Nokia in the years before they went down I would really like you to explain what that business failure has to do with programming language design.

Hmm...perhaps. In the Rust world "unsafe" just means the compiler is not going to do a bunch of checks for you. You have to do them yourself. Given the definition and rules of the language, including "unsafe" parts, I don't see how that makes anything unpredictable.

Having spent some years working on avionics, including the Primary Flight Computers of the Boeing 777, I can assure you that the possibility of a program "going into exception" or otherwise misbehaving is anticipated. The 777 for example had three boxes called PFC's. Each contained three different processor architectures, running code built with three different Ada compilers. If anyone of those combinations of hardware and compiler disagreed with the majority it was "voted of the island" as it were. Bottom line being that the compilers and the hardware running the code was deemed "unsafe".

You are forgetting many other scenarios:

  1. The I/O call never returns.
  2. The OS decides to terminate your code on that I/O call.
  3. Something in the OS causes it to corrupt the data or program memory of your program.
  4. Etc.

Of course it is not just the OS that can do this, could be the hardware itself. The Rust language cannot prove anything about any of this, ergo "unsafe" is required to indicate that fact.

5 Likes

I understand the goal you're describing and this could apply to a language with a different design, but it it is not Rust's definition of unsafe. And what unsafe means in Rust isn't likely to change very much. Writing unsafe code in Rust is specifically needed to create memory safe abstractions such as those in the std library but also those in other crates. This part of Rust's design is fundamental.

2 Likes

Maybe. Nothing in the same lane as Rust has appeared yet, or at least, nothing I've heard about.[1] And you may be underestimating how much work it is to get to the point Rust is already at; @kornel put it well here. It's not impossible Rust will fail due to not going 2.0, but I think it's more likely there's never a Rust 2.0 and instead Rust holds on to more complexity and niche-cases than is ideal, ala C++, but still sticks around.

I'm not sure what you had in mind with the GC comparison. But being intrinsically GC isn't Rust's "lane".

I could see a language similar in some ways but in an adjacent lane (with no unsafe, etc) taking away some mindshare. Arguably they already exist. Some people consider Go and Rust rivals, say.

It's definitely used for that.

That's more a matter of regulation and fault-tolerance more generally. They also have to run in "real time", which is a harder bar for GC languages to meet. You can't just pause the world arbitrarily.

I'm not an avionics engineer, but according to this the big three languages are C, C++, and Ada.

So it's not "written in language X, wings stay on; language Y, wings fall off". If you've been in a commercial airplane, you've probably sat in an airplane with safety critical software written in something with less guardrails than Rust.

The ecosystem does need better/more review and curation. Deciding how and by whom is a large topic.[2]

Given your answer, it sounds like you trust upstream to be non-malicious, but not necessarily to write sound unsafe code. That's a fair stance. There are some tools for it.

If you need dependencies for an async framework or base level data structures[3] or the like, you'll probably find some uses of unsafe. Ultimately you have to trust someone, unless you can afford to write it all yourself.


  1. I've seen a few claims which, upon investigation, proved to be bunk. โ†ฉ๏ธŽ

  2. AFAIK the same is true of any modern dependency-manager based language. Well, any language really, but the ease of pulling in a transitive forest of dependencies via crates.io or npm or what-have-you makes it an order-of-magnitude larger issue. โ†ฉ๏ธŽ

  3. including the ones in std โ†ฉ๏ธŽ

5 Likes

Rust is a product, Nokia phones are products, and what was the main reason for Nokia's downfall? It was their determination to keep a mechanical keyboard on their phones and not dare follow the trend, as Apple did with the Iphone.

So a conceptual choice swung Nokia from cell phone leader to second-rate nostalgic brand..

Rust came up with a proposal for a design that existed long before, and with years of experience, the solution proposed by Rust has reached its limits, so it's time to break with the legacy and propose another way of doing things.

I'm not an expert in language creation, but I'm sure there's a simpler way of doing things, it's just a question of finding it. But to find it, you have to start by cutting the bridge with what already exists, so that you can explore new horizons.

Either Rust is Safe or Unsafe, but it can't be both. If it allows you to make Unsafe code, then it's an Unsafe language.

10 * 1 = 10
10 * 0 = 0
1544 * 0 = 0
(0 = unsafe)

For example, Crate.rs needs to tag crates containing unsafe code with an UNSAFE badge,
to force developers to produce safe code if they want better visibility. And allow the crate user to be warned.

I'm not criticizing the language, I'm criticizing the false impression of security it provides.

What you're describing here is the principle of redundancy: whether the program is Safe or not, any system must always be deployed with hardware and software redundancy mechanisms.

You're going to create redundancy on a program that's going to generate a Panic! on all the nodes? You're just replicating the same problem on several instances.

I agree with you, but the purpose of the standard library is to offer an abstraction, as Rust's library already does: write operations return a Result, so you can handle exceptions.

So in the end, if Rust offers a standard library with all the basic tools to create everything without having to use Unsafe, and offers functions that use Unsafe code internally but encapsulated in Result or Option returns, that's already enough.

We're back to the same thing: Rust needs to force its ecosystem to be Safe, and offer sufficient flexibility so that developers don't have to use Unsafe code directly.

I 1000000% agree with you, unsafe in STD's internal code is legitimate, in the development of interfaces with external libs too, in pure system programming but only on condition that you offer mechanisms that allow the consumer code to handle both OK/KO scenarios.

I know it's difficult, but for example, propose a methodology that imposes the separation of safe and unsafe code in two different crates, and that unsafe code must always be encapsulated in a function that returns a Result, thus obliging the consuming crate to manage the exceptions.

A language has to be simple, and Rust is becoming a "Language Framework" :laughing: , as it becomes so complex over time. (just look at the Async in Traits + #[trait_variant::make])

I share your point of view, but when it comes to the simplicity of GC languages, we can take Go as an example (even if I don't like its syntax), but it's close to Rust while being much simpler, it just lacks the +20% performance that Rust has.

but you're right, the example of a GC language isn't appropriate. It should be a mix of Rust and Zig, perhaps. :thinking:

Crates that are obliged to use Unsafe, must be limited and framed, let's take the example of ActixWeb (which I love by the way), there are many alternatives that do the same thing without using Unsafe directly.

Complicated language + Uncompromising compiler = massive use of Unsafe/Unwrap shortcuts :laughing:

You can only touch the "language complexity" parameter to avoid Unsafe as much as possible.

Despite everything, I love Rust, and if I criticize a little, it's to help the language evolve from good to better :yellow_heart: .

I know it's easy to criticize, and finding solutions isn't easy at all, but I really thank the team and the community who are working hard to make Rust evolve.

I just realized that the topic of discussion was the bug related to lifetimes, we're totally off track :laughing: (but it was a really good debate) .

This is incorrect. Invalidly reading memory is worse than giving you random/arbitrary data: it produces inconsistent and incoherent data. E.g. if you do let x = ptr.read();, using x is often as bad and unpredictable as doing the pointer read, including that x == x may be false.

And that's only with "simple, straightforward" optimization and actual hardware behavior; the UB still means that literally anything could occur, including (but not limited to) time travel at a distance. And of course, writes can corrupt arbitrary other data, including your permitted unsafe that powers Box or Rc.

Yes, minimizing unsafe is good. No, demonizing unsafe is bad. Yes, using unsafe when there's no potential to cause UB by misuse is a bad idea. Yes, tightening bounds on what code is within trust boundaries makes soundness more composable. No, shaming non-std crates for utilizing unsafe doesn't improve anything. The beauty of Rust is seamless integration between safe and unsafe Rust; introducing friction for shifting dialects is essentially splitting the language into two. Maybe you want a "managed" "scripting" language and an "unmanaged" "systems" language as two separate concepts. Rust isn't that; Rust wants to empower anyone to do "systems" where that makes sense and "scripting" where that makes sense without barriers.

5 Likes

This is a great time to go back to 1984: https://dl.acm.org/doi/10.1145/358198.358210

There's a massive Trusted computing base - Wikipedia on which whatever you're doing depends, no matter what language you're using.

All* languages have unsafe, it's just that Rust does a better job of emphasizing it than most. Python is, in some ways, more unsafe than Rust because it doesn't require the annotation that you're doing something unsafe when you call out to C -- CDLL is just another method.

* Well, all the ones that allow you to call C, which is just about all the useful ones.

Remember that Rust is very specific about what it means when it talks about safety. It's like how we say that Rust prevents data races, but notably it doesn't prevent race conditions in general. Aborting (or panicking) is considered safe in Rust, because -- in the most trivial way -- not running is always safe, since it can't do anything unsafe.

3 Likes

It's clear to me that you want an entirely different language, just called "Rust". I suggest you to let go of the attachment to the name, as it doesn't matter, especially if what you want is a big departure from today's Rust. Instead look for new experimental languages, maybe one of them will have the features you're looking for (for example Hylo).

5 Likes

That seems like a reasonable logical deduction. A chain is only as strong as its weakest link and all that. The problem is that a totally "safe" language (I guess you mean one like Rust but with no "unsafe" blocks) is totally useless. One cannot do any I/O or call foreign code or make OS sys calls without "unsafe". Without any I/O a program can do nothing useful. One could not write an OS in such a totally safe language. Rust as a "systems programming language" should be capable of creating operating systems and low level libraries in. It should be useable on embedded systems with no OS underneath. It has to be able to do what C can do. "unsafe" is an essential part of Rust.

As I said elsewhere here recently, one should not conflate memory safety in the Rust sense of the word with insecure as in the current ideas of "cybersecurity". They are very different things. Memory safety will help prevent many memory misses bugs that could be exploited to subvert security. But it takes a lot more than that to ensure systems are secure. I don't believe there can be a false sense of security promoted by Rust anymore than say Python or Javascript.

1 Like

"compromised" in what sense?

unsafe { } doesn't actually mean "unsafe" (in the sense "I introduced a problem in this block of code"). There have been a few discussions over the years about what could have been a better name for it. Some have, in jest, suggested things like i_promise_i_checked_this_for_errors { }, to better illustrate what unsafe { } is meant for.

But more importantly, the argument "if a single unsafe exists anywhere in a code base, it implies that all the code is tainted" isn't a very useful metric for assurance evaluation. Context really does matter here; using unsafe { } in order to be able to read from a memory mapped hardware peripheral at a fixed address is vastly different than frivolously using unsafe to try to bypass the borrow-checker.

Even high-assurance evaluation frameworks used to evaluate "this could mean the difference between life and death" systems can allow context to be used in their argumentation.

2 Likes

Well, if any code in any crate tries to access an array out of bounds, in "safe" code, Rust will detect that and likely the program terminates. Should we then say that:

Since the majority of projects depend on external crates, and each crate depends on other crates, if a single crate makes an out of bounds array access, all the advantages of Rust are indirectly compromised.

I think not.

1 Like

And in that context, Rust's split into Unsafe and Safe Rust is helpful; it reduces the amount of code you need to check to ensure that there's no memory unsafety from "all of the codebase" to "unsafe blocks plus any code that has access to break guarantees that the unsafe block depends upon (dependencies of unsafe blocks, plus the module it's in)".

This is huge for auditing your trusted computing base (TCB) in a real program; instead of the TCB for memory safety being "all code in your program, plus everything it depends upon, including other code and hardware", it becomes "hardware, all unsafe blocks in your program, plus everything that those unsafe blocks depend upon". Being able to eliminate huge chunks from consideration trivially makes it much simpler to do the audit - there's just a lot less code to read.

3 Likes

No, Redox OS is a regular microkernel. @ZiCog was likely thinking about GitHub - theseus-os/Theseus: Theseus is a modern OS written from scratch in Rust that explores ๐ข๐ง๐ญ๐ซ๐š๐ฅ๐ข๐ง๐ ๐ฎ๐š๐ฅ ๐๐ž๐ฌ๐ข๐ ๐ง: closing the semantic gap between compiler and hardware by maximally leveraging the power of language safety and affine types. Theseus aims to shift OS responsibilities like resource management into the compiler. which is similar to Singularity and Midori from Microsoft.

5 Likes

BINGO! That the one. Theseus.
"How Safe Language OSes work, with Theseus OS examples ": https://www.youtube.com/watch?v=n7r8zO7SodE&t=1s

Sounds like it might be great for micro-controllers with no MMU. Wonder if it will ever get out of the thesis stage.

1 Like