11 ms·
Memory safety is necessary, not sufficient
- mannuch 3y agoI wonder what the author thinks about Swift’s new C++ interop story? Since the Swift compiler includes Clang, and can thus compile both your C++ and Swift into LLVM IR, without the need for an FFI later between the two, couldn’t this be the “Typescript for C++” that the author points out a space for? The Swift folks are very much thinking about Swift as a C++ successor that can be incrementally migrated to so I’m a bit surprised the author didn’t discuss it further — especially given Swift’s spiritual similarities to Rust. There’s a couple great talks on this by folks on the Swift team. John McCall at CppNow https://m.youtube.com/watch?v=lgivCGdmFrw https://m.youtube.com/watch?v=lgivCGdmFrw Konrad Malawski at StrangeLoop 2023 https://m.youtube.com/watch?v=ZQc9-seU-5k https://m.youtube.com/watch?v=ZQc9-seU-5k
- steveklabnik 3y agoI do not know a ton about it. Thanks for the pointers. I kept up with Swift more in the old days, but it doesn't seem to have gained a ton of relevance outside of Apple platforms, which I don't develop for. Doesn't mean that I think that it's bad, just that that's why I haven't spent a lot of time with it yet.
- mannuch 3y agoFor sure! Yeah I think a lot of people don’t realize that there’s actually a ton of really interesting work going on in Swift that is very relevant to the current conversation around languages these days (i.e. memory safety, data race safety, performance, development experience, etc.) The cross-platform story is getting quite good with Swift, but I think a lot of people just aren’t aware of it. Linux support is good and Windows is getting better and better. For example, The Browser Company is betting big on using Swift for its Windows app and talks about why they are doing it https://open.substack.com/pub/speakinginswift/p/interoperability-swifts-super-power?utm_campaign=post&utm_medium=web https://open.substack.com/pub/speakinginswift/p/interoperabi...
- humanrebar 3y agoSwift is an interesting option, though that interop requires dropping support for non-clang toolchains, and I don't see that happening any time soon. I mean, it makes sense for code that exclusively targets Apple platforms, and maybe that's all that Apple and the Swift team care about?
- fweimer 3y agoI agree with the sentiment. Consider a hypothetical variant of Python that requires using eval() (with the same behavior as in Python, so full Python expression support) for converting from strings to integers or floats. Or that implicitly calls eval() on list subscripts, to turn strings into integers. None of these changes impact memory safety, but it still makes it much more likely that common code has security vulnerabilities. (There is actually a widely used programming language with the eval-on-subscript feature …) > While a Go program may exhibit what a Rust or C++ program would consider undefined behavior, and it does also consider it an error, the consequences are very different. You don’t get time travel. You get 998 instead of 1,000. This isn't correct. Data races on multi-word objects of built-in type, such as slices and interfaces, actually have undefined behavior, in the sense that array bounds checking and type checking may break down. Russ Cox's old example still works if you disable optimizations: https://research.swtch.com/gorace https://research.swtch.com/gorace It looks like some form of dead store elimination happens to eliminate the data race with current compilers. For now, it's possible to bring it back by adding a pointless atomic operation, like this: go func(){ for !done { k = i atomic.AddUint32(&global, 1) k = j } }()
- lambdaone 3y agoEnforcing 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 process, and build on it.
- steveklabnik 3y agoTo 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.
- lambdaone 3y agoThanks 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.
- dgreensp 3y agoI 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 language designer. Any vague notion of “unsafe” in the context of a modern language—like saying well, if there’s a syscall, maybe something unexpected could happen—doesn’t compare to the bad decisions made by C and C programmers/culture that affect us today because we still use C code in our things. The deep, idiosyncratic flaws of C trace back to “worse is better.” Few people remember or look up what “worse is better” actually meant. Wikipedia wrongly says it’s a “less is more” sort of thing, and some people think it’s about not being a perfectionist. But actually… actually actually, if you read the essay, it says “worse is better” means (paraphrasing) it’s more important that C compilers be easy to implement than easy to use. Also, it is more important that the implementation—or the design of the implementation—of a piece of software be simple than that it be correct. It is more important that it be simple than that it be consistent. It is more important that it be simple than that it be “complete” (for example, handle edge cases; it just needs to work in “most situations”). This is not just anti-perfectionism, it is an objectively terrible set of engineering values. But there were so many different kinds of computers and operating systems back then—you didn’t even know if a byte was 8 bits—that it helped a lot that C compilers could do whatever they wanted in many situations. And making it easy for C compiler implementers enabled C to spread far and wide. It was a very different world, and just being able to write in a higher-level language than assembly on a particular computer was a big deal.
- nullc 3y agouh. 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.
- angiosperm 3y agoNobody 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.
- atq2119 3y ago"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.
- Gehinnn 3y agoI'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.
- quelsolaar 3y agoLet 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.
- amelius 3y agoOur 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.
- zozbot234 3y agoJava can use SecurityManager to do this.
- prosody 3y agoIt's deprecated for removal.[0] [0] https://openjdk.org/jeps/411 https://openjdk.org/jeps/411
- kaba0 3y agoGraalVM has Isolates that can do this on a much more fine-grained level and with multiple languages (it can also run LLVM-languages now).
- mlinksva 3y agoI'd seen that https://www.graalvm.org/latest/security-guide/polyglot-sandbox/ https://www.graalvm.org/latest/security-guide/polyglot-sandb... is JavaScript-only at this point, I guess it must build on https://www.graalvm.org/latest/reference-manual/embed-languages/#polyglot-isolates https://www.graalvm.org/latest/reference-manual/embed-langua... and one could roll their own for any language? Is there a good example, doc, or is it even a thing, to use GraalVM Isolates to defend against Java supply chain attacks, nevermind other languages? I guess it might be possible, going by a comment on another thread https://news.ycombinator.com/item?id=38278131 https://news.ycombinator.com/item?id=38278131 but require careful construction of anything you'd want to have only the capabilities you pass to it? (Naive questions, apologies, I should really learn by trying it out instead!)
- kaba0 3y ago
- tptacek 3y agoI'm not sure I understand what this piece is trying to say about Python memory safety. Conventionally, in software security, Python is considered a memory-safe language. The piece makes the case that Python isn't memory safe when you FFI into a C library. But neither is Rust, nor is it when you use `unsafe`. What matters in both case is how little unsafe code you end up writing. Memory safety is a software security concern. You can squint and make it about reliability or resiliency, but the reason we talk about memory safety is (to a first approximation) browser vulnerabilities. The piece goes on to discuss data races. I'm a little keyed up on software security essays that bring data race safety into the discussion. I have a hard time not reading them as shibboleths for "Rust is the only safe language", which is manifestly false. The vulnerabilities endemic to memory-safe languages (logic and higher-level vulnerabilities like SQLI, metacharacter quoting, filesystem traversal, and cryptography bugs) are common both to languages like Python and Java and also to Rust --- the only super-common class of vulnerability endemic to languages like Python and Java that Rust avoids is deserialization (you avoid deserialization vulnerabilities by not building hypercapable serialization formats). Data races are a common source of reliability bugs. They're a meaningful software engineering concern. In exotic scenarios (userspace-sandboxed attacker-controlled code), they can constitute practical security vulnerabilities. But in the main, data races have not empirically proved out as a source of exploited vulnerabilities. If you have a fixed budget to transition from a C codebase that would allow you to migrate to Python now, or if you saved up, to Rust next year, and all you care about is security, then ceteris paribus you should do the Python thing. The data races aren't going to burn you.
- tedunangst 3y agoAre you grouping kernel exploits in with user space sandboxes? Lots of local roots come from data races which I would not call exotic. And there's always https://portswigger.net/research/smashing-the-state-machine https://portswigger.net/research/smashing-the-state-machine for web stuff.
- tptacek 3y agoRight, these aren't data races; they're distributed systems races, more akin to tempfile races from the 1990s than to memory corruption.
- LispSporks22 3y ago> coming legislation around MSLs for government procuremen What’s he talking about here?
- steveklabnik 3y agoI link to them in the post, as well as discuss exactly how I came across them.
- tempodox 3y ago> threads.into_iter().for_each(|t| t.join().unwrap()); What a great title for the almost-last section, it made me laugh. When I saw the title of the final section, I fell from my chair: > int atexit(void (*func)(void))
- steveklabnik 3y agoThank you!
- keikobadthebad 3y ago[flagged]
- weinzierl 3y agoNot sure what to make of the first part of your comment. I read the article differently and it definitely had some interesting points and is certainly more nuanced than you make it appear. Regarding your last sentence: I don't think Steve Klabnik has a newsletter and the blog post does not annoy anyone with a call to action in any shape or form.
- weinzierl 3y agoRegarding "Now, I am not a politican, and these bills are huge, so I wasn’t able to figure out how these bills do this specifically [..]", I think the relevant reference is on page 864 of [1]: "SEC. 1613. POLICY AND GUIDANCE ON MEMORY-SAFE SOFT- WARE PROGRAMMING. (a) POLICY AND GUIDANCE.—Not later than 270 days after the date of the enactment of this Act, the Secretary of Defense shall develop a Department of Defense wide policy and guidance in the form of a directive memorandum to implement the recommendations of the National Security Agency contained in the Software Memory Safety Cybersecurity Information Sheet published by the Agency in November, 2022, regarding memory-safe software programming languages and testing to identify memory-related vulnerabilities in software developed, acquired by, and used by the Department of Defense." The mentioned "Software Memory Safety Cybersecurity Information Sheet" is probably [2] which explicitly lists "C#, Go, Java®, Ruby™, Rust®, and Swift" as examples for memory safe languages. I'm still looking for the equivalent EU document and would be grateful for any hints. EDIT: I could not find any reference to memory safety in any of the EU documents but interestingly the "Impact Assessment Report" [3] mentions Rust and Go specifically. [1] https://www.armed-services.senate.gov/imo/media/doc/fy24_ndaa_bill_text.pdf#page=864 https://www.armed-services.senate.gov/imo/media/doc/fy24_nda... [2] https://media.defense.gov/2022/Nov/10/2003112742/-1/-1/0/CSI_SOFTWARE_MEMORY_SAFETY.PDF https://media.defense.gov/2022/Nov/10/2003112742/-1/-1/0/CSI... [3] https://ec.europa.eu/newsroom/dae/redirection/document/89545 https://ec.europa.eu/newsroom/dae/redirection/document/89545
- steveklabnik 3y agoThank you! I really appreciate it.
- zelon88 3y agoI don't think the government's goal in improving memory safety is because of Rust or any other particular technology. The government buys technology from wherever, and until recently they never really cared where they got it from. If they need a USB Emulator, they buy the same Chinese Gotek from Ebay that you or I buy, and they get the same "Driver CD" full of buggy, broken English software, written by one person for $27 over the course of about 7 hours. Same thing with DVR systems, access controls, network appliances, access points. I've worked on government projects that were using 20 year old, broken, encryption libraries on active web servers. And when brought to their attention the people who use it don't care, and the people who are capable of doing something about it will never find out because it's too much work to replace and nobody wants to take that Zoom call. I think the author is getting a little over-excited about Rust. Granted, they are a Rust developer. I'd love it if we could build everything in my favorite language. But the government doesn't want to replace everything with Rust. The government wants results. Period. It is the DOD, NSA, and CISA who need to become experts on how to realize those results. And they will probably release guidance that says "if you can't program securely in a memory unsafe language, don't use a memory unsafe language". The thing I think the author is missing is that nobody is going to mistaken "Made with RUST!" for "Impossible to cause undefined behavior!" You can be a terrible programmer and still create undefined behavior, insecure code, data leaks, or any number of other problems in a memory safe language. What the government is going to do is implement strict code import controls, similar to the export controls we already have. Purchasing departments will have to get software purchases signed off multiple times by qualified parties, and department leaders will have to prove that the technology they procure is adequately safe. In some situations I anticipate that means re-writing a bunch of code in Rust, and in other situations I'm sure that means hardening existing C. The majority of the government's security holes that need to be patched are coming from low quality unvetted imported technology, low quality self-written code, and code that should have been replaced 20 years ago. While it's exciting to think that this could mean a renaissance for the author's technology of choice, I just don't see it working out that way. If the government could write memory safe code, it would have. Rust isn't safe ENOUGH to save the government from itself alone.
- marcosdumay 3y agoJust like in every other kind of engineering, for software capability pulls the requirements. The only reason everybody is complaining about lack of memory safety now is because there are alternatives for every use case. Before Rust existed, it was seen as an inevitable issue that one must work with, not as a problem to solve.
- pornel 3y agoI 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 around the program. It's possible to build walls around it to contain it completely. In Swift + unsafe or Java + JNI I've struggled building equally solid abstractions around FFI. They don't have "you can call it only once" or "you can't call go() after finish()" as a compile-time check. They don't have "you can use it only within this scope" (they may use a closure/callback to give access to an unsafe object, but these aren't hermetic, so that's a convention not a guarantee). Exposing objects to Swift or Java requires the higher level language to be in charge of their lifetime.
- mplewis9z 3y agoI 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.
- lmm 3y ago> They don't have "you can use it only within this scope" (they may use a closure/callback to give access to an unsafe object, but these aren't hermetic, so that's a convention not a guarantee). You can use the same trick as Haskell's STRef to prevent reusing a "leaked" unsafe object, although it's cumbersome enough in Java that you may not want to.
- mananaysiempre 3y agoThe ST monad trick requires second-order polymorphism, though [to type runST :: forall a. (forall s. ST s a) -> a]. Are any of those languages capable of it?
- saghm 3y agoThe part about potential C++ "successor" candidate languages had this interesting tidbit: > This leads to four approaches: > * We do not break backwards compatibility, and we ignore the memory safety issue (this is the ‘do nothing’ option). > * We do not break backwards compatibility, but we try to make things as memory safe as possible given the constraints we have. > * We break backwards compatibility, and we try to make things as memory safe as possible but still remain unsafe. > * We break backwards compatibility, and we become memory safe by default. > The first option is seemingly untenable, but is also where I see C heading.... > The second option is how I view at least one of the possible C++ successor languages, Circle..... > The third option is where the most famous successor languages live: Carbon and cpp2.... > But what about a fourth option? What if someone did a TypeScript for C++, but one that was closer to what Rust does? You might argue that this is basically just Rust? (Interestingly, it is also sort of what Zig is with C: you can use the same compiler to compile a mixed codebase, which is close enough in my opinion.) What makes me worry about option #3 is that it doesn’t pass the seeming test that is coming: memory safe by default. I still think moving to a #3 style solution is better than staying with #1, but is that enough? I don’t know, time will tell. When put that way, I can't help but wonder about approach #3; if you're already going to break compatibility, why not go the whole way and be memory safe by default? Even as someone who really likes most of the design decisions that Rust has made that are orthogonal to memory safety, and there's a lot of room for differentiation here. Offhand, some of the features of Rust (and its ecosystem) that I've seen people express a desire for alternatives on include: * package management (centralized repos, lack of namespaces) * scope of stdlib (relatively batteries-not-included e.g. regex, randomness APIs, http clients, async runtimes all being third-party) * error handling (lack of exceptions, special operator to early return, somewhat prone to boilerplate like Ok-wrapping without resorting to heavy use of macros) * integer semantics (no variable width or decimal type in std, overflow checked in debug but allowed in release mode, no implicit casting but lossy explicit casting allowed) * type definitions (structs/enums with traits, no inheritance, reliance on the "newtype" pattern for certain things) * function definitions (required type annotations for parameters and return values no overloading, no optional parameters, no variadic arguments) * syntax/readability (ML-style type annotations, immutable by default, postfix operators like ? and .await, use of macros instead of functions for printing/formatting strings) * concurrency design (built-in support for locks and 1:1 OS threading, de facto need for third-party runtime to use async, need for 3rd-party libraries for channels other than the deprecated ones in std, no "structured concurrency") * opinionated APIs for allocations (heap pointers constructed via generic type wrappers, panicking by default if allocation fails, non-trivial boilerplate required to swap out allocator) * linking (statically linked dependencies that don't use FFI, dynamically link to system's libc by default) I have to cut myself off here because I keep thinking of more stuff as I write this list, and I probably wouldn't run out of ideas any time soon. I think there's plenty of room for design space for a systems language that's memory safe by default but doesn't look or feel anything like Rust by making different choices on some or all of the above concepts, and that's without even getting into the fact that the unsafety boundary itself can be defined in a variety of different ways, a point that the blog post makes after contrasting the way Java, Go, and Rust expose the ability to do "unsafe" programming: > If we think about all of these designs, they all are very similar conceptually: safe code is the default, but you can call into some sort of unsafe facility. And everyone is very clear on the relationship between the two: while the unsafe facility exists to be used, it must uphold the rules that the safe world relies on. And that means that safety and unsafety have a super/sub-set relationship. In the core is unsafety. But at some point, we draw a line, and on top of that line, safety exists.