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

flag: FlagSet should be marked as noCopy to prevent buggy behavior #51507

Open
mvdan opened this issue Mar 6, 2022 · 6 comments
Open

flag: FlagSet should be marked as noCopy to prevent buggy behavior #51507

mvdan opened this issue Mar 6, 2022 · 6 comments
Labels
NeedsInvestigation Someone must examine and confirm this is a valid issue and not a duplicate of an existing one.
Milestone

Comments

@mvdan
Copy link
Member

mvdan commented Mar 6, 2022

I had a test that called flag.NewFlagSet followed by FlagSet.SetOutput(io.Discard). Yet, in some cases, the flagset would print straight to stderr, ignoring my instructions.

I reduced the code into this reproducer: https://go.dev/play/p/KgDpqcSeR2c

When reduced, it's relatively straightforward to see what is going on in the non-pointer case:

  1. The code calls NewFlagSet, which creates pointer P1, and sets f.Usage = f.defaultUsage with P1 as the method receiver parameter.
  2. The code then dereferences NewFlagSet, resulting in a variable of non-pointer type FlagSet, making a shallow copy of the struct.
  3. The code calls SetOutput, a method with a pointer receiver. It's equivalent to calling (&f2).SetOutput; note that this is a pointer to the stack, P2. P2's output field is updated, but not P1's.
  4. When calling f2.Parse, we ultimately end up calling f2.Usage on P2, which is still to P1's defaultUsage method.
  5. As such, we use P1's output field to print the usage, not P2's.

Arguably this is my mistake for dereferencing NewFlagSet, and I tend to agree. The buggy test was, in fact, in the standard library itself:

flags = *NewFlagSet("test", ContinueOnError)

I think we should add a noCopy field to the FlagSet struct to make vet error when users make full copies of flagsets. In some cases that can be fine, such as when the user doesn't call SetOutput, but in general I think it's confusing and can easily lead to bugs. And, most importantly, I don't see why anyone would ever want to make a shallow copy of a FlagSet like this. If we wanted to support a full "clone" operation, it would have to be a new feature without the output field bug above.

Another option would be to remove the f.Usage = f.defaultUsage line from NewFlagSet, so that then P2's call to the usage method would go into if f.Usage == nil { f.defaultUsage }, calling P2's defaultUsage rather than P1's. However, I think this could be a backwards incompatible change, as f := NewFlagSet(); f.Usage() works today, and it would start panicking with such a change.

Yet another option is to document this pointer dereference gotcha in the FlagSet docs. Certainly better than nothing, but I reckon that a vet check is still a much better outcome.

@mvdan
Copy link
Member Author

mvdan commented Mar 6, 2022

cc @robpike per https://dev.golang.org/owners for flag, and @matloob @timothy-king for cmd/vet
cc @josharian given his involvement in past noCopy issues

@mvdan
Copy link
Member Author

mvdan commented Mar 6, 2022

Yet another option is to change the field output io.Writer to something like output *io.Writer, so that calling SetOutput in the shallow copy of the FlagSet would also change all other shallow copies. I reckon that's not a good idea though, as other fields within FlagSet do not support shallow copies either.

@mvdan
Copy link
Member Author

mvdan commented Mar 6, 2022

https://go-review.googlesource.com/c/go/+/390234 fixes the test code.

@timothy-king
Copy link
Contributor

Thoughts:

  1. Ignoring backwards compatibility my preferred option is "remove the f.Usage = f.defaultUsage line".
  2. I am not sure this satisfies cmd/vet frequency criteria. *flags is kinda odd and I don't know why someone would want to do this. But someone could get data for this.
  3. Is it okay to make a shallow copy and then set the f.Usage field? This would be really odd to do, but it avoids this specific issue. If we this it is okay to do this, that might make this hard to fit into noCopy.

@mvdan
Copy link
Member Author

mvdan commented Mar 6, 2022

Responding to your second point: note that I'm not proposing an entirely new vet check, but rather adding the noCopy hint for the existing copy analyzer. I imagine the frequency criteria is less important given this.

Doing a bit of research on this is relatively easy, though. Using GitHub's new code search, we can already find over a hundred cases. It's very likely that not all of them call SetOutput already, but I would argue that they are all buggy in that they are hidden traps waiting to be triggered :)

@mvdan
Copy link
Member Author

mvdan commented Mar 6, 2022

As for your third point: if we wanted to explicitly allow the user to make a copy of a FlagSet, I think we would need to add a method for it that does so carefully (e.g. by also not sharing its pointer-like fields like the maps and slice). I personally don't think it's going to be needed, but we can also try the noCopy approach and see if there are any users who did rely on a form of copying for good reasons.

@seankhliao seankhliao added the NeedsInvestigation Someone must examine and confirm this is a valid issue and not a duplicate of an existing one. label Mar 8, 2022
@seankhliao seankhliao added this to the Unplanned milestone Aug 20, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
NeedsInvestigation Someone must examine and confirm this is a valid issue and not a duplicate of an existing one.
Projects
None yet
Development

No branches or pull requests

3 participants