Skip to content
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

Closed
dsnet opened this issue Nov 22, 2016 · 56 comments
Closed

cmd/vet: add check for sync.WaitGroup abuse #18022

dsnet opened this issue Nov 22, 2016 · 56 comments
Assignees
Labels
Analysis Issues related to static analysis (vet, x/tools/go/analysis) FixPending Issues that have a fix which has not yet been reviewed or submitted. Proposal-Accepted
Milestone

Comments

@dsnet
Copy link
Member

dsnet commented Nov 22, 2016

The API for WaitGroup is very easy for people to misuse. The documentation for Add says:

calls to Add should execute before the statement creating the goroutine or other event to be waited for.

However, it is very common to see this incorrect pattern:

var wg sync.WaitGroup
defer wg.Wait()

go func() {
	wg.Add(1)
	defer wg.Done()
}()

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 the Add(1) and subsequent call to Done in the correct way. That is:

func (wg *WaitGroup) Go(f func()) {
	wg.Add(1)
	go func() {
		defer wg.Done()
		f()
	}()
}
@dsnet dsnet added the Proposal label Nov 22, 2016
@dsnet dsnet added this to the Proposal milestone Nov 22, 2016
@cznic
Copy link
Contributor

cznic commented Nov 23, 2016

I don't like the idea. It's IMHO perhaps a task for go vet, if not implemented already. Also, the proposed method would add a(nother) closure layer when the go-ed function has parameters.

@minux
Copy link
Member

minux commented Nov 23, 2016 via email

@dsnet
Copy link
Member Author

dsnet commented Nov 23, 2016

A go vet check seems pretty reasonable. I just tried it right now on the following:

func main() {
	var wg sync.WaitGroup
	defer wg.Wait()
	go func() {
		wg.Add(1)
		defer wg.Done()
	}()
}

and vet doesn't report anything.

In terms of vet's requirements:

  • Correctness: The problem is clearly a race.
  • Frequency: My gut feeling is that this is fairly common. A number of internal bugs that I've fixed is of this nature. So it subjective feels common enough. I don't have hard numbers.
  • Precision: I don't have an algorithm in mind that can accurately identify the pattern and I can imagine some number of false positives.

@dominikh , staticcheck does report a problem:
main.go:10:3: should call wg.Add(1) before starting the goroutine to avoid a race (SA2000)
I'm wondering how accurate the check is and whether it is worth adding to vet.

@mvdan
Copy link
Member

mvdan commented Nov 23, 2016

As far as the proposal goes, I don't like how it limits the func signature to func().

@dominikh
Copy link
Member

@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 go with function literals where the first statement is a call to wg.Add – this avoids flagging wg.Add calls further down the goroutine, which tend to be valid uses.

I'm -1 on the proposed Go function. I'd prefer not having to read code with an unnecessary level of nesting that looks callback-esque and reminds me of JavaScript.

@mvdan
Copy link
Member

mvdan commented Nov 23, 2016

@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 wg.Go() will create a goroutine even though go is never directly used by the user. I don't know if the standard library does this anywhere else, but I would prefer if it was left explicit.

@dominikh
Copy link
Member

@mvdan The extra level of nesting would come from a predicted usage that looks something like this:

wg.Go(func() {
  // do stuff
})

as opposed to

go func() {
  // do stuff
}()

Admittedly the same level of indentation, but syntactically it's one extra level of nesting.

@mvdan
Copy link
Member

mvdan commented Nov 23, 2016

Ah yes, I was thinking indentation there.

@cznic
Copy link
Contributor

cznic commented Nov 23, 2016

The problematic case is that

wg.Add(1)
go func(i int) { ... }(42)

// becomes

wg.Go(func() {
        go func(i int) { ... }(42)
})

@mvdan
Copy link
Member

mvdan commented Nov 23, 2016

@cznic if you mean without the extra go, this would be solved if the restriction on the func() signature was removed.

@dominikh
Copy link
Member

@mvdan Do you mean by allowing something like the following?

wg.Go(func(x, y int) { ...}, v1, v2)

IMHO that's way too much interface{} and not enough type safety.

@mattn
Copy link
Member

mattn commented Nov 23, 2016

panic is recovered?

@mvdan
Copy link
Member

mvdan commented Nov 23, 2016

@dominikh true; I was simply pointing at the issue without contemplating a solution :)

@rsc
Copy link
Contributor

rsc commented Nov 28, 2016

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

wg.Add(1)
defer wg.Done()

back to back and reject that always. Thoughts about how to make vet catch this reliably?

@renannprado
Copy link

I agree that vet is better place for this. The proposed API reminds of JavaScript, which will force us many times to wrap the code or function within a function with no arguments, while you could have just go func()....
Still nothing can stop you from creating such a helper methid, even though I don't see the need for it.

@rsc
Copy link
Contributor

rsc commented Dec 5, 2016

It sounds like we are deciding to make go vet check this and not add new API here. Any arguments against that?

@dsnet
Copy link
Member Author

dsnet commented Dec 5, 2016

SGTM

@dsnet dsnet changed the title proposal: sync: add Go method to WaitGroup cmd/vet: add check for sync.WaitGroup abuse Dec 5, 2016
@dsnet dsnet removed the Proposal label Dec 9, 2016
@rsc
Copy link
Contributor

rsc commented Aug 5, 2020

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?

@dominikh
Copy link
Member

dominikh commented Aug 6, 2020

Staticcheck has a fairly trivial check: for a GoStmt of a FuncLit, if the first statement in the FuncLit is a call to (*sync.WaitGroup).Add, we flag it. That has potential for false positives, but none have been reported in all the years that the check has existed.

