-
Notifications
You must be signed in to change notification settings - Fork 17.9k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
runtime: nil-dereference panic refers to addr=0x8 #56568
Comments
I'd expect |
As you say, this is how the language works. It would be quite subtle for the language to behave differently depending on whether the embedded field is the first field in a struct. In any case, the way to propose a language change is the proposal process described at https://go.dev/s/proposal. However, iIn this case I think it is unlikely that the proposal would be accepted. |
@cherrymui Thank you, indeed I missed that it's actually guaranteed by the spec. Having said that, I didn't find references in the spec about how exactly the promoted methods must be implemented. The spec currently specifies:
There's no explicit mention that The reason it caused a confusion for me is closer to this example: package main
import "unsafe"
type A struct {
F int
next *A
}
func (a *A) NextA() *A {
return (*A)(unsafe.Pointer(a.next))
}
func (a *A) Foo() {
if a != nil {
a.F++
}
}
type B struct {
A
}
func (b *B) NextB() *B {
return (*B)(unsafe.Pointer(b.NextA()))
}
func main() {
var a *A
a.Foo()
var b *B
c := b.NextB()
c.Foo()
} This might be a different bug, or consequences of compiler optimization, but this is what can be observed here:
To be clear, it still panics, but for a different reason: de-reference of Thus, going back to this example, either the spec should allow evaluation of So @ianlancetaylor , I'm still not exactly sure how to proceed with this report. Should I then open a separate bug report for the second example? Should I open a proposal to clarify the spec in a way that allows promoting nil-protected methods with this example (e.g. propose an offset-independent implementation)? Both? In fact, in the real program was able to pass the |
That second case does seem like a potential quality of implementation issue. gccgo reports
That is, I think we agree that the program should crash, but the crash report from the gc compiler could be clearer as to where the bad nil dereference occurs. Go 1.4 reports
but after that it gets more confusing due to the fact that older gc releases were not as reliable at reporting failures in inlined functions. At least I think that's the reason, I didn't look too deeply. The reference to Reopening in case this is something we want to fix in the gc compiler. |
In the example above, both |
Possibly related to #42673 |
Yeah, the fix for #42673 fixes this one as well. |
Ok, this (the part that Ian reopened the issue about) should get fixed in 1.21 then. |
Change https://go.dev/cl/270940 mentions this issue: |
Convert the scheduling pass from scheduling backwards to scheduling forwards. Forward scheduling makes it easier to prioritize scheduling values as soon as they are ready, which is important for things like nil checks, select ops, etc. Forward scheduling is also quite a bit clearer. It was originally backwards because computing uses is tricky, but I found a way to do it simply and with n lg n complexity. The new scheme also makes it easy to add new scheduling edges if needed. Fixes #42673 Update #56568 Change-Id: Ibbb38c52d191f50ce7a94f8c1cbd3cd9b614ea8b Reviewed-on: https://go-review.googlesource.com/c/go/+/270940 TryBot-Result: Gopher Robot <gobot@golang.org> Reviewed-by: Keith Randall <khr@google.com> Run-TryBot: Keith Randall <khr@golang.org> Reviewed-by: David Chase <drchase@google.com>
Well, with the CL above it still panic with addr=0x8. The SSA dump before scheduling is
Basically, after inlining and constant folding, the compiler is smart enough to know that B is at constant address 0 and A is at constant address 8. We issue a nil check for B, but there is no scheduling edge between that nil check and the constant 8. Usually if a pointer is derived from another pointer, that derivation (pointer addition, or folded into a load with offset) is scheduled later than the nil check. But in this case the pointer is really a constant, which can be scheduled arbitrarily (and happened to be before the nil check in this case). I don't see an easy way to enforce scheduling order for nil check vs. constants. If we know statically a pointer is constant nil, maybe we can make derived pointers all constant nil, i.e. |
Ideally, if the compiler/runtime can enforce I hope that these cases with constants can start a larger discussion for possible spec change/clarification. |
Change like |
I think CL 537775 may help with the ordering here? It enforces ordering between a nil check and subsequent dependent loads. I'm not entirely sure what the bug is anymore. We certainly allow
|
@randall77 If I recall correctly, it was not about offset It's that the nil check from the method was removed/reordered, allowing passing From the description of https://go.dev/cl/537775 that you mentioned, it may indeed fix the root cause. I will check it a bit later. |
The example mentioned in #56568 (comment) panics with the stack trace below.
Specifically, it panics in On tip, it panics with
I.e. panics in Is this the expected panic? If so, we can call it fixed. Thanks. |
Yes, this was exactly the issue I was having. Thank you! Closing it then. |
What version of Go are you using (
go version
)?Does this issue reproduce with the latest release?
Yes, confirmed via playground.
What operating system and processor architecture are you using (
go env
)?go env
OutputWhat did you do?
Run the following program:
Link to a playground.
What did you expect to see?
No panic for both method calls.
What did you see instead?
Panic on the
b.Foo()
method call:Discussion
Technically, I would expect
b.Foo()
to be equivalent to(*A).Foo(nil)
in this case, at least when the struct offset of embeddedA
field is zero.It is clear that the following would case panics in any case (struct offset is not zero, equivalent to
(*A).Foo(0x8)
):As well as this code (always requires pointer de-reference):
If I understand correctly, as of now the behavior of
b.Foo()
is similar to(*A).Foo(&b.A)
, where firstb.A
causes an operation analogous to pointer de-reference. I think this is a surprising behavior that makes it hard to embed structs (promoting methods) and make the new type nil-safe.The only non-error-prone way to fix it currently, is to make a named type instead of embedding and manually expose all the methods:
As I mentioned previously, I see no technical reason why the compiler cannot call
(*A).Foo(nil)
instead of requiring an additional boilerplate. Otherwise it defeats the purpose of struct composition, when nil-safety is required for promoted methods.I believe this change doesn't break the backward compatibility promise, only makes more programs safe(er).
Open questions are:
(*A).Foo(0x8)
be translated to(*A).Foo(nil)
as well, so that the struct offset doesn't matter? This complicates the implementation, so likely is unnecessary.I admit this is not a bug per-se and works "as intended", however it's surprising enough to be worth fixing. Also, when using
unsafe
/cgo, I was able to somehow confuse the compiler to emit case similar to(*A).Foo(0x8)
(thus skipping the initial nil check). I will try to provide a reproducer for it as well.The text was updated successfully, but these errors were encountered: