Hacker Newsnew | past | comments | ask | show | jobs | submitlogin
Low-Level Software Security for Compiler Developers (llsoftsec.github.io)
78 points by signa11 on Feb 20, 2023 | hide | past | favorite | 17 comments


There was some discussion two days ago: https://news.ycombinator.com/item?id=34845954


This is an interesting overview.

It mentions, regarding sanitizers (and presumably other safety tooling): "They are often too expensive to run in production mode, as they tend to increase execution time and memory usage."

I'm surprised we don't have better, performance-neutral hardware support for safety constructs like bounds checking.

Legacy x86 had a BOUND instruction but apparently it sucked and was so little used AMD didn't even bother to implement it... MPX exists but alas still has overheads in practice that can approach 50%... seems we can't match Lisp machines of decades ago that did it properly (in parallel) and for free [1].

1: https://stackoverflow.com/questions/40752436/do-any-cpus-hav...


ARM's MTE is often nicknamed "HWASAN" - it implements a lock-and-key metaphor. The memory allocations emitted by the compiler will get a "coloring" for the base pointed which can be matched with the specific region accessed by indexed loads and stores.

BTW this isn't just an experiment or research - some android developers have this enabled today and Android 14 beta has a user-configurable option to enable it.


so, like a segment?


Yes, it's somewhat similar but much higher resolution. 16-byte "granules" instead of multi-KB/MB/GB pages. Also - instead of having the OS determine how to handle paging, the CPU can tell whether it should fail the load/store based on the color of the access and the color of the granule.


Besides ARM in the sibling replies, Solaris on SPARC has been using ADI for a decade, iOS has PAC, CHERI is being worked on as Microsoft/ARM collaboration, and then there is ARM MTE on Azure Sphere as well.

https://docs.oracle.com/cd/E53394_01/html/E54815/gqajs.html

https://developer.apple.com/documentation/security/preparing...

https://msrc.microsoft.com/blog/2022/01/an_armful_of_cheris/

Because we have come to the realisation that only "C Machines" can fix this issue, as even if Safe C would come tomorrow, the existing software wouldn't be rewritten anyway.


> I'm surprised we don't have better, performance-neutral hardware support for safety constructs like bounds checking.

It isn't necessary. Languages that have properly bounded arrays just emit the code and optimize it. E.g. if a loop steps a variable from i from 0 to N-1, accessing arr[i], the arr[i] bounds check can be moved out of the loop: if N-1 is within bounds, it's good to go.

I think the tooling being alluded to here is for checking bounds in programs that not only don't check it themselves, but don't pass around the information which makes it possible to know the size of an array.

The tooling has to build its own data structure to track objects and instrument the binary to intercept accesses; all of that is expensive.


> Languages that have properly bounded arrays just emit the code and optimize it. E.g. if a loop steps a variable from i from 0 to N-1

Modern languages have for value in arr, arr.forEach or equivalent. Such code makes it easier for compiler writers to make this optimization.

Swift even chose to remove for loops that step a variable (https://github.com/apple/swift-evolution/blob/main/proposals...), and I don’t think Rust has them, either.

I think that’s the right decision. In modern languages, classical for loops are a code smell.

And of course, golang goes the other way. https://go.dev/tour/flowcontrol/1:

Go has only one looping construct, the for loop.

The basic for loop has three components separated by semicolons:

- the init statement: executed before the first iteration

- the condition expression: evaluated before every iteration

- the post statement: executed at the end of every iteration


If the compiler has good loop optimizations, the front end can turn foreach into an ordinary loop with a hidden variable:

  for (x in array) 
    print(x)

  ->

  const __local0017 = array.len;
  for (var __local0018 = 0; __local0018 < __local0017; __local++) {
    const x = array[__local0018];
    print(x);
  }
The middle and back ends will infer that __local0018 doesn't exceed __local0017 which is the length of array and get rid of the bound check.


> Modern languages have for value in arr, arr.forEach or equivalent. Such code makes it easier for compiler writers to make this optimization.

Those features were modern 40 years ago, even Visual Basic (pre-.NET) already had them.


Bounds checking isn't the only type of memory unsafety detected by sanitizers: they can also detect things like use-after-free, undefined behavior, and so on.


hardware pointer type would be pretty cool too


Once the data escapes the CPU via a load or store, it's hard to enforce something like a "type" in hardware. ALU instructions operating on what appears to be integers could easily have been operating unknowingly on virtual addresses. Even if you used the "integer add" instruction, there's no way for it to know that it didn't just index into a buffer using a virtual address as a base.

However there are cooperative ISA features -- between compiler and CPU -- these can be used to constrain untrusted attackers. See ARM PAC, e.g.


the point is to put a guard when performing indirection, that checks a type bit in the register. setting this bit is a privileged operation. unprivileged address arithmetic can be performed if addresses also contain a bound.

but the whole point is that you just cant hand any integer to the hardware and ask it to fetch.



This looks pretty great. If you prefer videos and examples you can walkthrough yourself, LiveOverflow's Youtube channel covers most of the sections here with more explanation:

https://youtube.com/@LiveOverflow





Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: