References, and existing work:
branch-protection
tracking issuecf-protection
(CET) tracking issueOptions like this… | Become… |
---|---|
Various target-specific CFI choices… | -Cabi-variant=cfi |
-Zbranch-protection=pac-ret,bti |
-Zabi-variant=aarch64:branch-protection:pac-ret,bti |
-Ccontrol-flow-guard=yes |
-Cabi-variant=windows:control-flow-guard:on |
Our long-term objectives are to make CFI mitigations easy to enable in
Rust/Cargo, project-wide.
However, the scope of this proposal is simply to define a command-line interface
for controlling CFI options, as a step towards the overall objective.
In particular, whilst the command-line interface for stable and unstable options
is considered, no additional stabilisation is considered here today.
Typically, CFI (control-flow integrity) mitigations are implemented as
target-specific ABI variants. Rust already has some support for enabling these
variants, for example as target-specific CodeGen options:
-Zbranch-protection
(for AArch64),-Zcf-protection
(for x86),-Ccontrol-flow-guard
(for Windows).In addition, this proposal overlaps somewhat with -Zsanitizer
. See the open questions (at the end), for a discussion.
However, they aren't straightforward to use:
-Ccontrol-flow-guard
are unstable.build-std
option is required to get a std
built withcc-rs
does not forward these settings to the C compiler, so included C codestd
) will not include mitigations.A similar, but technically distinct problem occurs with the C library, which is
usually dynamically linked with Rust binaries. Since this is already an external
dependency, we might consider this out of Rust's scope, but that at least
requires documentation.
Note that, to be effective, most of these CFI techniques require that all code
in a process is protected. Even unreachable binary code can be targeted by CFI
attacks if it is loaded into an executable memory region. Usually, this is a
security concern, but we also consider that correct deployment may be required
for the correctness of some techniques.
We hope to find solutions to these problems, perhaps by stabilising build-std
,
but such solutions are out of scope of this proposal.
One minor inconsistency with the existing options is the handling of errors:
control-flow-guard
is ignored for targets other than Windows, whilst
branch-protection
fails if the target is not AArch64. This proposal aims to
unify the behaviours here.
C (or C++) projects might already deploy CFI mitigations, and this might even be
mandated by a system's security analysis team. If they introduce Rust code,
they will need a way to ensure that it implements the same standard of
mitigation.
This may also be required for some mixed-language LTO use-cases.
Rust should provide options that map, in a simple and easy-to-audit fashion,
onto common C compiler options.
In addition, if new options are added to future C compilers, we should be able
to support them in Rust too.
A notable disadvantage of matching C compiler options is that they are
target-specific, which makes them difficult to use in a project that is
otherwise target-agnostic Rust.
Rust should provide simple, target-agnostic Rust options that map, in a
target-specific manner, onto some ABI variant.
The specific mitigations used will vary from one target to another, and for some
targets may not be possible at all. The behaviour should be "best-effort":
targets that don't implement the option can simply ignore it, perhaps with a
warning.
Fine control is required for Rust-C interoperability, but may also apply to
pure-Rust programs, providing very fine control over the mitigations used, at
expense of portability. For example, the pac-ret
scheme for AArch64 can be
deployed with one of two keys ("A" or "B"). Rust should allow this
flexibility, even if it is not exposed by default.
This is quite unlike the general, best-effort approach. If fine control is
requested, it would be misleading for Rust to ignore it, so Rust should instead
refuse to build the project.
C compilers typically have to be concerned about compatibility, so that compiled
functions can be linked together without recompilation. Rust's approach of
statically linking (almost) everything somewhat frees it from this restriction,
and potentially allows it to deploy mitigations very quickly, at least when
using the Rust ABI.
In addition, we observe that whilst all mitigations listed here concern CFI,
that need not always be the case. For example, AArch64 Pointer Authentication
can also be used for data protection. Ideally, the option name should be chosen
with this in mind.
We propose no new ABI variants today, but the command-line interface proposal
should allow for such additions.
Currently, none of these features are enabled by default. However, if that were
to change (or if a default was set earlier on the command line), it can be
useful to have a means to turn them off. This is possible today for
cf-protection
and control-flow-guard
, but not for branch-protection
, which
is purely additive. It would be useful to make these consistent.
-Cabi-variant=...
/ -Zabi-variant=...
-C
rather-C
and -Z
forms provides a deployment path for new optionsabi-variant
itself is stable.-Ccontrol-flow-guard
should remain, forabi-variant
options.-Zbranch-protection
and -Zcf-protection
shouldabi-variant
is not currently in use, so should not come with anyThe option's value is a comma-separated list of zero or more <variant>
names, preceded by zero or more <group>:
selectors. However, behaviour
differs slightly if no <group>
is provided.
<variant>
Top-level variants (without any <group>
) are high-level, best-effort
protections. Suggested flags include:
backward-edge-cfi
, which enables best-effort backward-edge CFI.
aarch64:branch-protection:pac-ret
,x86:cf-protection:return
, something else, or nothing at all.forward-edge-cfi
, which enables best-effort forward-edge CFI.
aarch64:branch-protection:bti
,x86:cf-protection:branch
, something else, or nothing at all.cfi
enables both of the above.none
(the default) disables all top-level protections.All of these top-level variants work on a best-effort basis. It is an error to
name a variant that doesn't exist, but if a known variant cannot be implemented
for the compiler target, it is simply ignored. In addition, Rust may change the
exact meanings of these flags, for example if new techniques are introduced in
future versions.
Top-level variants are treated as simple shortcuts for target-specific variants
that the target platform supports, and are intended to avoid target-specific
configuration in build infrastructure.
<group>:[<group>:]...[<variant>,]...
<group>
-specific variants are treated as precise, target-specific controls.
Typically, we expect the first <group>
to specify the target (or group of
targets), and the next <group>
to match the name of the de-facto C compiler
option (if there is one).
For example:
-Cabi-variant=aarch64:branch-protection:pac-ret,bti
-Cabi-variant=x86:cf-protection:branch
-Cabi-variant=x86:cf-protection:branch,return
(or equivalently, x86:cf-protection:full
)-Cabi-variant=windows:control-flow-guard:on
Groups may be named loosely, but are chosen as a compromise between verbosity
and specificity. Notably, we don't expect to see full target triples here. For
example, "aarch64" would group features that only make sense on aarch64*
targets, but we don't require that all aarch64*
targets can actually
implement the included <variants>
.
It is an error to specify a variant under a named group unless that variant is
actually supported by the target.
It is valid to combine target-specific variants with a more general top-level
baseline, but we provide no forward-compatibility guarantees for this use-case.
For example, the following will work according to this proposal, but if cfi
is
later updated to use something other than pac-ret
, then b-key
will be
invalid:
-Cabi-variant=cfi -Cabi-variant=aarch64:branch-protection:b-key
However, it may be useful to permit these combinations for use-cases where we
want a "good" baseline, but want fine control over some aspects:
-Cabi-variant=forward-edge-cfi -Cabi-variant=aarch64:branch-protection:pac-ret,b-key
Is this behaviour reasonable? Should we instead forbid mixing these at all?
Evaluation should be intuitive, but the exact rules are described here to avoid
misunderstandings. The existing options have slightly different behaviours, so
different readers may have different intuitions.
Variants are evaluated in command-line order, then first to last in the
comma-separated list. Variants are typically additive, but that isn't required.
We expect none
and off
to have the effect of disabling the whole <group>
(but nothing outside it).
We don't check validity until the last abi-variant
has been parsed. Note that
this differs from today's branch-protection
, for example, which checks
validity as it goes (and therefore permits pac-ret,leaf
but not
leaf,pac-ret
).
<group>
selections can only be specified once per -Cabi-variant
, but in all
other respects, multiple abi-variant
options are equivalent to a single option
with multiple <variant>
s. For example, the following are all equivalent
(assuming that none
is the default):
-Cabi-variant=aarch64:branch-protection:pac-ret,bti
-Cabi-variant=aarch64:branch-protection:pac-ret -Cabi-variant=aarch64:branch-protection:bti
-Cabi-variant=aarch64:branch-protection:b-key -Cabi-variant=aarch64:branch-protection:none,bti -Cabi-variant=aarch64:branch-protection:pac-ret
-Zabi-variant
The unstable form of the option should remain permanently, and behave
identically to the stable form. The only difference is that the unstable
abi-variant
can accept unstable <variant>
s. This allows us to extend the
interface gracefully, without breaking stability rules.
Notably, it is possible to combine both forms. The following are all equivalent
(with a nightly compiler):
-Cabi-variant=forward-edge-cfi -Zabi-variant=backward-edge-cfi
-Cabi-variant=cfi
-Zabi-variant=cfi
-Zabi-variant=... |
Meaning |
---|---|
aarch64:branch-protection:... |
-Zbranch-protection=... |
x86:cf-protection:... |
-Zcf-protection=... |
windows:control-flow-guard:... |
-Ccontrol-flow-guard=... |
forward-edge-cfi |
Varies with target. |
backward-edge-cfi |
Varies with target. |
cfi |
-Zabi-variant=backward-edge-cfi,forward-edge-cfi |
Once abi-variant
itself is stabilised:
-Cabi-variant=... |
Meaning |
---|---|
aarch64:branch-protection:... |
Rejected until branch-protection is stable. |
x86:cf-protection:... |
Rejected until cf-protection is stable. |
windows:control-flow-guard:... |
-Ccontrol-flow-guard=... |
forward-edge-cfi |
Varies with target. |
backward-edge-cfi |
Varies with target. |
cfi |
-Cabi-variant=backward-edge-cfi,forward-edge-cfi |
-Zsanitizer
There is considerable overlap between this proposed {-Z,-C}abi-variant
and the
existing (unstable) -Zsanitizer
, because many of the available sanitizers
could be viewed as ABI variants.
Question: should this proposal be merged with -Zsanitizer
?
{-Z,-C}abi-variant
has three notable properties that -Zsanitizer
lacks:
{-Z,-C}sanitizer
).It is clearly undesirable to maintain abi-variant
mappings for every
sanitizer
option. Would it instead be acceptable to replace sanitizer
with
abi-variant
? Alternatively, if the abi-variant
behaviours described here are
deemed useful, should we attempt to fit them under the existing sanitizer
option?
Also note that the naming might require further attention. abi-variant
currently covers technologies that are designed to be used in release builds, in
production. They will have a performance and code-size overhead, but should be
"light". Conversely, the term sanitizer
suggests more comprehensive debug and
test tools, though many of its options are clearly designed for deployment too.
The ABI variant describes what the compiler should do, whilst the target
features describe the set of tools it has available to do it. For example:
pac-ret
paca
is enabled atpac-ret
using instructions that weren'tWe also consider the possibility that an ABI variant might require a target,
or target feature. Specifying such a variant should cause a compile-time error,
as noted earlier. For example, -Zsanitizer=shadow-call-stack
is only
available on aarch64-linux-android
.