-
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
cmd/vet: add check for sync.WaitGroup abuse #18022
Comments
I don't like the idea. It's IMHO perhaps a task for |
I think this has been proposed before (probably on the mailing lists)
and rejected.
|
A func main() {
var wg sync.WaitGroup
defer wg.Wait()
go func() {
wg.Add(1)
defer wg.Done()
}()
} and In terms of vet's requirements:
@dominikh , |
As far as the proposal goes, I don't like how it limits the func signature to |
@dsnet The check in staticcheck has no (known) false positives. It shouldn't have a significant number of false negatives, either. The implementation is a simple pattern-based check, detecting I'm -1 on the proposed |
@dominikh I don't see any extra level of nesting here, though (assuming any func signature is allowed). To be nitpicky, another thing that stands out from the proposal is how |
@mvdan The extra level of nesting would come from a predicted usage that looks something like this:
as opposed to
Admittedly the same level of indentation, but syntactically it's one extra level of nesting. |
Ah yes, I was thinking indentation there. |
The problematic case is that
|
@cznic if you mean without the extra |
@mvdan Do you mean by allowing something like the following?
IMHO that's way too much |
panic is recovered? |
@dominikh true; I was simply pointing at the issue without contemplating a solution :) |
The API change here has the problems identified above with argument evaluation. Also, in general the libraries do not typically phrase functionality in terms of callbacks. If we're going to start using callbacks broadly, that should be a separate decision (and not one to make today). For both these reasons, it seems like .Go is not a clear win. It would be nice to have a vet check that we trust (no false positives). Perhaps it is enough to look for two statements
back to back and reject that always. Thoughts about how to make vet catch this reliably? |
I agree that |
It sounds like we are deciding to make go vet check this and not add new API here. Any arguments against that? |
SGTM |
I've added this proposal to the proposal process bin, but it's blocked on someone figuring out how to implement a useful check. Is anyone interested in doing that? |
Staticcheck has a fairly trivial check: for a GoStmt of a FuncLit, if the first statement in the FuncLit is a call to The check could be trivially hardened by
Edit: which is pretty much what you have suggested in #18022 (comment) |
since there is an active discussion here, I would like to bring to your attention that SA2000 in staticcheck misses real bugs. If you run staticcheck against the following, it fails to find the bug. If you comment the fmt.Print in front of wg.Add(), it finds the bug. In general, as long as there is some code between
$ go install honnef.co/go/tools/cmd/staticcheck@latest SA2000 code is self-explanatory on why this happens. I wrote a linter to address this. Will move to staticcheck github repo to first discuss whether such improvement is welcome or negligible. |
Yes, SA2000 and the port of it in https://go.dev/cl/633704 are intentionally very limited in the patterns they match, to avoid false positives. I suspect that relaxing the "wg.Add must be the first statement" rule would cause the analyzer to report a spurious diagnostic for this program: func f() error {
var wg sync.WaitGroup
wg.Add(1)
go func() {
defer wg.Done()
...
// Create another goroutine.
wg.Add(1) // waitgroup: "WaitGroup.Add called from inside new goroutine"
go func() {
defer wg.Done()
...
}()
}()
return nil
} I am sure there is room for improvement, but in general our inclination is to optimize for fewer false positives, even at the expense of true positives. If you have concrete suggestions (or code) for better algorithms, we can easily test them out on the corpus of the Module Mirror. |
@adonovan yes, you spotted a valid false positive. Here is my linter that uses the golang analysis, not based on staticcheck. It also has fixing logic btw. I scanned our code at Uber, it led to two false positives, which look exactly like what you described. All other reports are true positives. It found one more true positive that SA2000 missed, which is arguably marginal :) |
@adonovan yep, I can take a look. |
From skimming a couple as well (maybe I'll get interested enough to be exhaustive):
These are false positives, the same exception @adonovan pointed out. They could also be written like this though, and I believe it'd incorrectly trigger SA2000 as well: func f() {
var wg sync.WaitGroup
wg.Add(1)
go func() {
wg.Add(1)
defer wg.Done() // done setting up work
...
go func() {
defer wg.Done() // done doing work
...
}()
}()
} but I suspect that's less common code in practice. " I've been trying to think of a check that'd be better at allowing these, because I like "same block" a fair bit, but the closest I can come up with is something like "Add(1) plus includes a deeper nested Done() == ignore". Basically "valid inside invalid is fine". It would definitely miss some, but it might be a good enough sign of "perhaps this is complex enough code that it can be ignored". This one is a bit interesting:
It's essentially inverted, and it's... arguably fine: var wg sync.WaitGroup
wg.Add(1)
// Do some concurrent setting to make sure that it works
for i := 0; i < concurrency; i++ {
go func() {
// Wait for waitGroup so that all goroutines run at basically the same
// time.
wg.Wait()
v.Set("hi")
atomic.AddInt32(&sets, 1)
}()
}
wg.Done() I've used similar constructs to try to maximize racing, but I'm fairly sure this one is ineffective since in practice the var ready, started, complete sync.WaitGroup
ready.Add(1)
started.Add(concurrency)
complete.Add(concurrency)
for i := 0; i < concurrency; i++ {
go func() {
started.Done() // inform that this goroutine has been started
ready.Wait() // wait for all to be started
atomic.AddInt32(&sets, 1)
complete.Done() // this goroutine has finished its work
}()
}
started.Wait() // wait for all goroutines to have been started, and likely at or near Wait
ready.Done() // unblock all ~simultaneously
complete.Wait() // wait for them all to finish which has so far been the most effective way to trigger real races that I've found, by quite a large margin, across various I suspect this would also trigger the linter, on the |
The proposal committee is on board with adding this check to vet. We'll let the domain experts hash out exactly what the right heuristic is. :) |
@adonovan done with the manual analysis, see https://gist.github.com/lpxz/10b2cd9c3d390064f0a51267daa80575. My linter has these: See the improved version here please. could you rerun it, and then I do another manual inspection? |
Impressive! Some of these new finds are really great. But I'm not sure that all of the new positives are true: Consider this one:
I can't see the full context (and nor can the tool), but it looks like it is temporarily bumping up a reference count, which seems like a reasonable thing to do. I wonder how many are of this form, and if there's a rule to exclude them. https://go-mod-viewer.appspot.com/github.com/getlantern/[email protected]/eventual_test.go#L102 is a delightful muddle! ;-) |
ok, the improved linter will still false positive on this, until we use inspect() to recursively look into all sibling nodes (may not be worthy the machine cost)
I think it is true positive, given this pattern, the goroutine may not start yet, when the shutdownWaitGroup.Wait() is reached, leaving that goroutine leaky. Did I miss some subtlety here? A relevant question, what is the next step then? :) |
To be clear, I meant that the code here is wrong in more ways than I can count; your linter algorithm is absolutely right to flag it.
A WaitGroup is a counting semaphore. The most common use is to count unfinished goroutines, but in this case I think it is counting various "holds" that prevent the program from completing its shutdown (like electricians each padlocking the main breaker so that they know they are safe while they are working). That is, the goroutines are already running, and any of them may temporarily increment the counter, far from where they are created. Presumably at the end the main goroutine will wait for the counter to fall to zero. The clue to recognizing this pattern is that there is no nearby
We should probably reclassify the new positives in light of these observations. Ideally the false positive rate should be 5% or less. |
Since it's a semaphore that is incorrect to use to (obviously this is worth verifying, but it seems extremely unlikely to be guaranteed-safe to me) |
I agree with @Groxx on this, If the goroutines want to declare they are live, why do not they do so at the beginning or even better before goroutine starts. I am not sure why they do so in the middle. By flagging this, we suggest developers to move add to an earlier point. This can only improve quality, at least will not degrade quality. I may miss some points, please let me know. |
That's definitely a bug. (As other people already deduced). The easiest way to see this is to imagine a |
Thanks, you have all convinced me that this is indeed a bug (and thus a true positive of the new algorithm). @lpxz, would you like to send me a CL to incorporate the new algorithm into the waitgroup analyzer (currently in gopls, but eventually to be promoted to vet too)? The CL should included a test for each distinct case you encountered (and false positives too, since they document the limitations). Thanks for improving this analyzer! |
@adonovan great, happy to make the contributions! Will create a CL and add tests. Peng |
Based on the discussion above, this proposal seems like a likely accept. The proposal is to add a vet check for sync.WaitGroup misuse where var wg sync.WaitGroup
defer wg.Wait()
go func() {
wg.Add(1)
defer wg.Done()
}() While complex heuristics are certainly possible here, for now we're going to limit this to checking for |
I am in progress of creating the cl, which has no false positive, fewer false negatives and is also simple. Feel free to let me know if the final decision is to move on with the first line approach which is a fine approach). |
This proposal is about adding the basic functionality into vet, which can likely proceed for go1.25. Independent of that, you should feel free to improve the algorithm; any improvements can be used immediately by gopls, and eventually by vet (without needing a separate proposal process), assuming they meet the usual criteria for precision and frequency. |
No change in consensus, so accepted. 🎉 The proposal is to add a vet check for sync.WaitGroup misuse where var wg sync.WaitGroup
defer wg.Wait()
go func() {
wg.Add(1)
defer wg.Done()
}() While complex heuristics are certainly possible here, for now we're going to limit this to checking for |
The API for WaitGroup is very easy for people to misuse. The documentation for
Add
says:However, it is very common to see this incorrect pattern:
This usage is fundamentally racy and probably does not do what the user wanted. Worse yet, is that it is not detectable by the race detector.
Since the above pattern is common, I propose that we add a method
Go
that essentially does theAdd(1)
and subsequent call toDone
in the correct way. That is:The text was updated successfully, but these errors were encountered: