Recent submissions

Comment · 2 days ago
Depends on what kind of exploitation you're talking about -- integer overflows can still cause out of memory conditions and crash the program, which is a denial-of-service vulnerability when talking about webapps.

But yes, memory-save languages like Rust are a massive help protecting against remote code execution. (unless said memory-safe language introduces its own category of RCE vulnerabilities, e.g. deserialization in Java)

Comment · 10 days ago
> It would've been much more useful to make the move constructor of indirect be deleted

No, that would be less useful. As-is, you can use a class with a `std::indirect<T>` member in a `std::vector` just fine, thanks to the move constructor.

With `std::indirect` in `std::vector`, the vector can use move the `indirect` internally when reallocating, and as a user of the vector you will never encounter the moved-from state.

It's annoying that C++ doesn't have destructive move and thus needs to relax class invariants after a move happened, but in practice it's usually not a big deal because use-after-move is usually a bug, nothing anyone would do intentionally.

Comment · 11 days ago
Are you forgetting about log4shell?

log4shell was easier to exploit. It also come with an initial patch that turned out to be incomplete, so everyone had to urgently patch every java web app twice!

Comment · 19 days ago
If a language allows taking references to fields within a tagged union (as is possible with Rust enums), then "shared-xor-mutable" is required to avoid use-after-free (switching the tagged union to another variant ends the lifetime of the old variant's fields).
Comment · 22 days ago
A surprising number of those optimizations are only valid by "reasoning from undefined behavior".

Under the as-is rule, optimizations must not change program behavior. A C program could theoretically use out-of-bounds pointers to scan its own stack, observing whether a value occurs on the stack. Thus, the as-is rule prohibits storing local variables in registers!

But because out-of-bounds pointers are undefined behavior, the compiler can ignore the programs doing stack scanning, and so register allocation becomes possible under the as-is.

So boring old register allocation is one of those "assume that UB doesn't happen" optimizations! Of course, the compiler never actually reasons "this pointer arithmetic is out of bounds therefore I can put that variable over there into a register" -- the reasoning from undefined behavior doesn't happen at compile-time, it already happened when the "register allocation" optimization was designed. But that's the case for most "assume that UB doesn't happen" optimizations! (this is also why it's so difficult for compilers to warn about undefined behavior -- they optimize based on it without ever detecting it!)

If you want to eliminate "reasoning from undefined behavior", you'd also need to replace the "as-is" rule with something else -- an explicit list of allowed optimizations in the language standard?

Comment · 22 days ago
> I honestly don't really care how many optimizations that would break; correctness is king.

It's approximately all optimizations. Good news: there's already a compiler option that does exactly what you want: -O0. It's even enabled by default (unless overridden by another -O switch)!

Comment · 23 days ago
The compiler is only allowed to do that when it can prove `puts` will return (i.e. it must prove `puts` does not call `exit`). In a world with SIGPIPE that shouldn't happen. (unless maybe the compiler can prove that there's enough space available in the stdlib IO buffers so that puts won't actually output anything. but then there's no obversable difference in behavior)
Comment · 1 month ago
Despite using C++ for a few years now, the EDG source code is still mostly C code (and in fact, the C++ code is still using the old .c file names). In particular, there's no usage of the C++ standard library.

Where other languages might use inheritance, EDG still uses the C-style `union { ... } variant;`.

On a related note, compiling EDG is extremely fast: on my machine, the EDG frontend compiles in <10s; whereas clang takes >10min (caution unfair comparison: clang includes much more than just a frontend).

Comment · 1 month ago
It's one of the 4 big remaining C++ compiler frontends: gcc, clang, MSVC, EDG. Most other C++ compilers are based on either EDG or clang. (e.g. the Intel C++ compiler used to be based on the EDG frontend, though modern versions are based on clang instead)

The "frontend" is the part of the compiler that understands the input language: lexer, parser, template instantiation, constexpr evaluation, ... The EDG frontend produces an intermediate representation (IL) that is then used by the different compiler vendors to generate machine code. Or do code analysis.

EDG can also be used as a C++-to-C compiler.

Comment · 1 month ago
> ILP64 (wherein int is 64 bits) exists. It's not very popular, but it exists; e.g. ICC supports it.

ILP64 is problematic for existing code: there is lots of stuff like hashcode computations using uint32_t with multiplications, relying on the C standard guaranteeing wraparound for unsigned overflows. But with 64-bit int, uint32_t will promote to a signed int, and overflows will thus be undefined behavior. This problem already exists with uint16_t multiplications on current architectures, but moving the problem to uint32_t will cause trouble for a lot of existing code that thought using fixed-size types like uint32_t would be safe.

Comment · 1 month ago
For 32-bit floats, subnormals are the numbers closer to 0 than 2**(-126) == 0.0000000000000000000000000000000000000117549. For 64-bit doubles, it's 2**(-1022), a number starting with 308 decimal zeroes.
Comment · 1 month ago
I don't know why I wrote the comment about parameters earlier -- (N)RVO is about return values. Those have a different reason for being passed behind a hidden pointer: the class type might have self-referencing pointers, so there must be an explicit move/copy constructor call whenever it changes address, to give the class an opportunity to update those pointers. This cannot work when returning in a register: the callee doesn't know the target address, and the caller doesn't the source address, so neither can call the move constructor. Thus, all ABIs must pass a pointer (or let caller+callee agree on a memory location in some other way) for types that aren't trivially copyable.
Comment · 1 month ago
> Memory safety bugs aren't any different from other bugs.

This is highly dependent on what kind of software you are writing.

On the one end, there's stuff like an image format parser in a browser -- a pure function from untrusted bytes to untrusted pixels. In a memory-safe language, it's pretty hard for such code to have vulnerabilities (other than DoS) in such code -- you'd have to explicitly go out of your way to do weird stuff (open unrelated files, start subprocesses, ...). Simple logic bugs can only lead the wrong pixels or panics. But when memory safety bugs are possible, remote code execution is common in such code. Memory-safe languages make a massive difference here!

On the other end, you have stuff like a javascript JIT compiler, which turns untrusted javascript into trusted machine code -- here pretty much any logic bug leading to "wrong output" can be turned into a remote code execution exploit. Memory safe languages are not very useful here.

Comment · 1 month ago
The optimization is often possible even if the computer does not see the call, because most (all?) ABIs have always required hidden pointer parameters for class types with non-trivial destructors.

https://godbolt.org/z/9WvnEvEYh Note how `std::unique_ptr<int>` effectively passed as a `int**`; and that the by-value unique_ptr is not destroyed at the end of the function -- destroying parameters is instead the caller's job (and commonly only happens at the end of the full expression containing the call -- though this choice is implementation-defined). But that can only work if the caller can see the updated value of the parameter (to avoid double-free for `clear`) -> thus the need to pass the parameter by hidden pointer.

Comment · 1 month ago
The compiler cannot optimize that into `while(true)` because the original code does not encounter undefined behavior when `limit` is small enough to fit into `int`. What it can do: infer that `limit <= INT_MAX` and use that to optimize the code following after the loop (and in some cases, even the code before the loop).

Live from the official Hacker News API

A modern reader for Hacker News · SvelteKit + Skeleton