-
Notifications
You must be signed in to change notification settings - Fork 17.7k
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: unsafe: clarify unsafe.Pointer rules for package syscall #34684
Comments
The part you quoted is the rationale. The real rule is in the part you didn't quote:
My answers, such as they are, to your questions.
Per above, the rule applies to any function implemented in assembly.
I think the functions have to be called directly, and we should update the unsafe package docs to say that.
I suppose that first we have to come up with a rule for When I introduced |
Hm, I've always interpreted the first line as the rule, and everything below as the rationale, restrictions, and details. For example, that rule 4 is you can convert unsafe.Pointer-to-uintptr when calling syscall.Syscall*, and the part you describe as the real rule is just an explanation of the implementation details about how the compilers implement this today (i.e., that syscall.Syscall* are implemented as assembly functions, and the compilers generically handle calls to assembly functions this way). If rule 4 is in fact that all assembly functions should be handled that way (with syscall.Syscall as just a special case thereof), I think we should reword the first sentence to better emphasize that. |
Yes, my interpretation is that this applies to all functions written in assembly, since the rule clearly applies to more than the single function |
I think making |
@beoran The issue here is we've never defined exactly what it means for I've generally been under the assumption that (1) rule 4 is meant to apply specifically to package syscall's functions like Syscall, RawSyscall, Proc.Call, and LazyProc.Call, and (2) For example, I'm under the impression that while today users can write their own assembly functions that pass pointers as However, it seems at least @ianlancetaylor had a different interpretation. |
My thoughts, not definitive: Q1: It seems like the answer must be yes or else those functions are impossible to use safely at all. That's the same reason we answer yes to syscall.Syscall. Q2: It would be nice if indirect calls worked, but it seems like they probably can't be made to work without significant overhead, and it also seems like they don't work today. If both those are true, then probably we should say that indirect calls don't apply. Q3: In general there's almost zero difference between x.M() and T.M(x), so I don't see why we'd introduce a difference here. So I guess I'm suggesting yes/no/yes. |
Thanks for sharing your thoughts, @rsc.
It should only introduce overhead for indirect calls involving unsafe.Pointer->uintptr conversions as arguments, and I don't think those are very common. So I don't expect the overhead would be significant, but I'll measure this.
That's my tentative position as well. The two counter arguments here though are (1) historically it hasn't worked; and (2) package unsafe imposes its own set of rules, and maybe we think T.M(x) is uncommon enough to not allow it. |
Across the standard library and Kubernetes, the only indirect call that involves a unsafe.Pointer->uintptr conversion is here: go/src/syscall/syscall_unix.go Lines 95 to 98 in 32b6eb8
(And the same code appears in x/sys/unix.) For this particular call, the overhead would amount to an extra stack slot and an instruction to populate it. |
Change https://golang.org/cl/200137 mentions this issue: |
I think //go:uintptrescapes would be nice to have to make indirect calls as in Q2 possible. It makes it easier to keep Go code that interests with the operating system well factored and easier to read. |
How do you plan to fix the indirect calls? |
Conservatively assume any indirect function calls might be to an assembly function or a function annotated with //go:uintptrescapes, and handle unsafe.Pointer->uintptr conversions passed to their uintptr-typed arguments appropriately. See CL 200137 for details. |
Sorry, I don't understand what you're suggesting here. In general, users shouldn't have to worry about //go:uintptrescapes. It's an implementation detail about how we make sure package syscall works. Go doesn't support users writing their own functions that accept or return pointers using uintptr-typed parameters. |
I am currently writing a game library in Go that avoids cgo and calls the
OS directly, certainly on Linux thanks to it's stable kernel API. (
https://gitlab.com/beoran/galago
<https://gitlab.com/beoran/galago/blob/master/os/linux/input/input_linux.go>
)
So, I have functions like these:
// Icotl performs an ioctl on the given device
func (d * Device) Ioctl(code uint32, pointer unsafe.Pointer) error {
fmt.Printf("ioctl: %d %d %d\n", uintptr(d.Fd()), uintptr(code),
uintptr(pointer))
_, _, errno := syscall.Syscall(
syscall.SYS_IOCTL,
uintptr(d.Fd()),
uintptr(code),
uintptr(pointer))
if (errno != 0) {
return errno
}
d.KeepAlive()
return nil
}
Notice the KeepAlive? It would be great if it wasn't needed and I could
instruct the compiler that everything escapes, maybe with the mentioned
pragma.
Please don't underestimate us "normal" Go users, we too need to perform low
level system calls for certain uses. To boldly Go where no one has used Go
before. :-)
|
I agree it would be nice if users didn't have to manually insert runtime.KeepAlive calls when using os.File.Fd. Or if there was at least vet/lint tooling to suggest when they probably need it. However, file descriptors are not pointers, so the unsafe.Pointer safety rules do not apply to os.File.Fd. You're suggesting entirely new functionality, whereas this issue is about clarifying corner cases of existing functionality. I recommend filing a new feature request / proposal issue if you'd like to pursue that idea. |
@mdempsky, it sounds like you are suggesting that the Do I have that right? (You didn't specifically say "any call", but it seems like if you are going to do a fixed set of specific calls as well as all indirect calls, you might as well just complete the set and do all calls. Otherwise direct calls are somehow disadvantaged compared to indirect calls.) |
@rsc I think that's close, yes. I would say my suggestion was specifically that the conversion is safe when "calling (directly or indirectly) to a fixed set of specific functions". The particular implementation detail today would be that we handle all indirect calls as though they might be to one of those functions (which has very few false positives), but compiler optimizations might later let us rule some of those out. E.g., calls to non-exported interface methods (outside of package syscall) can never be one of those specific functions; moving escape analysis to SSA might allow us to see that the set of possible called functions at a call-site is disjoint from the set of specific functions; or a Go JIT that does dynamic monomorphic call optimization might know the target function isn't one of those specific functions. I think further simplifying to "when calling a function" is reasonable and has merits, but that's not specifically my suggestion. My only concern would be this might introduce more unnecessary KeepAlive calls in current code; I'll measure this. |
I am definitely in favour of simplifying this to "when calling a function", as this makes low level programming that much easier. As for unnecessary KeepAlive calls, in this case, I thought KeepAlive is always needed, only now it sometimes accidentally seems to work without the call. |
Note that even if we generalize to "when calling a function", to allow users to write their own functions that accept pointers as uintptr-typed parameters we'll still need to specify how those uintptr parameters can actually be used by the function. Currently we side step that because only the standard library contains this code, and we can ensure it's kept in sync with the compiler's own conventions (e.g., using undocumented compiler directives). |
Change https://golang.org/cl/198043 mentions this issue: |
The logic for keeping arguments alive for calls to //go:uintptrescapes functions was only applying to direct function calls. This CL changes it to also apply to direct method calls, which should address most uses of Proc.Call and LazyProc.Call. It's still an open question (#34684) whether other call forms (e.g., method expressions, or indirect calls via function values, method values, or interfaces). Fixes #34474. Change-Id: I874f97145972b0e237a4c9e8926156298f4d6ce0 Reviewed-on: https://go-review.googlesource.com/c/go/+/198043 Run-TryBot: Matthew Dempsky <[email protected]> TryBot-Result: Gobot Gobot <[email protected]> Reviewed-by: Keith Randall <[email protected]>
Change https://golang.org/cl/205244 mentions this issue: |
It sounds like the "any function" rules may be fine but we are waiting on @mdempsky to confirm that there's not unexpected overhead. I'm going to tag this Proposal since it is a real (if small) change to the effective language. |
Putting this proposal on hold until @mdempsky has had a chance to check the overheads. |
This stung me too. I had code wrapping I think if the documentation had been more explicit that other functions/methods also had special treatment, I might have spotted the issue sooner. Although this was a genuine bug in my code, I was fortunate that the former escape analysis coincidentally worked in my favour and kept the values live. It appeared as an intermittent issue when upgrading to go 1.13. From bisecting, I discovered the failure started occurring in 996a687 when the escape analysis was cleaned up. What would be the correct / supported approach for logging values in/out of With the existing wrapper, I'm aware I could add the It feels like logging inputs/outputs of syscalls should be something doable in the langauge without requiring the use of an undocumented feature, so I wanted to check if I'm missing something obvious, or what the recommended approach should be. Many thanks! |
Offhand I don't think there is a safe way to interpose Using |
Many thanks for the prompt reply. Would there be value in making |
One guideline for adding yet another feature is whether it is possible to write simple succinct documentation for that feature that any programmer can understand. I don't think |
I respectfully disagree. I believe not all Go programmers need to know all Go language features. The common high level features of the language need to be widely understood, yes, and need therefore need to be easy to understand and easy to explain. However most "advanced", or low level features, like unsafe.Pointer, syscalls, //go:uintpointerescapes, etc, are not for general use by Go programmers, but for use by those of us who want to integrate Go with OS kernels, existing C libraries, work on compilers or kernels in Go language, etc. But, the problem is that C libraries, kernels, etc, are not easily explained in any succinct way, and it takes time and effort to learn about these advanced topics. The best the Go team can do here is to provide a few pointers to the information available on the internet or in books, and explain the advanced features of Go in detail so they can be learned smoothly by those of us with the special interests mentioned. |
…ntptrescapes The unofficial documentation for this pragma can be found in the source code: * https://github.com/golang/go/blob/go1.17.6/src/cmd/compile/internal/noder/lex.go#L71-L81 Instead of relying on this pragma, generic-worker now calls syscall.Syscall<x> directly. This change has been made since I suspect that nested go:uintptrescapes functions may not work as expected, but furthmore, since this pragma is not an official feature, it presumably may change over time, so we probably should not assume it will always be supported. There have also been several issues raised against functions and methods that rely on its behaviour, so it is not clear whether it is entirely safe to use: * golang/go#16035 * golang/go#23045 * golang/go#34474 * golang/go#34642 * golang/go#34684 * golang/go#34810 * golang/go#42680 Note, in future an option might be to generate the function bodies using mkwinsyscall: * https://github.com/golang/go/blob/go1.17.6/src/internal/syscall/windows/mksyscall.go#L9 * https://github.com/golang/go/blob/go1.17.6/src/syscall/mksyscall_windows.go#L47 * https://pkg.go.dev/golang.org/x/sys/windows/mkwinsyscall * https://github.com/golang/sys/blob/master/windows/mkwinsyscall/mkwinsyscall.go One advantage of this approach is that we can probably log the syscalls again using: * https://github.com/golang/sys/blob/50617c2ba19781ae46f34bb505064996b8fa32e8/windows/mkwinsyscall/mkwinsyscall.go#L43-L44 Alternatively if tracing doesn't do what we expect, we could write our own generator.
unsafe.Pointer's rule 4 says:
It talks about "the Syscall functions", but on Windows, there's also Proc.Call and LazyProc.Call.
Q1: Are these two functions "Syscall functions"? Strictly speaking, I would interpret "Syscall functions" to mean Syscall{,6,9,12,15,18}. Perhaps the docs should be clarified.
Q2: Do functions have to be called directly, or are indirect calls allowed? The rule explicitly warns that conversions have to be performed directly in the argument list, but it doesn't say anything about indirect calls.
Q3: For Proc.Call and LazyProc.Call, are direct calls via method expressions allowed? E.g., if
x.Call(uintptr(p), 0, 0)
is valid, then is(*Proc).Call(x, uintptr(p), 0, 0)
also valid?Clarifying this is relevant to determining how far we need to go in fixing #34474.
Incidentally, Q2 and Q3 also apply to rule 5. The status quo there is that cmd/compile safely handles
unsafe.Pointer(f(...))
for allf(...)
, so we do allow indirect and method expression calls to reflect.Value.Pointer and reflect.Value.UnsafeAddr./cc @rsc @ianlancetaylor
The text was updated successfully, but these errors were encountered: