Future design of hardening flags

Up until now, hardening flags (as understood by hardeningEnable, hardeningDisable et al.) have been fairly architecture/processor neutral.

On the horizon are a number of new compiler hardening options that are backed by new hardware features being added by vendors, and they vary quite significantly from architecture to architecture. We’re going to need to decide how fine-grained we want to make our hardening flags for these.

Let’s take as an example a new Control Flow Integrity (CFI) related feature present in recent Intel (and now AMD) processors - “Shadow Stack” (Shadow stack - Wikipedia, https://lwn.net/Articles/885220/). This is a feature that helps secure the “backward edge” of a control-flow-graph (i.e. function returns) from being hijacked. Once kernel support is present (very recent kernels only) it’s just a matter of enabling one compiler flag to start using this on supporting processors.

Aarch64 doesn’t (yet?) support shadow stack, but recent processors do have another means of implementing “backward edge” CFI using a similarly new-ish “pointer authentication” (PAC) feature. This is supported by both clang and gcc through the -mbranch-protection flag.

(Note this is not a discussion about support for older processors lacking support for these features - these features are generally designed in a backwards-compatible way using e.g. instructions that are interpreted as NOPs on unsupporting processors)

I can see three ways we could expose the x86_64 “shadow stack” feature:

  • A “hardware CFI backward edge” hardening flag (hwcfibwedge? …whether we should camel-case or snake-case our flag names is a separate debate…). This is the most abstracted option and denotes the intent to use hardware features to protect return addresses. On x86_64 this would mean use of shadow stack, on aarch64 this may mean use of return-pointer-authentication.
    • Pros:
      • Gathering information on packages where this broadly causes problems (and should be disabled) is aggregated across architectures. Minority architectures benefit from problem discovery on major architectures.
      • It may give us some leeway on exactly what flags are used to implement this, and we might be able to make different decisions as new features become available & widespread.
      • Apparent simplicity to users.
    • Cons:
      • The accuracy of this “problem discovery” may not be great if different mechanisms are being used behind the scenes.
  • A shadowstack hardening flag that corresponds to an actual shadow stack feature. Initially only having an effect on x86_64 but as other architectures gain support for it they can have support added.
    • Pros:
      • Problems are more likely to be similar across platforms
      • Packages that have problems with shadow stack implementations could potentially choose a different mechanism to provide this protection (e.g. return-pointer-authentication - exposed as retptrauth perhaps?)
    • Cons:
      • Opens the gate to confusing & complex logic around features used to achieve the same thing conflicting with each other.
  • A mshstk hardening flag. This is the literal name of the architecture-specific flag used by gcc & clang. This would directly control use of -mshstk on x86_64 and have no effect on other architectures. If another architecture gains shadow stack support, it gets its own flag.
    • Pros:
      • Accurate, fine grained control of flag usage. Enabling or disabling this flag won’t have any effect on any other architecture where the implementation details might be slightly different.
    • Cons:
      • No shared “problem discovery” - the work finding packages where the flag needs to be disabled must be repeated for each architecture. Minority architectures suffer.
      • Similar conflicting-options issues. I guess use of this flag implies a more manual/explicit approach to setting flags and users would take more of that burden on themselves?
      • Flag proliferation.
      • Once the user’s wanting this level of control, is the “hardening flags” system really serving its purpose? User could just as well use NIX_CFLAGS_COMPILE etc.

Keen to hear peoples thoughts on this. As I’ve written this I’ve changed my preferred option a couple of times, but am currently leant most heavily towards the first option.