RFC: C++ Buffer Hardening
discourse.llvm.org
discourse.llvm.org
Some non template helpers might be improved of course.
That has been a thing since the Windows 16 bit days, and other OSes with similar linking models like Symbian, OS/2 and AIX.
In the old days, all C++ frameworks bundled with compilers tended to have bounds checking enabled by default (Turbo Vision, OWL, VCL, MFC, PowerPlant, CSet++, Qt,...).
Nowadays I always set _ITERATOR_DEBUG_LEVEL to 1 on VC++, unless I am not allowed to.
Mostly the performance problems that were dealt with, had to do with badly chosen algorithms and data structures.
Modern Android ships with FORTIFY enabled.
https://android-developers.googleblog.com/2017/04/fortify-in...
If you just link against an hardened library, only the implementation of the (non inlined) library calls will be hardened, the user code itself won't see much benefits.
Of course these days we have binary level optimizers, so who knows what it is possible in principle?
But the butthurt side of me wants to say: let those who wish for unsafety be unsafe.
You would not say: Let those [civil engineers] who like unsafe bridges build unsafe bridges
Because the people using them might not be able to judge the unsafety of the system. Because the work of programmers multiply we have a huge responsibility and a huge chance to improve the world.
Edit: and there is no way to prevent that damage except by detailed planning and by stopping to cut when the patient (under local anestesia) ceases talking or performing their craft. Other damage can only be detected later though.
It’s like you have gone out of your way to prove the opposite point of what you were ostensibly trying to prove.
I really doubt we are every going to see a "hardened C++" making any inroads, but at least I find that slightly more plausible than any other new language displacing it.
Trying to do otherwise will trap.
If your system provides a special allocator that actually does tell the hardware which allocations correspond to which object then that's nice (and not a new thing... many lisp machines had this, plus x86 segmentation, etc.), but it really does not prevent and cannot prevent a process using the allocated memory however it sees fit. The only thing that could is throwing turing-completeness out of the window.
Yes, (ancient) x86 segments are a form of memory tagging. I always found it ironic that many people complain about "the PDP-11 memory model" and yet we already had a popular architecture trying to get away from it.. only to result in one of the most hated memory models ever. While you can definitely do better, I am skeptical that any new type of memory tagging is going to improve significantly on it.
I think pjmlp is referring to this: https://msrc-blog.microsoft.com/2022/01/20/an_armful_of_cher...
In order to create a system where you can't have a way to avoid bounds checking (as the above post was claiming), you would basically have to throw turing-completeness away, e.g. by preventing the implementation of arrays, lists, etc. The moment you can implement practically any data structure whatsoever, you can use it to build an allocator on top of it (through a heap... or not), and then proceed to access each (sub)object allocated by it without any bounds checking whatsoever. From the point of view of the hardware/OS, you will only have one large object. This is literally how most user programs work today (from the point of view of the hardware/OS).
You can provide a system allocator that tags objects separately as you allocate, but there is no way to prevent a user program from managing its own memory as it sees fit.
Pretty much possible and already available for those listed above, with Solaris having a decade of experience in production.
However it is now being combined with ARM technology to provide such capabilities in selected Windows devices.
https://blogs.windows.com/windowsexperience/2022/02/28/mwc-2...
If a process wants to commit suicide, there is hardly anything that can be prevented I guess.
These measures prevent access to the tagging metadata from userspace, that is how they prevent it, and usually MMU isn't accessible to userspace.
> By using spare bits in the cache and in the memory hierarchy, the hardware allows the software to assign version numbers to regions of memory at the granularity of a cache line. The software sets the high bits of the virtual address pointer used to reference a data object with the same version assigned to the target memory in which the data object resides.
> On execution, the processor compares the version encoded in the pointer and referenced by a load or store instruction, with the version assigned to the target memory.
https://docs.oracle.com/cd/E86824_01/html/E54766/adi-3c.html...
Now if one goes out of their way to work around security mitigation and enjoy the freedom of corrupting their data, maybe they should ask themselves if they are on the right business.
You mean PAC. PAC does not really do what you think it does; the original program can still corrupt its data as much as it wants (even due to bugs). And I don't think it has anything to do with Pluton.
> If a process wants to commit suicide, there is hardly anything that can be prevented I guess.
> Now if one goes out of their way to work around security mitigation and enjoy the freedom of corrupting their data, maybe they should ask themselves if they are on the right business.
No; what I have been trying to do for the entire comment chain is to show that this is how things work _right now_ (the entire process' memory being a single opaque object/blob with the same tag), and that for obvious reasons you cannot forbid that and just claim that "there will be NO alternative" because you will always be able to continue doing what everyone is doing _right now_, barring a significantly upheaval in the traditional definitions of computing.
If you provide an allocator that integrates more closely with the hardware then that is all and fine, but you just _cannot_ claim there will be no alternative specially when the alternative is just to continue what you're doing right now.
Again, we've had many architectures with memory tagging, hardware bounds checking, whatever you can think of. E.g. even x86 had memory tagging at a time (386 segmentation which was globally panned), hardware bounds checking (MPX, completely ignored due to the reduced performance), etc.
And is one of the drivers to have memory hardware tagging enabled in all devices, just like Microsoft is doing with Pluton, Oracle has been doing for a decade with Solaris SPARC, and Apple is adding to their ARM flavours as well.
What stops the engineer from building bridges are the law regulations and his work environment. This regulations and work environment exists in programming too.
Regulations and work environments seem to not exist to a sufficient degree in programming to prevent the sort of errors that efforts like from TA are directed against. The attitude of many programmers towards testing would be considered reckless in many other domains.
On the other hand, compilers and IDEs have very much become part of our work environment and should help as much as feasible in avoiding as many errors ahead of time.
We might also put construction engineering on too much of a pedestal here. Bridges are very well-defined artifacts that humans have been building for centuries. How well exactly are we doing with other infrastructure projects?
* library/language side: some domain of bullet proof memory
* compiler/language side: move build dependencies into the compiler or something that frontends the compiler ala GO/Rust with a flag to enforce it so it's optional. Here don't let perfect be the enemy of the good. There'd need be some new keywords or namespaces to make this work.
That'd be superb
I have all kinds of chances and opportunities to learn Rust but our eco system is endless C/C++. So I'd rather not deal with another complicated language. There's no near term or medium term timeline to move even 20% into Rust. I bail out to GO for I/O bound work.
I believe the entire MS kernel is written using this.
- a bound checked libc++, which is not particularly exciting, libstdc++ has had _GLIBCXX_ASSERTIONS for a while for example. In fact it seems that libc++ has had it as well.
- clang warnings to flag potentially unsafe code plus fix-it suggestions to convert it to safer code using the bound checked libc++.
The later is quite interesting, especially the fix-it might make it easier to incrementally "harden" a large c++ code base.
Agree the second one both sounds interesting and also quite disruptive to existing code.
The warning about pointer arithmetic is most likely suppressable per compilation unit (for things like tagged pointers).
> “we are going to pessimize all C++ software”
Could you clarify?
Hence you can’t make the change incrementally, which is a big risk.
Concerning Linux distributions, I seriously doubt if they have resources to assess the performance impact across the wide range of software.
Concerning performance impact, we end up with more instructions and more memory accesses and increased register pressure. This has a chance to make things slower.
On the other hand if you change the layout of structures (like it would be the case for bound checked pointers), it is much much harder. GCC has ABI tags, but they only help a bit.
In any case nobody is going to bound check iterators by default, these already exists when using whatever debug standard library your compiler provides and they are orders of magnitude slower than unchecked iterators.
sickosyes.jpg
Getting bad operations to abort execution (e.g. by throwing an exception) seems impossible to ship, not in the least due to certain, uh, entities really being against it. But abusing UB to allow people to turn on checks is excellent. This is why we should be very careful about what we choose to standardize-a common request is making signed overflow wrap instead of being UB, which prevents its diagnosis in this way.
The fact the UB is still being used as an excuse to clearly undermine reasonably expected behaviour remains a BS trope, and WG21's continued acceptance of new features and specifications that say perfectly specifiable behaviour is UB instead of unspecified is a continuing source of avoidable security bugs.
It is inexcusable for new features to be added to C++ that have UB in any case other than the undefinable (invalid memory access, etc). Removing the copious unnecessary UB from the existing specs should be a higher priority that many of the new features being proposed.
I don't actually oppose, in the long run, slowly picking some behaviors and changing them to be standardized. But I do vehemently oppose many of the suggestions that are brought up. The kinds of things that should be moved out of being undefined are things like "you named your function isfoo" and not "you passed NULL to memset".
Can you provide an example of currently specified behaviour that should be UB?
To be very clear I am using specification language here. "Unspecified behaviour" is very much not "undefined behaviour".
C++ does not add runtime overhead to validate that programs are well-formed.
You have succinctly described the problem.
If you want a language that is slow, there are plenty of options.
This is an example of something that cannot be anything other than UB.
In practice I dare you to find compiler documentation for each implementation defined behavior.
https://timsong-cpp.github.io/cppwp/n4868/intro.defs#defns.u...
https://timsong-cpp.github.io/cppwp/n4868/intro.defs#defns.i...
edit:
C wording:
For gcc there is this:
https://gcc.gnu.org/onlinedocs/gcc/C-Implementation.html
No idea how complete it is. Also a lot of stuff is architecture/platform specific, not compiler specific, so you won't find it in the general compiler docs but you have to look at the psABI.
All of undefined/unspecified/implementation-defined allows this.
edit:
Whether unspecified or implementation-defined would allow trap depends on the exact wording. If the wording is along the lines of "[on overflow] the value of the expression is unspecified/implementation-defined" then trap is not allowed. But it can be also "[on overflow] the behavior of the program is implementation-defined", which would also allow trap. Point being, unspecified/implementation-defined also have a defined scope, while undefined behavior always just spoils the whole program.
I'm fine with the UB overflow though.
You're basically defining undefined behavior. The way to do this "properly" would be "the program is allowed to do x, y, z, or also trap"–there's really not much else you can do.
For example bound checking, while fairly cheap for serial code, makes it very hard to vectorize code effectively. Then again so does IEEE FP math and that's why we have -ffast-math...
In the end, it is not impossible, it is just tradeoffs.
I'm generally on the opinion that there should be as little UB as possible, but probably this is better done by compilers that can push the limit of static and dynamic checking technology instead of the standard mandating it.
That's why I didn't bother with "the behavior of the program is unspecifed", because that really is just undefined behavior.
But yeah, actually listing allowed behaviors and stating that the actual behavior out of them is unspecified/implementation-defined is more practical.
One example is conversion to signed integer in C17:
> ...either the result is implementation-defined or an implementation-defined signal is raised.
You're not going to get that. It would make these proposed safety checks unshipably slow. Languages that have bounds checked array access by default rely on the compiler/JIT being able to recognize in the common case that those branches are dead code & eliminating them. Those "magically disappearing null checks" are in the same category. Mandating that they cannot be optimized away, even though the compiler can "prove" that they are not necessary, isn't going to fly.
That's why this proposal isn't suggesting throwing an exception - think brk or int3, which is essentially what other bounds checked languages like rust do.
This also isn't mandating that the check can't be thrown away. What it is doing is changing UB to unspecified/implement defined behavior. The difference is important: when something is UB the compiler is free to say "UB is not valid, therefore any preceding branches that would result in UB cannot happen" and then remove them. With unspecified behaviour the compiler only loses the ability to make assumptions based on "this UB path cannot be taken".
C and C++ both have a problem in overuse of undefined vs unspecified behaviour. Take integer overflow: there is no reason that that should be UB, on all hardware it has well defined behavior, and the impact of pretending that this isn't the case has been numerous security exploits over the year. Unspecified behaviour does not (by definition) mean that the spec has any prescriptive behavioral rules beyond "this must be consistent across all sites". For example integer overflow should always be 2's complement or it should always trap, the compiler doesn't get to mix and match, and doesn't get to pretend overflow doesn't happen.
They're not expensive because they are optimized away. Branches are still relatively expensive in general. Prediction only goes so far.
> Take integer overflow: there is no reason that that should be UB
Yes there is, it allows for size expansion of the type to match the current architecture. Like promoting a 32 bit index into a 64 bit index for optimal array accesses on a 64 bit CPU. Which you can't do if you're needing to ensure it wraps at 32 bits.
It's not the how it overflows that's the problem, it's the when it overflows that is. Defining it means it has to happen at the exact size of the type, instead of at the optimal size for the usage & registers involved.
> and the impact of pretending that this isn't the case has been numerous security exploits over the year.
And many of those exploits still exist if overflow is defined, because that the overflow happened was the security issue, not the resulting UB failure mode. UB helps here because now it can categorically be called a bug, and you can have things like -ftrapv. That unsigned's wrap is defined and not UB is actually also a source of security bugs - things like the good ol' `T* data = malloc(number * sizeof(T));` overflow bugs. There's no UB there, so you won't get any warnings or UBsan traps. Nope, that's a well-defined security bug. If only unsigned wrapping had been UB that could have been avoided...
But if you want code that is hyper consistent and behaves absolutely identically everywhere regardless of the hardware it's running on (which things like crypto code does actually want), then C/C++ is just the wrong language for you. By a massive, massive amount. Tightening up the UB and pushing some stuff to IB doesn't suddenly make it a secure virtual machine, after all. You're still better off with a different language for that. One where consistency of execution is the priority, abstracting away any and all hardware differences. But that's never been C or C++'s goal.
C/C++ do have problems with UB. But int overflow isn't actually an example of it. And unsigned's overflow being defined is also really a mistake, it should have been UB.
Acting in a crash-predictable way on UB: abuse
A strange world this be.