Live data from Hacker News

Memory safety is necessary, not sufficient

steveklabnik.com

21–30 of 162 posts

Re: Memory safety is necessary, not sufficient

#21

Nobody has been able explain to me what would be lost if we defined data races to yield one of the values that had been written to the memory in the past, instead of being undefined. It is not as if any optimizer can see that you are racing and delete the code path that has it.

"One of the values written to memory" isn't really a thing when memory accesses can tear (e.g. because the values are 16 bytes large and are accessed using 8 byte memory ops). So it's not even necessarily about what would be lost, it's about what can be reasonably defined in the first place.

If you restrict yourself to relaxed atomic loads and stores -- i.e., memory accesses that are atomic in the sense that they cannot tear, but don't have much in the way of ordering guarantees -- then you do get "one of the values that had been written to the memory in the past".

Aside from memory tearing, one other issue is that you generally want to be able to rematerialize loads (i.e., in the face of register pressure, you may want to turn one load of a value into multiple loads instead of loading it once, then spilling to the stack and reloading from the stack). But when the compiler rematerializes a load, then you don't get "one of the values written to memory". From the perspective of the original program it looks like you get some weird superposition of values.

Re: Memory safety is necessary, not sufficient

#23

Nobody has been able explain to me what would be lost if we defined data races to yield one of the values that had been written to the memory in the past, instead of being undefined. It is not as if any optimizer can see that you are racing and delete the code path that has it.

I'm pretty sure that you have either an undecidable problem or a non deterministic piece of code that sometimes computes some value, other times a different value, depending on how the threads are scheduled. Neither is good.

Re: Memory safety is necessary, not sufficient

#24
post #6

I think it's underappreciated that Rust's `unsafe{}` doesn't exist in isolation. Rust has facilities for building safe abstractions on top of it, and has a culture of taking this abstraction layer seriously. Danger of unsafe features and FFI is usually conditional — you can use a pointer only until some point, or only on a single thread, etc. A use of unsafe in Rust doesn't become "be careful!" kryptonite spreading a…

I haven’t had a chance to fully explore the new features and there are probably still some sharp edges, but the addition of non-copyable types and borrowing/consuming bindings in Swift 5.9 should bring it a lot closer to Rust in those respects, especially the hermeticity aspect. If you haven’t experimented recently, might be worth doing - this is also one of the big focuses of the language in the near term, so there should be lots more progress coming too.

Re: Memory safety is necessary, not sufficient

#25

Enforcing memory safety is good thing, even if it's not perfect; it's the first stage in the long-needed move from throw-it-in-a-bucket-and-hope-it-works "software engineering" toward proper formal-methods-driven actual software engineering. I feel complaining about it as insufficient is not the ideal way to push things forward. Instead, let's treat the progress on memory safety policy as a first victory in that proc…

To be clear, I absolutely do as well. I'm wondering about other possibilities, but I am ecstatic that this all is going as well as it is.

Re: Memory safety is necessary, not sufficient

#26

Nobody has been able explain to me what would be lost if we defined data races to yield one of the values that had been written to the memory in the past, instead of being undefined. It is not as if any optimizer can see that you are racing and delete the code path that has it.

Let me _try_ to explain it.

One reason is that non-atomic writes can be torn. So if you have a value like 0x00000000 and over write it with 0xFFFFFFFF, some hardware may do it as two separate writes. This means that another thread can read the data when its half way written and get 0xFFFF0000. I'm using a 32bit value here to illustrate In reality modern hardware is unlikely to tear it, but in other cases it may.

Another issue is that what you are writing may depend on something else. Consider this code:

x = 42; p = &x;

these two operations are independent, and a compiler, CPU or memory system could chose to execute them in any order. This means that even if the second operation is "atomic" and another thread can only read the before or after value of p, another thread could read p and access it, before 42 is written to x. This is why the p = &x; needs a "release" barrier that guarantees that everything before it is completed before the change to p happens.

Atomics are complex due to the way modern computers optimize and this is just a very surface level explanation. Still, I hope this helps.

Re: Memory safety is necessary, not sufficient

#27

I think it’s worth emphasizing that the C spec’s love of undefined behavior—if you do X by accident, anything can happen—and the apparently massive amount of memory-unsafe software that has been written that will just allocate 16 bytes on the stack and then read from a file descriptor until it encounters a null byte… are examples of things that aren’t considered remotely sane or reasonable to a modern programmer or l…

uh. No. Rust unsafe gives rust behavior a lot like C. If you at all break the rather subtle rules, then essentially anything can and will happen.

So for example, there was recently a thread where someone had code that checked if a value was in range to safely coerce it directly to an enum then did so. But because of eager evaluation of an argument the unsafe cast happened first. From this the compiler reasoned that the variable was preconditionally range constrained to always be in range and it optimized out the in-range test (which itself was not unsafe code).

This is a classic C bug where someone implements an overflow check that itself can overflow, causing the branch for overflow to get optimized out. But at least in C the simpler syntax at least made it clear that the triggering code got executed first. The more complex rust syntax obscured that.

Rust has improved the situation by narrowing the cases where you can get into this trouble, but on the other hand it adds a lot of other complexity that contributes to faulty code (and a nearly mandatory packaging ecosystem which is a security nightmare-- it's the norm for even simple rust utilities to pull in a million lines of unauditable (just by bulk) third party code, including multiple HTTPS libraries).

As a result, I don't think it can be taken for granted that rust as a whole is an advancement in software integrity-- it may be, but it's something that ought to be formally studied. In some cases rust might be replacing memory safety bugs with an even greater number of other defects which, depending on the application, may be worse. (not everything is an internet exposed service where hacks are the only failure of consequence and where input really should be assumed to be intelligently adversarial.)

In any case, "break the rules and all bets are off" is an issue that likely will continue to exist in any performant language. Automatic code generation will generate stuff with awful performance unless an optimizer goes through and eliminates 'impossible cases', but optimization isn't possible unless the compiler can assume the rules are followed.

Re: Memory safety is necessary, not sufficient

#28
post #22

Our programs are growing so big by having so many (indirect) dependencies that we need a way to sandbox the libraries that we include from our main programs. This is the type of safety that I'm looking for, really.

Java can use SecurityManager to do this.

Re: Memory safety is necessary, not sufficient

#29

I think it’s worth emphasizing that the C spec’s love of undefined behavior—if you do X by accident, anything can happen—and the apparently massive amount of memory-unsafe software that has been written that will just allocate 16 bytes on the stack and then read from a file descriptor until it encounters a null byte… are examples of things that aren’t considered remotely sane or reasonable to a modern programmer or l…

It's not like PL/I didn't allow for similar flaws, so "Worse is better" is neither here nor there. At least C was simple enough to learn for most programmers. It's still generally the case that a simple approach is more likely to be correct than an overly complicated one for which it's not even clear what "correct" means.

Re: Memory safety is necessary, not sufficient

#30

Enforcing memory safety is good thing, even if it's not perfect; it's the first stage in the long-needed move from throw-it-in-a-bucket-and-hope-it-works "software engineering" toward proper formal-methods-driven actual software engineering. I feel complaining about it as insufficient is not the ideal way to push things forward. Instead, let's treat the progress on memory safety policy as a first victory in that proc…

To be clear, I absolutely do as well. I'm wondering about other possibilities, but I am ecstatic that this all is going as well as it is.

Thanks for all your work on Rust! If I had the option to choose just one small thing to go after next, it would be well-defined behavior and error handling for integer overflow and underflow in languages without bignums.
Post reply on HN