Mandatory enforcement of indirect branch targets
undeadly.org
undeadly.org
Without IBT, you'd have this equivalence between C and assembly:
main() {
void (*f)();
f = foo;
f();
}
void foo() { }
---
main:
movl $foo, %edx
call *%edx
ret
foo:
ret
If IBT is enabled, the above code triggers an exception because foo doesn't begin with an "end branch" instruction. When IBT is enabled by the compiler, the above code gets assembled as: main:
endbr64
movl $foo, %edx
call *%edx
ret
foo:
endbr64
ret
Now the compiler inserts endbr64 at the start of each function prologue. The reason for this feature, is to use as a defense in depth against JOP, and COP attacks, as it means that the only "widgets" available to you are entire functions, which can be far harder to exploit and chain.[1]: https://www.intel.com/content/dam/develop/external/us/en/doc...
## Why do Windows functions all begin with a pointless MOV EDI, EDI instruction?
[1]: https://devblogs.microsoft.com/oldnewthing/20110921-00/?p=95...
This just worsens my fear of changing "unnecessary" code when I don't know the original motivation for it.
Though, I guess even if it was, it'd be silly to rely on it even on x86 only. Maybe it would still make for a nice fast-path? Dunno.
The fact that it’s xor rax, rax rather than xor eax, eax is also interesting as it’s one byte longer for exactly the same effect (modifying the bottom 32 bits of a register clears the upper 32 bits). It makes me think there’s something weird going on other than compiler stupidity. I’d be interested in seeing the code it was compiled from.
AArch64 has a similar space: https://developer.arm.com/documentation/ddi0596/2020-12/Base...
And yes, PowerPC has a similar space as well holding hints like 'give priority to the other hardware threads on this core' and the like. https://utcc.utoronto.ca/~cks/space/blog/tech/PowerPCInstruc...
So consider the case of a standard mutex in the contended case. Normally the code will spin for a little bit before informing the kernel scheduler on the off chance that the thread that owns the lock is currently scheduled on another hardware thread. In that case it's in the best interest of the thread trying to grab the lock to shift most of the intracore priority to any other hardware threads so that it can potentially help the other hardware thread holding the lock get to a point where it gives up the lock quicker.
“random_samp_ele_crit=name
Specifies the random criteria for selecting the instructions for sampling. Valid values for this option are as follows:
ALL_INSTR
All instructions are eligible. This value is the default setting.
LOAD_STORE
The operation is routed to the Load Store Unit (LSU); for example, load, store.
PROB_NOP
Sample only special no-operation instructions, which are called Probe NOP events.
[…]”
It looks like endbr64 is a 4-byte instruction. That could be a significant code size overhead for jump tables with lots of targets: https://godbolt.org/z/xTPToaddh
Also, is it required to insert endbr64 command after function calls (for return address)?
And no, as I understand it, this is only for branch/calls not returns.
They're usually called directly, but unless the compiler can prove that they always are (e.g., if they're static and nothing in the same file takes the address), endbr64 is required.
> Also, is it required to insert endbr64 command after function calls (for return address)?
No, IBT is only for jmp and call. SS is the equivalent mechanism for ret.
Then why not just have the compiler break down every non-static function into two blocks: a static function that contains all the logic, and a non-static function that just contains an IBT and a direct jump to the static function? (Or, better yet, place the non-static label just before the static one, and have the non-static fall through into the body of the static.) Then the static direct callsites won't have to pay the overhead of executing the IBT NOP.
With that said, the first few instructions of a called function often stall due to stack pointer dependencies, etc. so the true execution cost is likely to be even smaller than the above might suggest.
From a tooling perspective, what you're describing (two entrypoints for a function, the jump you mention is pointless) would require changes up and down the toolchain; it would affect the compiler, all linkers, all debuggers, etc. By contrast, just adding an additional instruction to the function prolog is relatively low-impact.
It's also worth noting that at the time code for a function is emitted, the compiler is not aware of whether the symbol will be exported and thus discoverable in some other module, or by symbol table lookup, so emitting the target instruction is essentially mandatory.
Show me the code -- better yet, submit it to the relevant projects! :)
int FooWithoutChecks(void *p);
int Foo(void *p) {
if (p == NULL) return -1;
return FooWithoutChecks(p);
}
In general the caller is expected to call Foo if they aren't sure if the pointer is nullable, or if they already know that pointer is not null (e.g. because they already checked it themselves) they can call FooWithoutChecks and avoid a null check that they know will never be true.The naive way to emit assembly for this is to actually emit two separate functions, and have Foo call FooWithoutChecks the usual way. But notice that the FooWithoutChecks function call is a tail call, so the compiler can use tail call optimization. To do this it would inline FooWithoutChecks into Foo itself, so the compiler just emits code for Foo with the logic in FoowithoutChecks inlined into Foo. This is nice because now when you call Foo, you avoid a call/ret instruction, so you save two instructions on every call to Foo. But what if someone calls FooWithoutChecks? Simple, you just call at the offset into Foo just past the pointer comparison. This actually just works because Foo already has a ret instruction, so the call to FooWithoutChecks will just reuse the existing ret. This optimization also saves some space in the binary which has various benefits in and of itself.
The example here with the null pointer check is kind of contrived, but this kind of pattern happens a LOT in real code when you have a small wrapper function that does a tail call to another function, and isn't specific to pointer checks.
1: https://learn.microsoft.com/en-us/windows/win32/secbp/contro...
For example, for any virtual function call or function pointer call, the destination must have a correct tag with the hash of the arguments. It's much more secure, and also faster, since loading the tag from memory can be merged with loading the actual code after it.
I wish this was the one implemented in hardware..
I've not yet been able to use XFG in any production software, due to the requirement of rebuilding every static linked library with it enabled. But it didn't seem to fall back to CFG when I was testing it in a toy program.
COMEFROM considered harm-mitigating
It ingeniously makes Return Oriented Programming (ROP) a lot harder.
You know, that’d be a fantastic OpenBSD release name.
Here’s hoping a dev sees this comment; there’s already been a few commenting in this thread.
He's unquestionably brilliant, but I've had a few encounters with him on the mailing lists and he is so quick to take offense where none was meant and drop into name-calling and insults. I don't really get it. He may have some deep insecurities.
You could not mention Linux at all, or you could even say "we think this is better than Linux's approach because of X" and it would be a great improvement.
I have always found it interesting that Rust purposefully avoided doing language comparisons - "we're better than Python like this and better than C like that". Their message purposefully avoided any positioning of it as a competition, instead focusing just on articulating Rust's value. It was an eye opening approach given our instinct is normally to pit things against each other.
Egos tend to become exaggerated when benevolent dictator types make public statements. Their candor and bluntness on a mailing list or Twitter may be completely different than their demeanor and their kindness toward collaborators in private.
Now we have the very public drama that happened between Theo and that "other BSD" team to create the original schism. But have we had any subsequent drama that caused breakups or forks? I don't know. OpenBSD manages to plug away and push releases out the door on schedule, right?
Linus doesn't seem to have a lot of internal contributor drama, judging by the way they also push releases out the door and merge pull requests and add features.
Really, if either Theo or Linus were unreasonable men, their teams would fall apart and they would cease to be leaders of anything. I think their leadership abilities speak for themselves: they've both been committed and dedicated to the same project since decades ago, and they've both built and maintained cohesive teams of contributors who seem to mostly stick around long enough to make a difference.
They are "thought leaders", if you will; perhaps not charismatic ones, but canny businessmen who know how to nurture their pet projects.
The problem is that for every Linus, Theo, or RMS, you have a dozen tactless buffoons who aren't a tenth as talented as any one of these individuals, are a chore to work with, and couldn't manage their way out of a paper bag. I've even seen some developers defend their lack of social skills by drawing comparisons to people like Linus and Theo.
That's why Linus shows an incredible amount of insight and maturity by purposefully and vocally trying to distance himself from that image and set a better example. He might be able to make being abrasive work, but most people can't.
-- Byrne Hobart, https://web.archive.org/web/20200909035546/https://diff.subs...
Is a Canadian kernel developer more like a POSIX operating system than a POSIX operating system is like a POSIX operating system?
I'm not sure I understand. Perhaps you meant to write "Linus" since Linus is also a kernel developer? That seems more like apples to apples.
The fact that Linux hasn't learned the right lessons in 20 years, and has chosen to "double down" in respect to IBT/BTI, does not inspire confidence that they will ever fix it. I'd say his 20 year estimate was in fact being pretty generous given the evidence available.
To clarify slightly, OpenBSD is fine with the very high probability that an executable will fail under new rules. Otherwise, yes.
Isn't it any indirect branch in any program that will trip BTI/IBT? So most programs? I guess I disagree with the `small probability ` part.
Unfortunately, for C code using GCC’s nested functions extension (or for languages that want to be ABI-compatible with C and support nested functions, like that paragon of advanced features called Pascal /s ), there’s no other compilation strategy in current ABIs. The patches to switch C (and not just Ada) to function descriptors[1] with an ABI break have been sitting on the GCC mailing list since approximately forever[2], but it doesn’t seem like there’s been any progress.
[1] The strategy is basically to compile (*fp)() not as
call *%rax
but as (untested) test $1, %rax
jz 1f
mov 8(%rax), %r10
mov (%rax), %rax
1: call *%rax
thus essentially inlining the (currently stack-allocated) closure calling thunk at all indirect call sites. It is ABI-compatible on x86 and x86-64 with all code that does not involve nested functions, place functions at odd addresses, or tag function pointers itself (and I think with all arm64 and riscv code, although arm32’s usage of the low pointer bit for Thumb interworking is bound to make this trickier).[2] https://gcc.gnu.org/legacy-ml/gcc-patches/2019-01/msg00735.h...
Now all software must pay the price and miss out on important mitigations, for all eternity, just because of some largely unused feature in one compiler?
I wouldn’t characterize this as a “largely unused feature in one compiler” screwing things up, but rather as the ABI on most Linux and -adjacent platforms (except SysV Itanium and FDPIC IIRC) being incapable of supporting closures (without executable stacks). That these are missing from standard C, and only present in languages that are either niche (Pascal, Ada) or don’t care about following the platform ABI (Rust, Go, C++’s lambdas), is a defect of C (and that’s at least a somewhat popular opinion among ISO C committee members[1]).
Of course, OpenBSD essentially does not have a stable ABI, so it’s much freer to experiment here.
[1] https://thephd.dev/lambdas-nested-functions-block-expression...
It's clearly different philosophies leading to different outcomes with neither of them clearly better than the other, it just depends on what you need. It would be possible to make that statement in a more graceful way.
It seems like a reasonable, relevant, and plausible assessment of how the long-term outcomes may likely differ between OpenBSD's stricter approach versus a looser approach, specifically when it comes to the degree of security offered (which is one of OpenBSD's main focuses), based on a past situation that's similar.
How do you know that you aren't being, to use your words, "quick to take offense where none was meant" in this case?
Past knowledge about Theo?
Truth is, Linux has a lot more constraints on how it can implement something because it has users. Users that have all sorts of different ways they need it to work.
Explains why he spends all his time developing mitigations
I actually think NetBSD is a pretty interesting alternative, it has some nice security features like veriexec that don't get talked about much.
I see tools like AppArmor as band-aids to fix problems that shouldn't exist in the first place. The problem with these approaches are the band-aids tend to break things in unexpected ways and when that happens they simply get removed and unused.
I fundamentally disagree on that. I think tools like that are amazing at protecting against unknown threats/exploits. They let you lock down software and protect against future unknown exploits, badly behaving software, malicious employees etc. I think something similar should be a part of any OS claiming to be security focused. Basic DAC is woefully insufficient.
On the other hand, the industry has largely found other solutions like sandboxing, but I still think MAC or RBAC or whichever has a place, certainly as art of a defense in depth strategy.
They provide plenty of mitigations (https://www.openbsd.org/innovations.html). In fact OP's article is for preventing unknown bugs from being exploited.
What makes you think otherwise?
And you're right, kind of; you need to set the permissions for apps, but that doesn't mean they need cooperation from the software developers. The whole point is that they don't. With those technologies you can lock down complex closed source programs, something not possible with pledge.
> The problem here is that trying to figure out what goes on this list is difficult for arbitrary programs, even when you’re the one writing it. When you’re just applying it to third party software it’s very likely something will not function correctly.
That's why there are things like, for example, SELinux permissive mode, where you run the software as needed and observe the permissions it needs, and then grant it those permissions while denying everything else.
Also, watching a program to see what it does is exactly the issue I’m talking about. You’re stuck with whatever behaviors you tested and everything else that you didn’t hit will fail (loudly if you’re lucky, silently if you’re not). There are platforms that do exactly what you’re talking about and believe me working on these rules is miserable. You’ll have reports on your desk like “the profiler doesn’t work anymore” (nobody tested this) or “on desktop controls don’t render anymore” (someone changed the implementation and it needs something you didn’t include in your rules). Again, this is when you control the stack, doing this for arbitrary programs is an order of magnitude harder.
I agree initial setup can be cumbersome, but I think it's worthwhile. I'm a fan of RSBAC personally, it's as powerful as SELinux but a lot simpler. If people run in permissive mode and test properly, not just run it and do a few things, but test every function exhaustively before setting up permissions, it should be good.
Really, it only has to be done once, and I think it's a worthwhile investment given the security gained.
That's what I was saying higher up in the thread though. OpenBSD is known for having good, simple implementations of complex stuff like this, so if they ever were itnerested in implementing a version, it would probably be amazing.
Which is sad, as I kinda like the *BSD approach to things
Also I was pleasantly surprised to hear they support Apple M1/M2 Macs. Asahi Linux gets a lot of press around here but I had no idea OpenBSD supported it.
Could you give some examples/samples of things you have ran into off the top of your head?
I could go on, but, for my needs, it works very well and some of its simplicities are a godsend.
It would be more helpful if callers would store some machine specific hash of the function prototype and the function itself would check the hash, so that you could only redirect to calling a function with the right signature.
But that would also increase the overhead further. Already this is bad enough that it makes jump tables unattractive (which is too bad, considering the usually jump tables have little to no risk of control flow redirection).
Using better languages makes the entire problem disappear. You don't get a stack smash, the resulting opportunities for remote code execution disappear.
It suggests that maybe the "C magically shouldn't have Undefined Behaviour" people were onto something after all. Maybe C programmers really are so wedded to this awful language that just being much slower than Python wouldn't deter them. There is still the problem that none of them can agree how this should work, but if they'll fund it maybe it's worth pursuing to find out how much they will put up with to keep writing C.
Try to work this through in your head. Imagine how you need to specify the working of the abstract machine in order to allow this. How do we talk about an "instruction pointer" on the abstract machine? What are the instructions it's pointing to? Am I defining an entire bytecode VM?
Nah, instead you're going to do one of two things. One: "Undefined Behaviour" which we explicitly took off the table, or Two: "If this happens the program aborts". And with that the big problem evaporates. Does it make those C programmers happy? I expect not.
In languages like C that are closer to the machine, not everything has to be specified strictly in terms of a generic abstract machine.
I'm not trying to be hostile or evasive or derisive, I'm just genuinely responding to your original comment, that I think missed on some important info. And my point was that if we imagine a different world from the real world we're in right now, where in this new world, all undefined behavior became implementation defined behavior, then there would still be a need for mitigations like endbr64. So I'm not painting a rosy picture for C. I just think undefined behavior is a red herring. Assembly doesn't have undefined behavior, but obviously you can have all sorts of issues there.
The machine is in the real world and is thus obliged to have some actual behaviour, but it is not always practical to discern what that behaviour would be let alone make it reliable across a product line and document it in an understandable way. As a result actually your CPU's documentation does in effect include "Undefined Behaviour".
To be clear, unlike the comment root, I don't think this particular hardware mechanism obviates the need/benefits of related software mechanisms. But in terms of cost/benefit/applicability, endbr64 type mechanisms look pretty good all around.
An advantage of the software solution is that you don't need to have the feature compiled into every library for it to work, you just lose protection in those parts. That makes for a much quicker rollout. Also faster iteration times, in the Windows Insider Preview you can get the extended version that also checks that the hashed function signature matches.
1: https://learn.microsoft.com/en-us/windows/win32/secbp/contro...