4 ms·
Question: it looks like the generational memory strategy prevents use-after-free bugs at runtime, which is nice, but a language like Rust prevents these kind of
by sumy23 4y ago
Question: it looks like the generational memory strategy prevents use-after-free bugs at runtime, which is nice, but a language like Rust prevents these kind of bugs at compile time. Am I missing something, or is Rust fundamentally safer than Vale?
- darthrupert 4y agoWhy would a run-time check make anything less safe than a compile-time check, if they perform the same check?
- rich_sasha 4y agoBecause at runtime you find out about the bug only when it occurs. A compile type check fails to compile the buggy code.
- darthrupert 4y agoAh yes, I see. Strictly speaking, both are exactly as safe, since safety is about whether unsafe actions can be done. It does not matter at which point the unsafe action is stopped. But it could have some implications for software stability (compile time being obviously better) and productivity (run time being obviously better).
- yaantc 4y ago> It does not matter at which point the unsafe action is stopped. It does in practice. With a compile time check the issue simply cannot happen, and there is no need to deal with it in the code or at system level. With a runtime check the issue may happen, but will be detected when it does. Still, the choice then is either to crash or to deal with it with some runtime recovery action. Either way has some cost: the system around the executable must deal with more crashes, or the code gets more complex (and sometimes more brittle, even if the intention is the opposite). Only the compile time check makes an issue really go away. This is to me the attraction of strong typing and any form of compilation time check. There's a price to pay too in accepting the related constraints: type checking must pass. So there's a cost here too. For complex or sensitive applications I personally much prefer this upfront cost. But it's definitely not the only way: Erlang has runtime type checks, but a very good runtime error handling framework (the reference?), and it works well. Still, most environment relying on runtime checks are not at this level.
- foldr 4y ago>Only the compile time check makes an issue really go away. The issue that goes away in both cases is the issue of unsafe memory access. There are disadvantages to runtime checks, as you mention, but there is no difference in the level of safety achieved.
- yaantc 4y agoYou need to consider not just the memory access itself, but the consequences of preventing it. With a compile time check, there's a development cost but absolutely no runtime consequence. With a dynamic check, sure the memory access is detected and blocked. But if you stop there the application crashes, which may be completely unacceptable. In an embedded system such a crash may be as bad as the incorrect access itself. More generally, the issue is not so much the incorrect access than its possible adverse consequences. With a compile time check, there are no runtime consequences. With a runtime check, there are still runtime consequences: either the impact of an application crash, or the extra error handling code and behavior to deal with the detected wrong access and mitigate it at runtime. Whether such consequences are acceptable or not depends on the context, but it's there and do make a difference with a compile time or static analysis check.
- foldr 4y ago>In an embedded system such a crash may be as bad as the incorrect access itself. I don't agree on this point. An incorrect access on an embedded system has the potential to cause all kinds of horribly subtle bugs involving memory corruption. A simple crash is generally much better.
- yaantc 4y agoPossibly, but I won't argue about hypothetical kinds of bad when a runtime issue happen ;) The point I tried to make was that you want neither for such use cases. Hence compile time verification (type base, static analysis, proofs for the most critical).
- astrange 4y agoA runtime check can be more precise; a compile time check has to reject more potentially unsafe code, which means people might turn it off. I think there's a lot of possibility in runtime assertions that fail as fast as possible, ie the instant the program enters an invalid state rather than the later time you get to the part of the code the assertion is written in. Don't know if there's any systems like that, it's just something I thought of.
- verdagon 4y agoI share this perspective, it's why Vale chose generational references over a borrow checker. In practice, the borrow checker has to reject a lot of perfectly fine patterns, such as observers, dependency injection (the pattern, not the frameworks), delegates, backreferences, many forms of RAII [0], graphs, etc. Sometimes, the workarounds lead to more complexity, less flexibility and decoupling, and more refactoring which would be unnecessary in other languages. This is likely why GUI is difficult with the borrow checker, and why one has to bring in frameworks to compensate. The borrow checker can prevent certain kinds of logic bugs at compile-time, which might mean a release lets 9 bugs into production instead of 10 or 11 (comparing to a safe language with a strong type system). However, that tradeoff might not be worth it, depending on the domain. Flexibility and decoupling can be more important, at least in the domains I've worked in (roguelike games, web servers, apps) and the size of the program. There are domains where it's better to add more complexity to detect even more bugs at compile-time, that's where I'd choose Rust (or perhaps GC'd FP languages, which prevent even more bugs). Just my two cents! Note that this is only a problem if a programmer is a bit too religious with borrow checking; in practice Rust offers reference counting which can nicely avoids these problems. One of Vale's principles is to move checks up to compile time, but prefer not to when it causes too many architectural problems or "infectious leaky abstractions" so to speak. This is also why Vale will be using coroutines (similar to Go) instead of async/await. It's also why we're adding a region-based borrow checker, which is opt-in and doesn't impose constraints on its callers. [1] If we do it right, it should give Vale a lot of the performance benefits of Rust's borrow checker, but without the complexity and architectural constraints. [0] https://verdagon.dev/blog/higher-raii-7drl https://verdagon.dev/blog/higher-raii-7drl [1] https://verdagon.dev/blog/zero-cost-refs-regions https://verdagon.dev/blog/zero-cost-refs-regions
- sumy23 4y agoIn my opinion, a runtime check is less safe than a compile time check. In both cases, the language is free from undefined behavior when using a freed reference. However, while a language with a runtime check might not have UB, programs written in this language might have UB. What happens in the program when the use-after-free happens? Depending on how the error is generated and propagated / handled, the program could end up in an undefined state. Programs written in safe Rust, however, will never have such undefined behavior
- azakai 4y agoRust with RefCell will have such runtime errors, though. (Likewise, Rust using the indexes-in-an-array pattern can have use-after-free, but the harm is limited.) Also, IIUC this is not actually UB in Vale: it's a guaranteed error.
- sumy23 4y agoYes it’s a guaranteed error in Vale, but the error may cause UB in the application logic. This won’t happen in Rust because the program won’t compile if a use-after-free is possible.
- azakai 4y agoI wouldn't say it causes UB. It causes the problem to halt, deterministically, and safely - but maybe annoyingly. Yes, it's not as good as a static guarantee. But there are tradeoffs where it makes sense. Again, RefCell in Rust does the same - it's a useful technique.
- imtringued 4y agoAccording to your definition everything is undefined behaviour. Imagine a simple program that successfully halts if it's input is empty and returns an error if it's input is no empty. C adds a third state undefined. When you reach this state you know nothing about what is happening in the program. Now Vale adds a third state called "memory error" which is just a refined error and well defined. This means that if you have handled the error case, you already handled the memory error case even if not in a satisfactory way. What is strange to me is that you consider the former okay and the latter undefined behaviour in the application logic when it just means that an additional exit state has been added which a highly defined behaviour.
- e4m2 4y agoVale compares its safety to Rust's on a few occasions[0][1] and the exact opposite claim is made, i.e. Vale is safer than Rust. [0] https://verdagon.dev/blog/hybrid-generational-memory#afterword-how-might-it-compare-to-rust https://verdagon.dev/blog/hybrid-generational-memory#afterwo... [1] https://vale.dev/comparisons#safe https://vale.dev/comparisons#safe
- astrange 4y ago> C++ is completely unsafe. You can largely trust Javascript code that someone else wrote. This is an interesting claim since JavaScript runtimes are written in C++. I suppose we do trust them, but only after wrapping them in all the other layers of safety we can get our hands on.
- TobTobXX 4y agoBut when you start with that, it's turtles all the way down. Who guarantees that the Rust compiler is correct? Who guarantees that a kernel implements syscalls correctly? Who guarantees that RAM bits don't flip? Their point wasn't about the implementation of JS (which can always be improved), but on the definition of the language.
- astrange 4y agoAh, but you actually do need to care about all that stuff too if you're going to run code someone else wrote in your browser, or at least if you're a security engineer for that browser. That's how 0-days get you. It's not a problem for your own programs because you're not trying to hack yourself, but if you're an aggressive enough developer you will find bugs in your compiler. In fact, this is a great reason to have full test coverage.
- verdagon 4y agoGood question! If one defines safety as a lack of UB or vulnerabilities, then I'd say Vale's a bit safer than other languages like Rust. I say this because: 1. There are no unsafe blocks in Vale. 2. Vale memory is decoupled from native memory, it only passes messages and handles between, and uses a different stack. [0] This prevents accidental bugs in unsafe code from corrupting data from safe code. We just finished our proof-of-concept of this last month! 3. Building on that, we could automatically sandbox any native code for a module in theory, with either subprocesses or wasm2c. [0] (Note we've only just started on this part.) I'd say that's safer than Rust, where unsafe code can undermine the code around it. I'm particularly excited about how this might make it safer to use dependencies; #1 and #2 protect from accidental corruption, and #3 could help protect against malicious corruption and certain kinds of supply chain attacks. We're even tossing around a potential #4 to add permissions, so dependencies must be whitelisted to be able to access network, files, etc. We're also thinking about relaxing the above 3 restrictions on a per-module basis if we can do it in a way that doesn't compromise the safety of the ecosystem (note that these relaxations are not implemented yet): * Instead of full sandboxing (3), we can rely on the decoupling (2), if we trust a dependency's intentions. * Have operators to skip generation checks for extra speed. [1] These operators would be by default ignored for dependencies. It's unclear if this will help, as the combination of regions [2] and HGM [3] might combine to eliminate the vast majority of generation checks. We'll see! * Perhaps add a keyword for blocks, to ignore all generation checks within (maybe `unsafe`?). This would also be ignored by default for dependencies. Rust must always allow unsafe blocks, because libraries often believe it's necessary for their performance, or sometimes for just working around the borrow checker. Vale would default to disallowing it (and sandbox FFI), and it would be much more noticeable (and suspicious) if a dependency said it wouldn't work without unsafe; they can't just sneak it in there like they can in other languages. These adjustments are still under consideration (thoughts are welcome!) but as of today, there's no unsafety in any Vale code. To summarize, Vale has stronger protections against memory unsafety and UB, so I'd say it's a bit safer. Though, if one wants to prevent all bugs at compile time, then one shouldn't be looking at Rust or Vale which often panic, but instead look at languages like Pony (which doesn't even have panics, hence its amazing uptime) or proof languages like Coq. Hope that helps! [0] https://verdagon.dev/blog/next-fearless-ffi https://verdagon.dev/blog/next-fearless-ffi (still a draft, read generously!) [1] https://vale.dev/guide/unsafe https://vale.dev/guide/unsafe [2] https://verdagon.dev/blog/zero-cost-refs-regions https://verdagon.dev/blog/zero-cost-refs-regions [3] https://verdagon.dev/blog/hybrid-generational-memory https://verdagon.dev/blog/hybrid-generational-memory