-
Notifications
You must be signed in to change notification settings - Fork 17.5k
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
proposal: add GOEXPERIMENT=checkptr #22218
Comments
Virtual machines are perhaps rare, but they may use code making such conversions every few nanoseconds and in that case the performance would be hurt substantially. |
The example you give is one that could be caught at compile-time (e.g. by |
Note that in C, converting an arbitrary integer to |
True, yet unfortunately some well known and widely used C code bases do that (and not only to void*). For example SQLite, including magic-valued function pointers, IIRC. |
I assume you're talking about language-level virtual machines here, where the implementation is in Go. Why would they require constant conversions between uintptr and unsafe.Pointer? I don't follow. It seems like their internal implementations would aim to be well-typed, just like everything else. Do you have an example?
Sure. Suppose you have something like:
You might argue that the create_foo() interface is bad. Agreed, but it's still common. It's even used within the runtime itself for e.g. mmap return values within mem_linux.go:
The above function uses "invalid" behavior per #21897. If it were not marked with go:nosplit, and happened to grow while p was on the stack, then it would panic the runtime. I also encountered this issue in a specific context as well that I will follow up with you about separately.
I'm with you here, but it still happens. The go runtime itself is guilty of this. |
Isn't that exactly the use-case that the Go
The runtime can rely upon whatever implementation details it likes. (Runtime code is not user code!) That said, arguably we should fix |
I'm talking about a particular VM.
VM memory consists of the code memory, stack, text and data segments and heap memroy. Except for the code memory, all other VM memory is allocated outside of the Go runtime using mmaps. VM pointers are uintptrs. Every access to any non code memory converts a particular uintptr to an unsafe.Pointer. Essentially every VM operation/instruction has two or more mmaped memory accesses, for example AddI32. |
Sure, but this doesn't always happen. See the runtime. Note that in the reference change, I update the code there to use the uintptr correctly:
Uff. The argument that the runtime doesn't need to be "valid" Go code is disconcerting. (I say this because one of the referenced issues was closed as not a valid thing to do.) I understand the need for the pragmatic considerations and exceptions for the runtime (hence the exception in the original change), but that's a different thing.
Gotcha. Yes, I see the calls you're referring to here: This does hit the crux of the issue. The code there could be slightly restructured and there would be the risk of triggering runtime errors. (If the pointers lived on the stack during a growth event instead of being immediately consumed.) I'm not really sure about the cost in that case anyways. The branch predictor will likely make most of it disappear. If the proposal is friendly (though I get the sense that it is not), I would happily run some benchmark if you're got it to assess the impact for your case. |
That seems like a good idea regardless of whether we also add the proposed dynamic check. |
I note the following in the reference change description:
Does that actually address a significant fraction of bad pointers? If the pointer is coming from C code, odds are good that it's already encoded as an Or does the code generated by cgo itself do explicit conversions? (If so, it still seems unfortunate to couple the runtime pointer-validity check to a cgo implementation detail, especially when the interaction between cgo and the compiler is in question: see #16623.) |
This is a good question. It does seem like the structure would currently avoid this check. (The generated code effectively passes a *unsafe.Pointer which is filled in by the generated C stub.) I think the logical thing to do would be to update the CGo code generation to use uintptr across the boundary and convert to unsafe.Pointer only on the Go side. |
Change https://golang.org/cl/71270 mentions this issue: |
This seems like a not unreasonable debugging mode to me, but it's not clear that it should be on by default. It could be enabled, for example, by a GOEXPERIMENT setting. In that case, I wouldn't mind having it on even in the runtime, since the runtime shouldn't be making up bad Conversions that create bad
The real danger here is not that we observe a small-valued not-really-a-pointer. The danger is that such code may also generate a not-really-a-pointer that actually looks like a real pointer. That can crash GC hard. The main point of detecting small-valued pointers is as a first line of defense against this. Turning off that check won't do anyone any favors. |
Currently mmap returns an unsafe.Pointer that encodes OS errors as values less than 4096. In practice this is okay, but it borders on being really unsafe: for example, the value has to be checked immediately after return and if stack copying were ever to observe such a value, it would panic. It's also not remotely idiomatic. Fix this by making mmap return a separate pointer value and error, like a normal Go function. Updates #22218. Change-Id: Iefd965095ffc82cc91118872753a5d39d785c3a6 Reviewed-on: https://go-review.googlesource.com/71270 Run-TryBot: Austin Clements <austin@google.com> TryBot-Result: Gobot Gobot <gobot@golang.org> Reviewed-by: Ian Lance Taylor <iant@golang.org>
There doesn't seem to be much support for the original proposal, which seems to be unconditionally adding some checking code to conversions from As a debugging mode it makes sense, perhaps through a compiler flag. |
Following Austin's line of thought, I suggest GOEXPERIMENT=checkptr and make it a throw, not a panic. It would be great as a debugging mode. |
I apologize if I've lost track of some of the history of this, but was this motivated by an observed problem, or is this theoretical? Neither the issue nor the CL give a concrete motivation. I discussed this with several of the runtime and compiler developers today and came up with a counter-proposal: enable this behind a GOEXPERIMENT flag, but make the check much more exhaustive than simply looking for small-valued pointers. The small-valued pointers check in stack copying and GC is just a canary in a coal mine: it's meant to detect strong evidence that there are bad pointers around, but it's in no way exhaustive. Silencing the canary is too little, to the point of being misleading. Pointers < 4096 are not more bad than any other bad pointers. So if we're going to add a debugging mode for this, we should own it and detect all of the bad pointers we can detect, including pointers to dead objects in the Go heap. |
This was an issue hit in a production system. Given the nature of the issue (panic on stack check), it wasn't obvious to debug. An experimental check mode seems like a good proposal, presumably it would have caught the bad usage fixed by https://golang.org/cl/71270 at a minimum. |
Everyone seems to agree that the resolution here would be to add GOEXPERIMENT=checkptr and check for more pointers than just < 4096. Accepting that proposal, although no one is promising to work on it. |
Some suggestions:
For the last one, at compile time, given
This picks out all the The compiler then just needs to invoke a runtime function like:
This is a bit more expensive than alignment checking, but should still be reasonably fast for how infrequent pointer arithmetic is. It may also be possible to apply checks for objects on the stack or in globals. For example, we can at least guarantee that a pointer into a stack object or global object was derived from a pointer to that same stack/object region. |
Change https://golang.org/cl/162237 mentions this issue: |
I pushed a mostly functional proof-of-concept CL to 162237 that checks for pointers between 0 and minLegalPointer, pointer alignment, and pointer arithmetic. (It does not check pointed-to-object size.) It noticed a handful of "failures" within the standard library:
I'd be interested if it catches anything interesting in other code bases. To try it out:
|
This seems like a bona fide failure, not a scare-quote failure. I'm surprised it hasn't caused any GC-related crashes. |
I think golang.org/cl/162237 is polished and ready for testing if folks want to try it out an real code bases. Install cmd/compile with that CL applied, and then build/test your programs with |
Change https://golang.org/cl/201617 mentions this issue: |
Caught with -d=checkptr. Updates #22218. Change-Id: Ic0fcff4d2c8d83e4e7f5e0c6d01f03c9c7766c6d Reviewed-on: https://go-review.googlesource.com/c/go/+/201617 Run-TryBot: Matthew Dempsky <mdempsky@google.com> TryBot-Result: Gobot Gobot <gobot@golang.org> Reviewed-by: Ian Lance Taylor <iant@golang.org>
Change https://golang.org/cl/201778 mentions this issue: |
This CL extends the runtime instrumentation for (*T)(ptr) to also check that the first and last bytes of *(*T)(ptr) are part of the same heap object. Updates #22218. Updates #34959. Change-Id: I2c8063fe1b7fe6e6145e41c5654cb64dd1c9dd41 Reviewed-on: https://go-review.googlesource.com/c/go/+/201778 Run-TryBot: Matthew Dempsky <mdempsky@google.com> Reviewed-by: Keith Randall <khr@golang.org> Reviewed-by: Cuong Manh Le <cuong.manhle.vn@gmail.com> TryBot-Result: Gobot Gobot <gobot@golang.org>
Change https://golang.org/cl/201781 mentions this issue: |
This CL tweaks escape analysis to treat unsafe.Pointer(ptr) as an escaping operation when -d=checkptr is enabled. This allows better detection of unsafe pointer arithmetic and conversions, because the runtime checkptr instrumentation can currently only detect object boundaries for heap objects, not stack objects. Updates #22218. Fixes #34959. Change-Id: I856812cc23582fe4d0d401592583323e95919f28 Reviewed-on: https://go-review.googlesource.com/c/go/+/201781 Run-TryBot: Matthew Dempsky <mdempsky@google.com> Reviewed-by: Keith Randall <khr@golang.org> Reviewed-by: Cherry Zhang <cherryyz@google.com> TryBot-Result: Gobot Gobot <gobot@golang.org>
Change https://golang.org/cl/201782 mentions this issue: |
We need to explicitly convert pointers to unsafe.Pointer before passing to the runtime checkptr instrumentation in case the user declared their own type with underlying type unsafe.Pointer. Updates #22218. Fixes #34966. Change-Id: I3baa2809d77f8257167cd78f57156f819130baa8 Reviewed-on: https://go-review.googlesource.com/c/go/+/201782 Run-TryBot: Matthew Dempsky <mdempsky@google.com> TryBot-Result: Gobot Gobot <gobot@golang.org> Reviewed-by: Keith Randall <khr@golang.org>
Change https://golang.org/cl/201840 mentions this issue: |
Change https://golang.org/cl/201839 mentions this issue: |
Change https://golang.org/cl/201937 mentions this issue: |
Same as CL 201617 did for package syscall. Caught with -d=checkptr Updates golang/go#22218 Change-Id: I8208f8e6d9bd62376bf9e0458dc18956daabd785 Reviewed-on: https://go-review.googlesource.com/c/sys/+/201937 Run-TryBot: Tobias Klauser <tobias.klauser@gmail.com> TryBot-Result: Gobot Gobot <gobot@golang.org> Reviewed-by: Elias Naur <mail@eliasnaur.com>
Escaping all unsafe.Pointer conversions for -d=checkptr seems like it might be a little too aggressive to enable for -race/-msan mode, since at least some tests are written to expect unsafe.Pointer conversions to not affect escape analysis. So instead only enable that functionality behind -d=checkptr=2. Updates #22218. Updates #34959. Change-Id: I2f0a774ea5961dabec29bc5b8ebe387a1b90d27b Reviewed-on: https://go-review.googlesource.com/c/go/+/201840 Run-TryBot: Matthew Dempsky <mdempsky@google.com> Reviewed-by: Brad Fitzpatrick <bradfitz@golang.org>
Change https://golang.org/cl/202580 mentions this issue: |
A common idiom for turning an unsafe.Pointer into a slice is to write: s := (*[Big]T)(ptr)[:n:m] This technically violates Go's unsafe pointer rules (rule #1 says T2 can't be bigger than T1), but it's fairly common and not too difficult to recognize, so might as well allow it for now so we can make progress on #34972. This should be revisited if #19367 is accepted. Updates #22218. Updates #34972. Change-Id: Id824e2461904e770910b6e728b4234041d2cc8bc Reviewed-on: https://go-review.googlesource.com/c/go/+/201839 Run-TryBot: Matthew Dempsky <mdempsky@google.com> TryBot-Result: Gobot Gobot <gobot@golang.org> Reviewed-by: Brad Fitzpatrick <bradfitz@golang.org>
Follow the idiom for allowing -d=checkptr to recognize and verify correctness. Updates #22218. Updates #34972. Change-Id: Ib6001c6f0e6dc535a36bcfaa1ae48e29e0c737f8 Reviewed-on: https://go-review.googlesource.com/c/go/+/202580 Run-TryBot: Matthew Dempsky <mdempsky@google.com> Reviewed-by: Brad Fitzpatrick <bradfitz@golang.org> TryBot-Result: Gobot Gobot <gobot@golang.org>
Change https://golang.org/cl/202677 mentions this issue: |
They're still lacking in details, but at least better than being printed as raw interface values. Updates #22218. Change-Id: I4fd813253afdd6455c0c9b5a05c61659805abad1 Reviewed-on: https://go-review.googlesource.com/c/go/+/202677 Run-TryBot: Matthew Dempsky <mdempsky@google.com> TryBot-Result: Gobot Gobot <gobot@golang.org> Reviewed-by: Brad Fitzpatrick <bradfitz@golang.org>
Change https://golang.org/cl/214238 mentions this issue: |
We had a few test cases to make sure checkptr didn't have certain false positives, but none to test for any true positives. This CL fixes that. Updates #22218. Change-Id: I24c02e469a4af43b1748829a9df325ce510f7cc4 Reviewed-on: https://go-review.googlesource.com/c/go/+/214238 Run-TryBot: Matthew Dempsky <mdempsky@google.com> TryBot-Result: Gobot Gobot <gobot@golang.org> Reviewed-by: Brad Fitzpatrick <bradfitz@golang.org> Reviewed-by: Austin Clements <austin@google.com>
This is a proposal for this change, and see if there's any interest in having it move forward.
Background
During stack growth, pointers that have values between 0 and minLegalPointer will cause a "invalid pointer found on stack" exception to be thrown by the runtime. This suggests that storing these pointers on the stack is not a valid operation, i.e. the following code may cause a runtime exception if the stack happens to grow.
Since this is not a permitted state and will cause a runtime exception, it is preferable for the user to receive a recoverable error at the time the state is introduced. Even if this results in the program crashing, the full stack trace will be available making the cause of the problem much easier to diagnose.
Proposal
I propose to add code that causes a recoverable panic at the time the conversion is made. This conversion adds a small cost to each unsafe.Pointer cast. I believe that these conversions in user code should be relatively rare (usually associated with some other costly event, such as allocating a new object), but there may be cases where it's common (e.g. some CGo usage?). The original change did not apply these checks to code in the runtime package itself, due to its presumed safety and the fact that the conversions were common for the implementation of core types (e.g. map, channels, etc.).
Alternate Proposal
As noted in the original change, this behavior may be surprising. Users may be encoding some CGo void* equivalents as pointers in Go, which could contain "illegal" values. The constraint imposed by the stack growth code is non-obvious and undocumented.
Currently, debug.invalidptr is used in both heapBitsForObject and stack adjustment and defaults to on. I propose that a new debug variable is introduced, debug.stackptr, that toggles the behavior of the check during stack growth. I would advocate that this check should default to off, but regardless it would allow just that check to be disabled if it proves problematic.
The text was updated successfully, but these errors were encountered: