4 ms·
Couldn't compilers be configured to use such values for for any enum type? And maybe even auto-insert the appropriate check in the final unchecked else anywhere
by rcthompson 3y ago
Couldn't compilers be configured to use such values for for any enum type? And maybe even auto-insert the appropriate check in the final unchecked else anywhere that enum type is otherwise exhaustively checked?
- f_devd 3y agoYes it's possible, but it's not desirable. It wouldn't be backwards compatible, and not safe for shared libraries. It's better suited for a linter-type error/warning.
- andrewaylett 3y agoIt's not plausible in C, for the reasons you mention, but it might be more possible in other languages -- Rust, for example, only guarantees specific representations when instructed and doesn't allow for shared libraries without a specified representation, so it wouldn't have either issue for most application code. Dynamically-typed languages similarly should be able to choose enum values at runtime in many cases.
- rcthompson 3y ago> Dynamically-typed languages similarly should be able to choose enum values at runtime in many cases. I wonder, is choosing random enum values at runtime more secure against Rowhammer than just having fixed values that were chosen randomly once and compiled in, since presumably the attacking code now has no way to know which bits it needs to flip? If so, it might even be desirable to implement this as a "secure enum" in a compiled language.
- mcculley 3y agoFrom the commit: “The values used were chosen such that it takes a large number of bit flips to change from allowed to denied. Using random values doesn't really protect against this attack.” It would be neat to see an algorithm that generates suitable values.
- f_devd 3y agoThe basic algorithm for the 2-enum case from the commit seems to just be `enum { A = rand(), B = ~A}`. Although I'm not sure if it's optimal, the many case seems to be the same as the 2-case but repeated for every 2 items. I expect they double checked that the amount of bitflips is still pretty high. Maybe a better algorithm for the many case would be something like the popcnt parallel patterns: * 0b0101010101010101 * 0b0011001100110011 * 0b0000111100001111 * 0b0000000011111111 Since they would all have equal hamming distance between each of the entries.
- mcculley 3y agoYeah, I was specifically wondering about n>2. Your approach seems reasonable.
- tomsmeding 3y agoThe n=2 case also occurs in the commit: https://github.com/sudo-project/sudo/commit/7873f8334c8d31031f8cfa83bd97ac6029309e4f#diff-bc39bcb6f7119485e73eb37357a3e3ae1123fe09ac06b040f090b3218f186cb1R39 https://github.com/sudo-project/sudo/commit/7873f8334c8d3103... And indeed, the two values ate bitwise complements.
- loeg 3y agoThe problem with C and C++ is that enum values are explicitly incrementing, even when the user does not specify a literal value for each one. So if anything depends on a specific value (e.g., disk or network formats), this will break it. I think Rust enum values are similar, but I'm not a language expert.
- fl0ki 3y agoRust enums are incrementing too so that they can generate machine code with dense jump tables. If you also want the discriminant value for something, you opt into that with e.g. #[repr(u8)] and you can even override the values. Note that unlike in many other languages, casting from a number to the enum is a falliable operation because not all values are valid. Making something like this into a panic is not a good fit for Rust as-is. Because enums are proven to have only correct values, not only is code written to assume pattern matches cannot panic, but compilers are free to optimize around only having valid values as well. That goes not only for the enum discriminant, but for any associated values being properly initialized values of their respective types. In a sense, Rust lets you write code as if invalid values never happen, so there's less to check for in your code. It's understandable from the perspective of the abstraction needed for computer code to be "correct" and not just temporarily getting away with Undefined Behavior. There are simpler ways to violate it than just rowhammer, write straight to process memory for example, which can also violate invariants that compilers assumed while optimizing. If you wanted to compile Rust (or anything else) with a hardening mode that does check what should be redundant values, it would be a lot slower and code that never panicked before would now panic, but it would probably be a worthwhile tradeoff for some programs to opt into. After all, if you built for CHERI or arm64e and got a machine exception from an unauthenticated pointer, you'd be thrilled you mitigated a vulnerability even if it violated your higher-level language model. Defense in depth and all that. Maybe someone feels motivated enough to write an RFC and prototype for this. It just wouldn't stop at enum values, it should mean all sorts of other things too, such as not eliding any other checks that appear redundant given assumptions like immutability. That's what makes it slow and hard to reason about.
- kazinator 3y agoYes; gcc and clang could, in principle, support an extension like: __attribute__((rand)) enum e { FOO, BAR, ... }; which randomizes the values, as an extension. You only need this in specific places, like setuid programs. Randomization can be bad because it wrecks build reproducibility; it would have to be tied to the GNU Build ID. If such an enum is used in any interface between files, the randomization has to be the same in every translation unit. Maybe the syntax could specify a seed: rand(42).
- jmprspret 3y agoA comment in the commit notes that randomisation does not necessary mitigate the issue. Which is why only a couple of the values are random, with the others being those values, XOR'd with 0xff*