-
Notifications
You must be signed in to change notification settings - Fork 17.8k
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
cmd/compile, runtime: add and use go:nosplitrec compilation directive #21314
Comments
Should |
A function like Of course, we could change |
The stack no longer splits, but there are places where we use this to mean "no stack growth", not "no preemption". Both currently have the same consequence, but maybe now would be a good time to tease them apart?
I like this idea in general, but it may be a bit of a pain. Maybe it would make sense to combine making it recursive by default with renaming it? |
I've been looking through various uses of nosplit and there seem to be four distinct reasons for it. While we're pondering introducing a new annotation, I believe it's worth considering splitting out these different reasons, even if they have the same or similar consequences at the moment:
|
|
Change https://golang.org/cl/79615 mentions this issue: |
Add an explanation of why sigtrampgo is nosplit. Updates #21314. Change-Id: I3f5909d2b2c180f9fa74d53df13e501826fd4316 Reviewed-on: https://go-review.googlesource.com/79615 Reviewed-by: Ian Lance Taylor <[email protected]>
Change https://golang.org/cl/79815 mentions this issue: |
exitsyscall should be recursively nosplit, but we don't have a way to annotate that right now (see #21314). There's exactly one remaining place where this is violated right now: exitsyscall -> casgstatus -> print. The other prints in casgstatus are wrapped in systemstack calls. This fixes the remaining print. Updates #21431 (in theory could fix it, but that would just indicate that we have a different G status-related crash and we've *never* seen that failure on the dashboard.) Change-Id: I9a5e8d942adce4a5c78cfc6b306ea5bda90dbd33 Reviewed-on: https://go-review.googlesource.com/79815 Run-TryBot: Austin Clements <[email protected]> Reviewed-by: Rick Hudson <[email protected]> TryBot-Result: Gobot Gobot <[email protected]>
I assume the change won't happen in Go 1.10. |
Yeah, not happening for 1.10. I might try to prototype this before the tree opens, though. Some thoughts from discussing these annotations with others:
There are also two more, more obscure uses of |
gccgo never required any special handling for |
I think @aclements's question is that we may want |
@cherrymui Ah, got it, thanks. Yes, for that to work right, in gccgo, any function that calls |
Thanks. Could gccgo instead automatically mark any function that calls |
gccgo could certainly mark any function that calls Having |
Oh dear. The stack trampoline doesn't save its own return PC anywhere convenient? |
gccgo lives in a more complex world than gc. The stack splitting code is not part of the Go frontend, it's part of GCC itself and the GCC support libraries. The Go library has no access to the data structures used by the GCC support libraries. It is also true that the stack trampoline doesn't save the return PC anywhere in particular. It just saves the stack pointer. Quoting gcc/libgcc/config/i386/morestack.S:
|
Change https://golang.org/cl/83015 mentions this issue: |
heapBits.bits is used during bulkBarrierPreWrite via heapBits.isPointer, which means it must not be preempted. If it is preempted, several bad things can happen: 1. This could allow a GC phase change, and the resulting shear between the barriers and the memory writes could result in a lost pointer. 2. Since bulkBarrierPreWrite uses the P's local write barrier buffer, if it also migrates to a different P, it could try to append to the write barrier buffer concurrently with another write barrier. This can result in the buffer's next pointer skipping over its end pointer, which results in a buffer overflow that can corrupt arbitrary other fields in the Ps (or anything in the heap, really, but it'll probably crash from the corrupted P quickly). Fix this by marking heapBits.bits go:nosplit. This would be the perfect use for a recursive no-preempt annotation (#21314). This doesn't actually affect any binaries because this function was always inlined anyway. (I discovered it when I was modifying heapBits and make h.bits() no longer inline, which led to rampant crashes from problem 2 above.) Updates #22987 and #22988 (but doesn't fix because it doesn't actually change the generated code). Change-Id: I60ebb928b1233b0613361ac3d0558d7b1cb65610 Reviewed-on: https://go-review.googlesource.com/83015 Run-TryBot: Austin Clements <[email protected]> Reviewed-by: Matthew Dempsky <[email protected]> Reviewed-by: Rick Hudson <[email protected]> TryBot-Result: Gobot Gobot <[email protected]>
Another instance where this would have helped: https://go-review.googlesource.com/c/go/+/85880/1 |
@ianlancetaylor, I'm sure it's more complicated than this, but reading over gcc's
I think the stack around the saved rbp looks like
|
@aclements The whole world is not an x86. Also, libgcc is not part of the gofrontend. I'd rather avoid having gofrontend depend on implementation details of libgcc. I don't think it matters, though. I looked through gccgo's version of the runtime package, and, remembering that gccgo does not support the race detector, there was only one meaningful call to |
Sure. I hadn't thought that it would. I assumed (perhaps incorrectly) that
Ah, great. That sounds like an easy solution. :) |
I've been experimenting with my proposed The problem with automatically propagating it recursively is it can easily worm its way into lots of surprising parts of the runtime. Most of its surprise reach is easy to fix, but fixing it requires noticing it, and if the propagation is automatic, it's hard to notice. But I also want to fix the problem of So my alternate proposal is: Add both a |
Are you suggesting any check for an unannotated function called by a go:noscan function? Or will that just be a potential bug? |
I am. I'm suggesting that the linker should check that all of the functions called by a go:noscan function must be go:noscancalled (perhaps or go:nosplit to make the transition easier). I'm also suggesting that the linker check that no additional functions are marked go:noscancalled. This would mean there are no surprises in what gets its stack check prologue removed. One potential downside is that different build configurations may have somewhat different sets of go:noscancalled functions. I'm not sure how to deal with that. Another possibility would be propagate the go:noscan annotation automatically (no go:noscancalled annotation), but have diagnostic output to report exactly which functions were automatically marked. We could check that in so there's an obvious diff in that file when something changes. |
Alternatively, maybe doing this all purely statically is just the wrong approach. In most uses, it's fine if these helper functions grow the stack, so it's a little unfortunate that just because one of their callers is noscan, they lose the ability to grow that stack for all uses. A possible hybrid static/dynamic approach would be for a noscan function to set a flag on entry (and clear it on exit) that inhibits stack growth even if the stack check fails and for the linker to check that the call tree does in fact fit in the nosplit space, so ignoring the stack check is okay. This would have to be integrated with the linker's nosplit check because if a nosplit function calls a noscan function, the caller (and any further nosplit functions above it) have to count against the space. Things also get dicey with indirect calls, though that's already dicey (and as it turns out we don't make any problematic indirect calls in noscan call trees). This approach wouldn't involve any recursive effect on generated code and would let us remove most of the nosplits from the runtime, while still letting us statically ensure the safety of this code. |
Another way to think of this is that the functions must have a static upper bound on stack size, even if the caller does not guarantee that the available stack space exceeds that bound. (Perhaps we could name that Are there cases where we care about a static upper bound but that bound is parametric (say, with the stack bound of a passed-in function)? |
I think this is a valid generalization, but would be difficult to actually implement. Currently, splittable functions ensure there's enough available space for any chain of nosplit functions, and we can use that same global bound to ensure that noscan call trees always fit on the stack. You could imagine that the prologue of a noscan function could grow the stack to whatever bound was required by its call tree (just use this size instead of its own frame size for the growth prologue). But there are some complications: 1. the prologue is written by the compiler, but the stack bound analysis needs to be done by the linker since it may cross assembly functions or package boundaries, and 2. for at least syscalls and and the write barrier, by the time we reach the noscan function we already can't grow the stack (for syscalls, because of the arguments to the syscall, and for the write barrier because its arguments are passed in registers).
Not for the functions I'm planning to mark noscan. They don't make any indirect function calls (at least, not without first switching to the system stack). |
Two cases where I believe a go:nosplitrec or go:noscan would have helped: CL 21314 would have missed a go:nosplit for the runtime.crash function if Austin had not caught my mistake in review. The breaking of the nosplit chain CL 111258 is fixing. A go:noscan would presumably have caught the subtle issue that the closure in systemstack(func() { ... }) is not marked nosplit. |
Another example: CL 131277 fixed a missing nosplit. This is a case of "bad state for stack growth/scheduling" since the runtime may not be initialized when entering this function via |
The naming and related breakdown of functionality needs to take into account 3rd party user code, as go:nosplit is available to 3rd parties independent of relation to the runtime. The main reason I can think of for 3rd party code is to make it easier to express scheduling control I would propose to set aside a directive for the go user, perhaps go:noruntime or, if needed User directives would then need to be treated as such w.r.t. changes over time: they would be independent of runtime implementation and provide a means for a plain old Go programmer to turn on and off the runtime (and take associated responsibilities such as not calling package runtime functions and working with preallocated memory only, small local variables, ..., but I guess sys calls should be allowed in some form ). This would enable clean real-time audio code which is user Go code, and may also enable a Go user to interface Go with various other operating system services which give guidelines that risk bad interactions with the runtime. By classifying a separate directive to the Go user, it would also allow runtime and compiler development to proceed relatively independently of external consumers of go:nosplit and vice versa. |
Change https://golang.org/cl/276173 mentions this issue: |
This fixes the unexpected growth of stack in child process, which is caused by stack checking code in runtime.sigfillset called from runtime.sigset while clearing the signal handlers in child process. The redundant stack checking code is generated due to missing '//go:nosplit' directive that should be annotated for runtime.sigfillset. Fixes #43066 Updates #21314 Change-Id: I9483a962a4b0747074313991841e2440ee32198c Reviewed-on: https://go-review.googlesource.com/c/go/+/276173 Run-TryBot: Austin Clements <[email protected]> TryBot-Result: Go Bot <[email protected]> Reviewed-by: Michael Pratt <[email protected]> Reviewed-by: Austin Clements <[email protected]>
Another example where nosplitrec would have prevented bugs: CL 431919 |
Another place where it would have helped described in #56044. |
Change https://go.dev/cl/441859 mentions this issue: |
Mark the "l1" and "l2" methods on "arenaIdx" with //go:nosplit, since these methods are called from a nosplit context (for example, from "spanOf"). Fixes #56044. Updates #21314. Change-Id: I48c7aa756b59a13162c89ef21066f83371ae50f1 Reviewed-on: https://go-review.googlesource.com/c/go/+/441859 Reviewed-by: Cherry Mui <[email protected]> Run-TryBot: Than McIntosh <[email protected]> TryBot-Result: Gopher Robot <[email protected]>
Mark the "l1" and "l2" methods on "arenaIdx" with //go:nosplit, since these methods are called from a nosplit context (for example, from "spanOf"). Fixes golang#56044. Updates golang#21314. Change-Id: I48c7aa756b59a13162c89ef21066f83371ae50f1 Reviewed-on: https://go-review.googlesource.com/c/go/+/441859 Reviewed-by: Cherry Mui <[email protected]> Run-TryBot: Than McIntosh <[email protected]> TryBot-Result: Gopher Robot <[email protected]>
nosplit is not recursive. A recursive version might be available in the future: golang/go#21314 nowritebarrierrec is recursive but is package scoped, so add it in the subpackages.
A key error leading to #21306 was a call from a nosplit function to a function not marked nosplit, permitting preemption at a point where it was not permitted.
Therefore, just as we have
go:nowritebarrierrec
meaning that such a function may not call any function that permits write barriers, we should havego:nosplitrec
meaning that such a function may not call any function that permits preemption.Since
nosplit
is now a misnomer--the stack no longer splits--we could also consider a name likego:nopreemptrec
.The text was updated successfully, but these errors were encountered: