13 ms·
Making C++ safe without borrow checking, reference counting, or tracing GC
- chris_wot 3y agoInteresting pigeon reference.
- gavinray 3y agoSee also: Thomas Neumann's current proposal for memory safe C++ using dependency tracking: - https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2023/p2771r1.html https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2023/p27... Google's proposal for memory safety using Rust-like lifetime analysis: - https://discourse.llvm.org/t/rfc-lifetime-annotations-for-c/61377 https://discourse.llvm.org/t/rfc-lifetime-annotations-for-c/... - https://github.com/google/crubit/tree/main/lifetime_analysis https://github.com/google/crubit/tree/main/lifetime_analysis
- pjmlp 3y agoAnd Microsoft' work on Visual C++ lifetime checker and SAL, as well. It will never be perfect, but every little improvement helps.
- Voultapher 3y ago> It will never be perfect, but every little improvement helps. Or it might convince people to stay longer on a plane with a provably [0] terrible safety record. [0] https://alexgaynor.net/2020/may/27/science-on-memory-unsafety-and-security/ https://alexgaynor.net/2020/may/27/science-on-memory-unsafet...
- pjmlp 3y agoTo put matters into perspective, Rust reference implementations depend on C++ toolchains. Same applies to all major Ada, Java, .NET, Swift, Ocaml and Haskell implementations. And any GPGPU toolchain. Which kind of shows it isn't going anywhere and those planes have to be improved no matter what.
- freeone3000 3y agoRust has been bootstrapped for nearly a decade. The rust reference toolchain is built in rust.
- detaro 3y agoif you pretend that LLVM and friends are not part of the toolchain
- pjmlp 3y agoSo no need for LLVM and GCC, Great news! Where can we download it?
- Voultapher 3y agoAssuming you are serious, there is https://github.com/bytecodealliance/wasmtime/tree/main/cranelift https://github.com/bytecodealliance/wasmtime/tree/main/crane... which is written in Rust and is targeted to become the default debug backend in rustc. LLVM has accumulated a lot of optimizations contributed by various groups and people over more than a decade. It's hard to catch up to that by virtue of resource limits.
- LoganDark 3y agoAbsolutely love to see CHERI mentioned here <3
- kubb 3y agoit's not about making C++ memory safe, but about describing a safe subset of C++
- pjmlp 3y agoIdeally we would have -fsafe and [[unsafe]], but it will take years for something like that.
- derefr 3y agoPresuming syntax for “unsafe” that gracefully degrades in non-aware compilers, why couldn’t a particular compiler start doing it right now, starting with a very trivial safety checker than can be iteratively improved upon once the framework is in place?
- pjmlp 3y agoYou can have it today on Circle, but its relationship with some C++ folks is complicated.
- bluGill 3y agoIt is easy to say add unsafe. However the details are very complex. I've read a few of the papers proposing something like this, and they spend a lot of time discussing some nasty details that are important to get right.
- eslaught 3y agoI feel like D has gone this route of incrementally adding features (like borrow checking) to the language that, in principle, improve safety. I wonder if anyone here has more experience to know how well it has worked? One massive advantage of Rust is that they started with borrow checking from the beginning. I think one thing that often gets understated in these discussions is how much it matters to have your entire ecosystem using a set of safe abstractions. This is a major drag for C++, and I suspect that even if the language went a route like D they'd still have gaping safety holes in practical, everyday usage.
- azakai 3y agoThere is also Type-After-Type: https://dl.acm.org/doi/10.1145/3274694.3274705 https://dl.acm.org/doi/10.1145/3274694.3274705 (though maybe that's covered by what the author meant by "arenas").
- verdagon 3y agoThis is one of the other three secret blends that I think could bring memory safety to C++! I wrote a bit about using type-after-type as the basis for an entire language (Arrrlang, with a parrot mascot) in my last article [0] and a little bit in a post about memory safety for unsafe languages [1] which we eventually talked about at Handmade Seattle. The downside is the extra memory usage, but I think we can combine it with temporary regions [2] to reduce it pretty drastically. TIL the phrase type-after-type! I've also heard it referred to as type stability. [3] [4] If you squint, this is what we often do manually with Rust when the borrow checker influences us to use indices/IDs into central collections. [0] https://verdagon.dev/blog/myth-zero-overhead-memory-safety https://verdagon.dev/blog/myth-zero-overhead-memory-safety [1] https://verdagon.dev/blog/when-to-use-memory-safe-part-1#the-safer-way-to-use-mmm-languages https://verdagon.dev/blog/when-to-use-memory-safe-part-1#the... [2] https://verdagon.dev/blog/zero-cost-borrowing-regions-overview https://verdagon.dev/blog/zero-cost-borrowing-regions-overvi... [3] https://www.usenix.org/legacy/publications/library/proceedings/osdi96/full_papers/greenwald/node2.html https://www.usenix.org/legacy/publications/library/proceedin... [4] https://engineering.backtrace.io/2021-08-04-slitter-a-slab-allocator-that-trusts-but-verifies/ https://engineering.backtrace.io/2021-08-04-slitter-a-slab-a...
- Voultapher 3y ago> Tracing GC is the simplest model for the user, and helps with time management and development velocity, two very important aspects of software engineering. > Borrow checking is very fast, and helps avoid data races. One thing many people seem to assume is that not having to care about memory means you can program faster and get to your goal faster. As the author here seems to do. However as it turns out, if your program is more complex than a ~100-1000 lines of code, explaining in a explicit way who owns what and who gets to change state when, is a very useful way to avoid bugs. Saoirse Shipwreckt aka withoutboats mentioned this a while ago in https://without.boats/hire-me/ https://without.boats/hire-me/ > Rust works because it enables users to write in an imperative programming style, which is the mainstream style of programming that most users are familiar with, while avoiding to an impressive degree the kinds of bugs that imperative programming is notorious for. As I said once, pure functional programming is an ingenious trick to show you can code without mutation, but Rust is an even cleverer trick to show you can just have mutation. and later follows up on this in https://without.boats/blog/revisiting-a-smaller-rust/ https://without.boats/blog/revisiting-a-smaller-rust/ > I still think this is Rust’s “secret sauce” and it does mean what I said: the language would have to have ownership and borrowing. But what I’ve realized since is that there’s a very important distinction between the cases in which users want these semantics and the cases where they largely get in the way. This distinction is between types which represent resources and types which represent data.
- liuliu 3y agoI don't write Rust. But here is what you said and what the author said don't conflict with each other, and it has been on my mind for a while. People who write similar code, or work on things for decades usually don't really think through what "sketch out some code" looks like. They spend most of their time on refactoring things that has clear use-cases, but not well-defined API boundaries within the component, or between components. So ownerships, nullability checks, data race checks are all comes very naturally as a starter. But there are other side of the world, where people constantly sketching out something, for things like creative arts, high-level game logic, data analysis, machine learning etc. Now putting yourself in that position, the syntax noises are actively in the way of this type of programming. Ownerships, even nullability checks are not helpful if you just want to have partial code running and checking if it draws part of the graph. This is a world Python excels, and people constantly complaining about why this piece of Python code doesn't have type-annotation. We may never be at peace between these two worlds, and this manifest itself somewhat into the "two-language problem". But that to me, is when someone mean "development velocity is faster".
- deleted 3y ago[deleted]
- coliveira 3y agoThe reason why safety in C++ is difficult to achieve is due to the memory model used by C and C++. The memory model is a flat space provided by the OS that can be addressed by pointers. In this sense, C++ is similar to assembly code. A language like Java, on the other hand, assumes a different model where you can only access objects with well defined behavior. To change this, one needs to disallow the use of native pointers in C++ or make them less powerful, like Java did.
- adamnemecek 3y agoYou just need an unsafe keyword.
- ajross 3y agoThat's pretty much what the article says though. "Don't use traditional pointers" is a fairly trivial rule to enforce via static analysis, and constructs like unique_ptr are syntactically identical anyway. The bit that has me confused is that it's inventing a new term, "borrowing affine style", to describe a longstanding paradigm that has traditionally been called "RAII". Now, neither term is very clear, but surely it's better to use the existing confusing jargon instead of inventing new terms.
- gpderetta 3y agoThe significant difference is a static guarantee of no reuse after move, hence the 'affine' qualifier (which is not new).
- bluGill 3y agoborrowing affine style is more than RAII. borrowing affine style means that there are no pointers, and always one owner. in borrowing affine style your functions take a unique_ptr for everything, if the lifetime of the data needs to live beyond the function, then the function returns a unique_ptr of that data back. std::unique_ptr<foo> var; // init and use var var = SomeFunction(std::move(var)); // use var again. Note that while in SomeFunction you lose access to var, but since SomeFunction returns it again you don't really lose anything. Of course Somefunction can also return some other unique_ptr<foo> that isn't var and you can't control that. It is an interesting idea, though I'm not sure if I like it for real world code or not.
- floor_ 3y agoUse memory arenas and never think about any of this again.
- spacechild1 3y agoHow do arenas prevent out-of-bound access, double free or stale pointers?
- estebank 3y agoOut of bound access is avoided because you ise handles that the arena has given you, creating an invalid handle is restricted. You avoid double free because of Rust's owbership semantics that make the arena itself reaponsible for "deallocation" (which is just blanking the value and letting Drop do its thing). You avoid stale pointers because every access is checked at runtime if you're using a generational arena.
- spacechild1 3y agoWe are talking about C++ ;-)
- estebank 3y agoYou're right, I was reading a sister thread that was talking about Rust and lost the plot.
- shadowgovt 3y agoSadly, untrue. Source: I use memory arenas, and it's still pretty trivial to copy (instead of reference) an object onto a stack and then try to save a pointer to that object. All you need is to leave out one `&` and the compiler won't tell you anything went wrong: it'll cheerfully let you retain a pointer to a stack-based object that is going to die because explicit lifetime analysis isn't a part of the language spec.
- latenightcoding 3y ago“Rule 4: When you want a raw pointer as a field, use an index or an ID instead.” literally just woke up but: wouldn’t it be simpler to use a pointer to a pointer, or am I missing something
- deleted 3y ago[deleted]
- corysama 3y agoYou might like: "Handles are the better pointers (2018) (floooh.github.io)" https://news.ycombinator.com/item?id=36419739 https://news.ycombinator.com/item?id=36419739
- nraynaud 3y agovery nice array of ideas to open the debate for us mere mortals.
- pizlonator 3y agoYou could also just isoheap according to type, where the type is whatever you come up with to make C++ casts sound. It could literally be C++ types or something looser (like if you want to say that bitcasting a int ptr to a float ptr is ok). Then you don’t need any language changes to make UAF type safe.
- winrid 3y ago> "We'll instead take and return the vector directly" Won't this clone it?
- rbancroft 3y agoNot necessarily, although it's a bit complicated to understand in C++. Starting with C++17, there is a feature called guaranteed copy elision that works for many/most scenarios that you would want. You need to read through the following resources to understand it fully: https://en.cppreference.com/w/cpp/language/copy_elision https://en.cppreference.com/w/cpp/language/copy_elision https://en.cppreference.com/w/cpp/language/value_category https://en.cppreference.com/w/cpp/language/value_category
- spoiler 3y ago> Not necessarily, although it's a bit complicated to understand in C++. One could say this statement applies to most lines of C++ code. Lol
- winrid 3y agoIndeed :) Makes me appreciate the explicit copy() and ref semantics in Rust. Although I bet in most cases such a method gets inlined so it doesn't matter.
- masklinn 3y agoCopy elision exists, the author might just assume (or know) it'll trigger. The rules are way too arcane for me so I could not tell.
- imtringued 3y agoRIP all the modern languages that haven't made any improvements in memory management at all. There is so much low hanging fruit in programming language design and nobody is picking it up and instead everyone produces marginal improvements over existing languages.
- antonvs 3y agoBecause implementing a new language and getting it to wide adoption is an enormously challenging task, with a much lower success rate than e.g. SV startups. Languages that try to implement one new bright idea don't go anywhere, because that's not enough to cause people to switch. At best they serve as examples for feature adoption in other languages. Look at Rust for example: it seems to be succeeding and gaining adoption, but right now it's still relatively niche (check the number of Rust job postings), and it's taken 17 years to get to this point, with sponsorship from major organizations like Mozilla. Given this, the idea that there's much low-hanging fruit that's being ignored, that could easily be exploited, seems dubious. What's an example of what you have in mind?
- kbenson 3y ago> it's taken 17 years to get to this point Yes and no. Rust went through quite a bit of changes early on, ro the point that it's not really that similar of a language, and 1.0 was released in May 2015. That's still quite a while (8 years), but IMO doesn't quite mean the same thing as a language that's been around for 17 years with a similar level of adoption. My impression (from the outside) is that Rust usage is still increasing, at least in specific areas, and has not leveled off or tapered. It doesn't seem to be exploding into lots of teams and places, but it does seem to be getting footholds still, like at Azure.
- tcmart14 3y agoWhile that is true about Rust, most new languages are gonna have the same thing. It'll be years before they get to 1.0. Look at Zig, just about every new language. So I don't think it is valid to discount the 1.0 days because all languages are gonna need awhile to get to the 1.0 day. It still took 17 years of time investment to get Rust to where it is today.
- pie_flavor 3y ago> Borrow checking is incompatible with some useful patterns and optimizations (described later on), and its infectious constraints can have trouble coexisting with non-borrow-checked code. Not that this isn't true, but the rest of the article introduces a system with a superset of those limitations, gradually decreasing over time but never becoming a subset. In fact the pattern described in the article is a common pattern in Rust and I make use of it all the time; the library for making use of it is `slotmap`.
- mr_00ff00 3y agoI would really love a definitive answer on whether the borrow checker and rust’s rules do really limit optimizations and such. It seems like I see this opinion often and every time there are tons of people on both sides who seem sure they are correct. What are the limitations for optimization? Does unsafe rust really force those?
- apendleton 3y agoWithout directly answering your question, it's worth noting that there are also additional optimizations made available by Rust that are not easily accessible in C/C++ (mostly around stronger guarantees the Rust compiler is able to make about aliasing).
- steveklabnik 3y agoThe question is far too broad, and contextual. You're never going to get an answer to that question. Sometimes, the rules add more optimization potential. (like how restrict technically exists in C but is on every (okay almost every) reference in Rust) Sometimes, the rules let you be more confident that a trickier and faster design will be maintainable over time, so even if it is possible without these rules, you may not be able to do that in practice. (Stylo) Sometimes, they may result in slower things. Maybe while you could use Rust's type system to help you with a design, it's too tough for you, or simply not worth the effort, so you make a copy instead of using a reference. Maybe the compiler isn't fantastic at compiling away an abstraction, and you end up with slower code than you otherwise would. And that's before you get into complexities like "I see Rc<RefCell<T>> all the time in Rust code" "that doesn't make sense, I never see that pattern in code".
- kbenson 3y agoMethods such as these for C and C++ are interesting, and needed, but only solve a part of the problem. As others have noted before, they do little good because they're opt-in. I think there's a bit of nuance to that which needs to be explored though, as I think it's less a problem that the extra checks are opt in, and more a problem of how we use and categorize libraries. As long as we encourage dynamic and static library inclusion (and why wouldn't we, it's how we build upon the work of others), every language has a problem similar to how C and C++ are opt-in and you can't easily control the code you include or link. If you load openssl from Java or Rust or Go, you might have some benefit from a well defined API layer, but ultimately you are still beholden to the code openssl provides in their library. Just as one of the real benefits of Rust or Java or Go is not necessarily that the code is completely safe, but that weird unsafe behavior usually requires special escape hatches which are easier to audit, what we need are ways to categorize the code we include, no matter the language it comes from, with appropriate labels that denote how strong the safeguard guarantees it was compiled with are and of which type, so we can make easier and better informed decisions on what to include and how to audit it easily when we do. This applies to including something written in Rust as well. If someone is writing something in C++ and wants to include a library written in Rust, that it's written in Rust is only part of the picture. It's equally important to how often (as a total and as a percentage of code) the safety checks that language required (or that the developers opted into) where escaped in that library. If the choice is a Rust library with 95% of the code in unsafe blocks or a C++ library that opted into multiple different safety checker systems and has almost no escapes from those requirements, Rust is not providing any real safety benefits in that situation, is it? What we need is better information exposed at a higher level to developers about what they're opting into when they use third party code, because we can all control what safety mechanisms we use ourselves, so that's mostly a solved problem.
- verdagon 3y agoThis is a great point, and one that doesn't get enough attention. The article talks about using a static analysis tool, but usage of that tool is indeed opt-in, like you say. I suspect a language could mitigate this with the ability to sandbox a library's code. That could be pretty slow though, but we could compile it to wasm and then use wasm2c to convert it back into native code. I wrote a bit about this idea in [0], but I'd love to see someone make this work for C++. [0] https://verdagon.dev/blog/fearless-ffi https://verdagon.dev/blog/fearless-ffi
- MagicMoonlight 3y agoDeleting and re-adding each item from an array every time you use something seems like a massive pain
- diabllicseagull 3y agolost me at the unordered map
- oleganza 3y agoThe reason I use Rust is because I can bypass all this messy business altogether and have my sensible patterns wrapped in a usable syntax and enforced by the compiler out of the box. Whenever people say "just follow these rules" I read "just add this extra mental burden and do not slip up". Computers were invented to automate things. Rust automates ownership and borrowing rules. Suggestions like "do not forget to initialize unique_ptr with something" are not intelligent solutions.
- SubjectToChange 3y agoKeep in mind that a lot of C++ users really didn’t have a choice, it was either C or C++ for a lot of applications. In fact C++ is often still the only option, with some segments (like HPC) doubling down on their investments in the language. So how do we serve those users? Rust doesn’t give them a path forward. Simply getting C++ compilers to agree is difficult enough, much less an entirely different language.
- verdagon 3y agoI'd definitely agree, and the article proposes a static analysis tool (similar to the borrow checker) for that purpose.
- rdtsc 3y agoIn Rule 3: struct Ship { int fuel; }; void print(Ship* ship) { cout << ship.fuel << endl; } Should that be "ship->fuel" instead?
- deleted 3y ago[deleted]
- Animats 3y agoWell, what's new here? Generational References, random or sequential That's an old idea. Goes back to at least the 1980s. It's a useful way to detect use-after-free at run time, but doesn't prevent it. "To get access to an object, we first check ("generation check") that the current generation number matches the remembered generation number. If not, we safely signal a segmentation fault." Um. Rule 5: We can only read a field by taking ownership of it, by either swapping something into its place or destroying the containing struct. Hm. This is single ownership with move semantics on steroids. There was once some enthusiasm for languages where you could only read a variable once. But that didn't go very far. On the other hand, single assignment, where you can only write once, on the other hand, is now widely accepted and useful. I doubt that any collection of hacks on top of C++ will make it safe. Too many people, including me, have tried. To increase safety, you have to take things out. This is unpopular and breaks backwards compatibility.
- pjmlp 3y agoOne way to take things out is to have something like Sonar on the CI/CD pipeline, configured exactly to take specific patterns out, that break the PR builds and won't get greelighted for merging. Yeah, not everyone likes those of us that share development roles alongside security best practices enforcement.
- axilmar 3y agoMy idea for C++ memory safety would be compile time reference counting. I.e. the compiler/tool to go through the code, as if it was executing, and apply reference counting to objects, revealing whether an object will be destroyed normally, prematurely or never. This is what we actually do as humans by the way, when we are developing an algorithm in C++ with manual memory management. We do it implicitly though.
- hawk_ 3y agoMy idea is to use your solution to solve the halting problem.
- SleepyMyroslav 3y agoI want to note couple of things from POV of C++ gamedev to safety/compiler people. This is completely IMHO and opinion is not of my employer and yadda yadda disclaimer. Feel free to disagree. It is posted here to learn where I am wrong about things. If you don't like our arrays that means we don't like your type system. Arrays is the only efficient way to access existing RAM and until hardware changes do not start your work with 'do not use arrays'. And do not write into array elements without dev explicit consent just for safety. If you want to lock access to whole array to single reference it does not scale with current hardware that has lots of CPUs and accelerators. Start thinking about array slices as things that can check safety. Start thinking about temporal aspects of multiple code blocks accessing slices into same array(s). If your type system is not allowing iteration in 'broken' project state it does not scale with projects that involve multiple people working at same time. Yes one has to put some mitigations to stop 'nasal demons' from running wild in practice to be able to work. Just drop the idea of blocking people from work until it is fixed.
- nico-mac 3y agoI read the note 9 easter egg! Hope someone believe me.
- LLyaudet 3y agoI had it too today. I saved the page in my bookmarks a few days ago, looked at it this evening. I suspect it is visible to anyone, and the joke is to have you make a comment on HN ;) XD. I checked IWA: It is visible here: https://web.archive.org/web/20230629052606/https://verdagon.dev/blog/vale-memory-safe-cpp https://web.archive.org/web/20230629052606/https://verdagon.... but not on the first snapshot here: https://web.archive.org/web/20230622185954/https://verdagon.dev/blog/vale-memory-safe-cpp https://web.archive.org/web/20230622185954/https://verdagon....
- LLyaudet 3y agoYep not even a random display in JS. I looked at the source code of the webpage. There is nothing curious. Custom JS is here: https://verdagon.dev/components/annotated.js https://verdagon.dev/components/annotated.js and does only trivial things. Not a real easter egg for the client part. Maybe the easter egg is in server part that sends the HTML with or without it depending on hour or something. No need to waste more time on it, there is no funny code behind :)
- rurban 3y ago> Stack objects are safe too "I'd like to have the same drink this gentlemen has." Of course stack objects are only fast (that'd why rust likes them) but entirely unsafe. You cannot return them, you cannot reference them from outside, and you cannot have too many or too big of them.
- deterministic 3y agoI use tools like Valgrind/Helgrind etc. to ensure that million+ line C++ code used in production by large international corporations has zero memory leaks, no buffer overruns etc. It’s easy. It works. No need to rewrite in any other language.