The check could be trivially hardened by

  1. looking for an immediately following defer of (*sync.WaitGroup).Done and comparing the two receivers.
  2. checking that the argument to Add is 1, not some other number.

Edit: which is pretty much what you have suggested in #18022 (comment)

@rsc
Copy link
Contributor

rsc commented Aug 12, 2020

Thanks for the info @dominikh.
Would it be OK with you for vet to do the same thing?
Would you want to send the code?

@lpxz
Copy link

lpxz commented Dec 12, 2024

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)

But I'm not sure that all of the new positives are true: Consider this one:
https://go-mod-viewer.appspot.com/github.com/status-im/[email protected]/protocol/messenger_handler.go#L1762

m.shutdownWaitGroup.Add(1)
defer m.shutdownWaitGroup.Done()

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? :)

@adonovan
Copy link
Member

adonovan commented Dec 12, 2024

https://go-mod-viewer.appspot.com/github.com/getlantern/[email protected]/eventual_test.go#L102 is a delightful muddle

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.

https://go-mod-viewer.appspot.com/github.com/status-im/[email protected]/protocol/messenger_handler.go#L1762
m.shutdownWaitGroup.Add(1)
defer m.shutdownWaitGroup.Done()
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 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 Add; go func() { ... Done; } (); Wait structure (or bungled attempt at that structure).

A relevant question, what is the next step then? :)

We should probably reclassify the new positives in light of these observations. Ideally the false positive rate should be 5% or less.

@Groxx
Copy link

Groxx commented Dec 13, 2024

re https://go-mod-viewer.appspot.com/github.com/status-im/[email protected]/protocol/messenger_handler.go#L1762 :

Since it's a semaphore that is incorrect to use to Wait() and then Add() and nothing seems to guarantee that goroutine is scheduled before shutdown, I think that can be called "true". Nothing related to that goroutine is passed anywhere after it is created, nor any references to it (afaict) in that method that could lead to "wait for it to start m. downloadAndImportHistoryArchives before allowing shutdown".

(obviously this is worth verifying, but it seems extremely unlikely to be guaranteed-safe to me)

@lpxz
Copy link

lpxz commented Dec 13, 2024

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.

@egonelbre
Copy link
Contributor

But I'm not sure that all of the new positives are true: Consider this one: https://go-mod-viewer.appspot.com/github.com/status-im/[email protected]/protocol/messenger_handler.go#L1762

That's definitely a bug. (As other people already deduced). The easiest way to see this is to imagine a time.Sleep(10*time.Second) before line m.shutdownWaitGroup.Add(1) or task.Waiter.Add(1)... then any task.Waiter.Wait() or m.shutdownWaitGroup.Wait() may miss the first add and hence the wait is unnecessary.

@adonovan
Copy link
Member

adonovan commented Dec 13, 2024

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!

@lpxz
Copy link

lpxz commented Dec 13, 2024

@adonovan great, happy to make the contributions!

Will create a CL and add tests.
It may not be ready until early next January, let me know if this is more urgent.

Peng

@aclements
Copy link
Member

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 Add can race with Wait, such as in:

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 wg.Add on the first line of a closure. This has a zero false positive rate on the data we sampled, and is simple to implement.

@aclements aclements moved this from Active to Likely Accept in Proposals Jan 16, 2025
@lpxz
Copy link

lpxz commented Jan 16, 2025

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).

@adonovan
Copy link
Member

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.

@aclements
Copy link
Member

No change in consensus, so accepted. 🎉
This issue now tracks the work of implementing the proposal.
— aclements for the proposal review group

The proposal is to add a vet check for sync.WaitGroup misuse where Add can race with Wait, such as in:

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 wg.Add on the first line of a closure. This has a zero false positive rate on the data we sampled, and is simple to implement.

@aclements aclements moved this from Likely Accept to Accepted in Proposals Jan 23, 2025
@aclements aclements changed the title proposal: cmd/vet: add check for sync.WaitGroup abuse cmd/vet: add check for sync.WaitGroup abuse Jan 23, 2025
@aclements aclements modified the milestones: Proposal, Backlog Jan 23, 2025
@adonovan
Copy link
Member

adonovan commented Mar 26, 2025

Related: #63796 takes up the proposal originally made in this issue before (in Dec 2016) the API change was rejected and this issue was repurposed for the vet check.

@gopherbot
Copy link
Contributor

Change https://go.dev/cl/661518 mentions this issue: cmd/vendor: update golang.org/x/tools to v0.31.1-0.20250328151535-a857356d5cc5

@gopherbot
Copy link
Contributor

Change https://go.dev/cl/661519 mentions this issue: cmd/vet: add waitgroup analyzer

@dmitshur dmitshur added the FixPending Issues that have a fix which has not yet been reviewed or submitted. label Mar 29, 2025
@dmitshur dmitshur modified the milestones: Backlog, Go1.25 Mar 29, 2025
gopherbot pushed a commit that referenced this issue Apr 1, 2025
…7356d5cc5

Also, [email protected].

Updates #18022

Change-Id: I15a6d1979cc1e71d3065bc50f09dc8d3f6c6cdc0
Reviewed-on: https://go-review.googlesource.com/c/go/+/661518
Auto-Submit: Alan Donovan <[email protected]>
Reviewed-by: Dmitri Shuralyov <[email protected]>
LUCI-TryBot-Result: Go LUCI <[email protected]>
Reviewed-by: Dmitri Shuralyov <[email protected]>
Commit-Queue: Alan Donovan <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Analysis Issues related to static analysis (vet, x/tools/go/analysis) FixPending Issues that have a fix which has not yet been reviewed or submitted. Proposal-Accepted
Projects
Status: Accepted
Development

No branches or pull requests