--
You received this message because you are subscribed to the Google Groups "cxx" group.
To unsubscribe from this group and stop receiving emails from it, send an email to cxx+uns...@chromium.org.
To view this discussion visit https://groups.google.com/a/chromium.org/d/msgid/cxx/CAFDkGJPZogOm8Esko_7hZUc_YWja1%3DFRHoeezf2cyPTdEW685g%40mail.gmail.com.
In general if you're working in code that's not a hotspot then there's not much harm in CHECKing your expectations, it may save you debugging time down the line. In developer / debug builds you also get better error messages if that's ever violated rather than segfault and on official builds where it's cheap unless used in an extremely hotspot (and you probably wanna find out that it's a problem rather than proactively avoid it).On Sat, Dec 14, 2024 at 3:06 AM Wojciech Dzierżanowski <wdzierz...@opera.com> wrote:Hi,
Every now and again a discussion pops up about constructs like
void DoSomethingWithT(T* t) {
CHECK(t); // To CHECK or not to CHECK?
t->f();
}There used to be guidance in Chromium that explicitly called this out as redundant CHECKing. But then it was removed and the reason is unclear to me.
I can think of arguments for and against CHECKing. For example:+ Dereferencing a null pointer is undefined behavior per the standard.
+ Even if we're expecting a crash, it may happen several levels above DoSomethingWithT() in the frame stack (upon attempting to access data members of T).
----- Accessing memory at the address 0x0 or close to it is guaranteed to crash on the platforms we target.- Assuming we're guaranteed to crash anyway, preceding every such dereference with a CHECK is a lot of CHECKing that is not strictly necessary.
Is either choice generally recommended in Chromium or should people weigh the pros and cons on a case by case basis?Hopefully, the quandary is going to be less and less common. If DoSomethingWithT() is written according to the current Google C++ Style Guide, it becomes:
void DoSomethingWithT(T& t) {
t.f();
}
and the question of CHECKing becomes moot. However, given that most of the existing code in Chromium predates the guidance to take never-null arguments by reference (and return never-null objects by reference), the new signature of DoSomethingWithT() can easily only move the question lower in the frame stack: The t reference can still potentially be formed by dereferencing a null pointer.--Wojtek
You received this message because you are subscribed to the Google Groups "cxx" group.
To unsubscribe from this group and stop receiving emails from it, send an email to cxx+uns...@chromium.org.
To view this discussion visit https://groups.google.com/a/chromium.org/d/msgid/cxx/CAFDkGJPZogOm8Esko_7hZUc_YWja1%3DFRHoeezf2cyPTdEW685g%40mail.gmail.com.
You received this message because you are subscribed to the Google Groups "cxx" group.
To unsubscribe from this group and stop receiving emails from it, send an email to cxx+uns...@chromium.org.
To view this discussion visit https://groups.google.com/a/chromium.org/d/msgid/cxx/CAGFX3sGgOWdrmiJxbH9wbsVEkBQMPUYpiySz6zQhCHXSLhFv7w%40mail.gmail.com.
--
Because we compile with (the badly named) -fno-delete-null-pointer-checks flag, clang treats null-pointer deref as defined behavior.
Thank you all for replying!Because we compile with (the badly named) -fno-delete-null-pointer-checks flag, clang treats null-pointer deref as defined behavior.
Maybe it's also badly documented :), but looking at the docs and related Chromium tasks (as no expert on this by any means), I get the impression it's the other way around (please correct me if I'm confused):
We assume a null pointer dereference crashes deterministically on the platforms we target. Furthermore, we want to be able to rely on this behavior so we tell the compiler not to optimize around a null pointer dereference being undefined behavior. In particular, with a base::WeakPtr referencing a destroyed object we really prefer a reliable crash over a UaF. The latter is possible (or at least was possible before hardening?) if the compiler removes a code block guarded by a null pointer check
that it considers garbage due to it only being entered when the pointer is null and it sees we're dereferencing it later. Without -fno-delete-null-pointer-checks the compiler is free to remove it -- just as it's free to remove any code that the standard says would execute within the realm of undefined behavior.
Most importantly, whether it's this or the other way around, the bottom line doesn't change: We assume a null pointer dereference is a sure crash -- when it results in an attempt to access memory at 0x0 (+ a small offset).
The responses people have provided seem to converge on the following reasoning wrt CHECKing before a dereference:
Since a null pointer dereference a sure crash, it's not strictly necessary to CHECK as there is no security risk. At the same time, CHECKing is typically not harmful and can be helpful for debugging, so feel free to do it (modulo hotspots). Try to place any such CHECKs strategically (at API boundaries) to avoid spamming the code with CHECKs.
Does that sound like a fair summary?
On Mon, Dec 16, 2024 at 4:18 AM Wojciech Dzierżanowski <wdzierz...@opera.com> wrote:Thank you all for replying!Because we compile with (the badly named) -fno-delete-null-pointer-checks flag, clang treats null-pointer deref as defined behavior.
Maybe it's also badly documented :), but looking at the docs and related Chromium tasks (as no expert on this by any means), I get the impression it's the other way around (please correct me if I'm confused):This is the documentation for -fdelete-null-pointer-checks whereas we pass -fno-delete-null-pointer-checks. Our flags make null pointer deref into *defined* behavior, i.e. the compiler assumes that data can live at null.
We assume a null pointer dereference crashes deterministically on the platforms we target. Furthermore, we want to be able to rely on this behavior so we tell the compiler not to optimize around a null pointer dereference being undefined behavior. In particular, with a base::WeakPtr referencing a destroyed object we really prefer a reliable crash over a UaF. The latter is possible (or at least was possible before hardening?) if the compiler removes a code block guarded by a null pointer checkThis is where it is badly named. :) There is no null check. In this WeakPtr case (pre-hardening), there is a branch that stores null to a pointer, and later we unconditionally dereference that pointer. Without -fno-delete-null-pointer-checks the compiler would assume that because the null store leads to undefined behavior (derefing null), it can just assume that branch is never taken and delete it (this is critical to lots of compiler optimizations when inlining code). But then we never overwrote our dangling pointer with 0, and so a UAF results when we deref the dangling pointer.
Does that sound like a fair summary?Yep this sounds good to me!
Do people think it would be worthwhile to bring back some guidance on this to styleguide/c++/checks.md?
The thing that prompted this thread was observing recurrent internal discussions on the use of CHECKs with pointers before dereferencing.
probably the only bit I'm finding to be potentially worth adding to styleguide/c++/checks.md is the clarification that undefined behavior cannot be evoked as reason to CHECK before a pointer deref in Chromium.
--
You received this message because you are subscribed to the Google Groups "cxx" group.
To unsubscribe from this group and stop receiving emails from it, send an email to cxx+uns...@chromium.org.
On Tue, Dec 17, 2024 at 1:03 AM Matt Denton <mpde...@google.com> wrote:On Mon, Dec 16, 2024 at 4:18 AM Wojciech Dzierżanowski <wdzierz...@opera.com> wrote:Thank you all for replying!Because we compile with (the badly named) -fno-delete-null-pointer-checks flag, clang treats null-pointer deref as defined behavior.
Maybe it's also badly documented :), but looking at the docs and related Chromium tasks (as no expert on this by any means), I get the impression it's the other way around (please correct me if I'm confused):This is the documentation for -fdelete-null-pointer-checks whereas we pass -fno-delete-null-pointer-checks. Our flags make null pointer deref into *defined* behavior, i.e. the compiler assumes that data can live at null.Yes, I realize that we pass -fno-delete-null-pointer-checks :). The point that I was trying to make is that the assumption that a null pointer dereference crashes deterministically (on our target platforms), i.e., that it's defined behavior for us, comes first. It's how our target platforms work. -fno-delete-null-pointer-checks has no effect on whether a null pointer dereference crashes predictably or not; our platforms have. (It does have an effect on optimizations that would otherwise be made on the compiler's default, standard-compliant assumption that a null pointer dereference is undefined behavior.) So if we're looking for a reason to be able to say "CHECKing before a deref is not strictly necessary", the reason is because our platforms are such that attempting to access memory close to the address 0x0 triggers a fault. The fact that we feel confident passing -fno-delete-null-pointer-checks is another consequence of this behavior.Does that make sense at all?
On Tue, Dec 17, 2024 at 3:47 AM Wojciech Dzierżanowski <wdzierz...@opera.com> wrote:On Tue, Dec 17, 2024 at 1:03 AM Matt Denton <mpde...@google.com> wrote:On Mon, Dec 16, 2024 at 4:18 AM Wojciech Dzierżanowski <wdzierz...@opera.com> wrote:Thank you all for replying!Because we compile with (the badly named) -fno-delete-null-pointer-checks flag, clang treats null-pointer deref as defined behavior.
Maybe it's also badly documented :), but looking at the docs and related Chromium tasks (as no expert on this by any means), I get the impression it's the other way around (please correct me if I'm confused):This is the documentation for -fdelete-null-pointer-checks whereas we pass -fno-delete-null-pointer-checks. Our flags make null pointer deref into *defined* behavior, i.e. the compiler assumes that data can live at null.Yes, I realize that we pass -fno-delete-null-pointer-checks :). The point that I was trying to make is that the assumption that a null pointer dereference crashes deterministically (on our target platforms), i.e., that it's defined behavior for us, comes first. It's how our target platforms work. -fno-delete-null-pointer-checks has no effect on whether a null pointer dereference crashes predictably or not; our platforms have. (It does have an effect on optimizations that would otherwise be made on the compiler's default, standard-compliant assumption that a null pointer dereference is undefined behavior.) So if we're looking for a reason to be able to say "CHECKing before a deref is not strictly necessary", the reason is because our platforms are such that attempting to access memory close to the address 0x0 triggers a fault. The fact that we feel confident passing -fno-delete-null-pointer-checks is another consequence of this behavior.Does that make sense at all?Good point, yes this makes sense to me.
We assume a null pointer dereference crashes deterministically on the platforms we target. Furthermore, we want to be able to rely on this behavior so we tell the compiler not to optimize around a null pointer dereference being undefined behavior. In particular, with a base::WeakPtr referencing a destroyed object we really prefer a reliable crash over a UaF. The latter is possible (or at least was possible before hardening?) if the compiler removes a code block guarded by a null pointer checkThis is where it is badly named. :) There is no null check. In this WeakPtr case (pre-hardening), there is a branch that stores null to a pointer, and later we unconditionally dereference that pointer. Without -fno-delete-null-pointer-checks the compiler would assume that because the null store leads to undefined behavior (derefing null), it can just assume that branch is never taken and delete it (this is critical to lots of compiler optimizations when inlining code). But then we never overwrote our dangling pointer with 0, and so a UAF results when we deref the dangling pointer.Right. I tend to think about this in terms of actual null checks, but in this case we just overwrite a pointer upon destruction. Thanks for clearing this up!Does that sound like a fair summary?Yep this sounds good to me!Awesome!Do people think it would be worthwhile to bring back some guidance on this to styleguide/c++/checks.md?--Wojtek
--
You received this message because you are subscribed to the Google Groups "cxx" group.
To unsubscribe from this group and stop receiving emails from it, send an email to cxx+uns...@chromium.org.
On Tue, Dec 17, 2024 at 4:19 PM 'Matt Denton' via cxx <c...@chromium.org> wrote:On Tue, Dec 17, 2024 at 3:47 AM Wojciech Dzierżanowski <wdzierz...@opera.com> wrote:Yes, I realize that we pass -fno-delete-null-pointer-checks :). The point that I was trying to make is that the assumption that a null pointer dereference crashes deterministically (on our target platforms), i.e., that it's defined behavior for us, comes first. It's how our target platforms work. -fno-delete-null-pointer-checks has no effect on whether a null pointer dereference crashes predictably or not; our platforms have. (It does have an effect on optimizations that would otherwise be made on the compiler's default, standard-compliant assumption that a null pointer dereference is undefined behavior.) So if we're looking for a reason to be able to say "CHECKing before a deref is not strictly necessary", the reason is because our platforms are such that attempting to access memory close to the address 0x0 triggers a fault. The fact that we feel confident passing -fno-delete-null-pointer-checks is another consequence of this behavior.Does that make sense at all?Good point, yes this makes sense to me.I don't think this is quite right. It is not true that dereferencing null at the language level will necessarily crash as a null dereference when lowered to hardware. Sometimes the compiler does not actually emit code to dereference the pointer, e.g. if you bind null to a reference and happen not to use it, or call a non-virtual method that happens not to dereference this. (Or maybe the compiler decided to delete the memory read and reorder stuff.) When getting the UBSan bot green, fixing instances of this was probably one of the bigger time sinks, and frequent regressions.
That means the CHECK is not actually redundant. NB: I'm just saying that it's not redundant. I'm not arguing that we should therefore aggressively CHECK everything. That sounds kinda tedious and will probably result in a bunch of extra runtime checks.
I suspect null-safety is better handled at the type system, though we don't have great tools for that in C++.
> The fact that we feel confident passing -fno-delete-null-pointer-checks is another consequence of this behavior.I think it's the exact opposite. :-) We pass it because this behavior is not true.If we knew that all language-level null derefs reliably lowered to hardware-level null derefs and therefore had defined trapping behavior, we would be perfectly comfortable letting the compiler exploit this fact to optimize code. However, we are not confident of this and, in fact, we know that language null derefs are sometimes left unchecked when the compiler lowers to hardware. See the examples listed above.Now, because there are unchecked null derefs, that means these compiler optimizations might arbitrarily mangle our code when we get it wrong, risking security vulnerabilities for our users. And so we tell the compiler that it cannot reason about the language's null deref preconditions, because we would rather have safety here over performance. (Also I suspect we're not really losing out on that many optimizations here. Although, ironically, the optimizations it would probably help with the most are deleting defensive null CHECKs. :-D)