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

Fix regression in handling flag-values starting with hyphen #1135

Closed
wants to merge 2 commits into from

Conversation

thaJeztah
Copy link

@thaJeztah thaJeztah commented May 8, 2020

fixes #1092

Fix for a regression between v1.22.1 and v1.22.2, where
flag values starting with a hyphen would be parsed as a flag:

runc update test_update --memory-swap -1
Incorrect Usage: flag provided but not defined: -1

This problem was caused by reorderArgs() not properly checking
if the next arg in the list was a value for the flag (and not
the next flag);

Before this patch:

args before reordering: --opt,--opt-value,arg1
args after reordering:  --opt,arg1,--opt-value
args before reordering: --opt,--opt-value,arg1,arg2
args after reordering:  --opt,arg1,--opt-value,arg2
args before reordering: arg1,--opt,--opt-value,arg2
args after reordering:  --opt,arg2,arg1,--opt-value

After this patch is applied:

args before reordering: --opt,--opt-value,arg1
args after reordering:  --opt,--opt-value,arg1
args before reordering: --opt,--opt-value,arg1,arg2
args after reordering:  --opt,--opt-value,arg1,arg2
args before reordering: arg1,--opt,--opt-value,arg2
args after reordering:  --opt,--opt-value,arg1,arg2

What type of PR is this?

(REQUIRED)

  • bug
  • cleanup
  • documentation
  • feature

What this PR does / why we need it:

see description above

Which issue(s) this PR fixes:

fixes #1092

Special notes for your reviewer:

(fill-in or delete this section)

Testing

Test was added

Release Notes

Fix a regression where flag values starting with a hyphen would be parsed as a flag instead of a value for a flag

@thaJeztah
Copy link
Author

thaJeztah commented May 8, 2020

wrote the test to a separate file (regression_test.go) and ran git bisect (don't do this often, so 😅)

git bisect start
git bisect good v1.22.0
git bisect bad v1.22.3
Bisecting: 62 revisions left to test after this (roughly 6 steps)
[41eb645fa27833f6d76257a58e62ab9b09366c82] Merge branch 'master' into pass-through-regression


git bisect run go test -run TestParseAndRunHyphenValues
...
Bisecting: 0 revisions left to test after this (roughly 0 steps)
[bfeb4c88ec4344b456abce2b587d7bc0b957e879] Merge branch 'master' into pass-through-regression
running go test -run TestParseAndRunHyphenValues
PASS
ok  	github.com/urfave/cli	0.006s
7d0751fa1309a01b405cde327cc43275d13016a2 is the first bad commit
commit 7d0751fa1309a01b405cde327cc43275d13016a2
Author: Lynn Cyrin <lynn@textio.com>
Date:   Wed Sep 11 19:25:51 2019 -0700

    add argIsFlag check

:100644 100644 d33435e6dd71cca15fab4a44d8ee3289f333232c 6471f1005b3e9c33be475d6dc1f322eddabfd606 M	command.go
bisect run success
git bisect log

git bisect start
# good: [bfe2e925cfb6d44b40ad3a779165ea7e8aff9212] Merge pull request #882 from urfave/lynncyrin-patch-1
git bisect good bfe2e925cfb6d44b40ad3a779165ea7e8aff9212
# bad: [c354ceca3e45cda773c3846bcb0c67cec4046b03] Merge pull request #981 from urfave/string-slice-issue-v1
git bisect bad c354ceca3e45cda773c3846bcb0c67cec4046b03
# bad: [41eb645fa27833f6d76257a58e62ab9b09366c82] Merge branch 'master' into pass-through-regression
git bisect bad 41eb645fa27833f6d76257a58e62ab9b09366c82
# good: [ef47250cda5ff52a313118c01ad6b0c5b4877a70] Merge branch 'master' into asahasrabuddhe-patch-1
git bisect good ef47250cda5ff52a313118c01ad6b0c5b4877a70
# good: [2071bcfb8328cb13e5575a586f06a33563b04054] checkpoint
git bisect good 2071bcfb8328cb13e5575a586f06a33563b04054
# bad: [09cdbbfe281fd79e92616a28f1fdf701e9007c22] more docs
git bisect bad 09cdbbfe281fd79e92616a28f1fdf701e9007c22
# bad: [fcfc69e54ff0e870bae1eaf599b7d15b46abb86b] Merge remote-tracking branch 'origin/master' into pass-through-regression
git bisect bad fcfc69e54ff0e870bae1eaf599b7d15b46abb86b
# bad: [7d0751fa1309a01b405cde327cc43275d13016a2] add argIsFlag check
git bisect bad 7d0751fa1309a01b405cde327cc43275d13016a2
# good: [bfeb4c88ec4344b456abce2b587d7bc0b957e879] Merge branch 'master' into pass-through-regression
git bisect good bfeb4c88ec4344b456abce2b587d7bc0b957e879
# first bad commit: [7d0751fa1309a01b405cde327cc43275d13016a2] add argIsFlag check

Looks like the regression was in 7d0751f (#872)

command.go Outdated Show resolved Hide resolved
command.go Outdated Show resolved Hide resolved
@thaJeztah thaJeztah changed the title [v1] Add test-case for flag-values starting with hyphen Fix regression in handling flag-values starting with hyphen May 8, 2020
@thaJeztah thaJeztah marked this pull request as ready for review May 8, 2020 12:12
@thaJeztah thaJeztah requested a review from a team as a code owner May 8, 2020 12:12
@thaJeztah
Copy link
Author

Found the issue; ready for review @AkihiroSuda @lynncyrin PTAL

@thaJeztah thaJeztah changed the title Fix regression in handling flag-values starting with hyphen [V1] Fix regression in handling flag-values starting with hyphen May 8, 2020
AkihiroSuda
AkihiroSuda previously approved these changes May 8, 2020
Copy link
Contributor

@AkihiroSuda AkihiroSuda left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks

command_test.go Outdated Show resolved Hide resolved
@coilysiren
Copy link
Member

@thaJeztah I have a requested change since this issue is particularly thorny!

Tangentially, I really appreciate how simple the fix is 🙏

@thaJeztah
Copy link
Author

Thanks for your review 🤗

Tangentially, I really appreciate how simple the fix is 🙏

Yes! It was hiding in plain sight, yet so easy to miss.

Fix for a regression between v1.22.1 and v1.22.2, where
flag values starting with a hyphen would be parsed as a flag:

    runc update test_update --memory-swap -1
    Incorrect Usage: flag provided but not defined: -1

This problem was caused by `reorderArgs()` not properly checking
if the next arg in the list was a value for the flag (and not
the next flag);

Before this patch:

    args before reordering: --opt,--opt-value,arg1
    args after reordering:  --opt,arg1,--opt-value
    args before reordering: --opt,--opt-value,arg1,arg2
    args after reordering:  --opt,arg1,--opt-value,arg2
    args before reordering: arg1,--opt,--opt-value,arg2
    args after reordering:  --opt,arg2,arg1,--opt-value

After this patch is applied:

    args before reordering: --opt,--opt-value,arg1
    args after reordering:  --opt,--opt-value,arg1
    args before reordering: --opt,--opt-value,arg1,arg2
    args after reordering:  --opt,--opt-value,arg1,arg2
    args before reordering: arg1,--opt,--opt-value,arg2
    args after reordering:  --opt,--opt-value,arg1,arg2

Co-authored-by: lynn (they) <lynncyrin@gmail.com>
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
@thaJeztah thaJeztah force-pushed the fix_flag_parsing branch 2 times, most recently from beaff81 to 119006d Compare May 14, 2020 12:52
If a `--` delimiter is encountered, any remaining _non-option-value_
arguments must be handled as argument.

From the POSIX spec (emphasis mine)
https://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1_chap12.html#tag_12_0):

> The first -- argument that *is not an option-argument* should be accepted
> as a delimiter indicating the end of options. Any following arguments
> should be treated as operands, even if they begin with the '-' character.

There was a corner-case scenario, where a `--flag` followed by a `--` did
not use `--` as value for the flag, but instead considered it as delimiter.

As a result;

    foo test arg1 --opt -- arg2

Would be reordered as:

   foo test --opt arg1 arg2

Which caused `arg1` to be used as value for `--opt`, instead of `--`,
which is the actual value.

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
Copy link
Author

@thaJeztah thaJeztah left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK, that was a lot of fun; adding tests for -- revealed a corner-case situation that looks like a bug. I pushed a second commit to address that problem, but (see my comment inline) could use some extra eyes; thanks!

@@ -546,8 +546,7 @@ func TestApp_CommandWithFlagBeforeTerminator(t *testing.T) {

expect(t, parsedOption, "my-option")
expect(t, args[0], "my-arg")
expect(t, args[1], "--")
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could use extra eyes on this change; it's a result of the latest commit / fix, but could potentially be a breaking change for users (so at least should be called out in the release notes); I think I interpreted the POSIX spec correctly with my patch, but specs can sometimes be ambiguous, so could use the extra eyes.

I also haven't checked the v2 branch/version yet (possibly the fix is needed there as well, or in another form)

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So far, the twitter-verse seems to consolidate to my interpretation being correct 😂 https://twitter.com/thaJeztah/status/1260921988102656002?s=20

Copy link
Member

@coilysiren coilysiren Jul 2, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm having a hard time imagining if this is [ a mundane change, or if it'll cause mass chaos for somebody somewhere ]. Is there a way to keep this behavior as-is???

@thaJeztah
Copy link
Author

(I'm ignoring codecov for now, as I think it's just a rounding error 😅)

@coilysiren coilysiren self-requested a review May 19, 2020 16:49
@thaJeztah
Copy link
Author

@lynncyrin anything needed to move this forward?

@coilysiren
Copy link
Member

@thaJeztah I have (hopefully only) 1 more question!

Copy link
Member

@coilysiren coilysiren left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(I'm formally requesting changes for my comment on the -- change! re-request my code review when you've answered it)

@@ -234,7 +234,7 @@ func reorderArgs(commandFlags []Flag, args []string) []string {
break

// checks if this arg is a value that should be re-ordered next to its associated flag
} else if nextIndexMayContainValue && !strings.HasPrefix(arg, "-") {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is still not quite correct if you wanted to do something like:

% runc run --bundle --some-real-runc-flag ctr

Where it would still re-order things. The least-ugly way I can think of solving this is that we change nextIndexMayContainValue to make use of TakesValue() (with some interface casting) and removing this "skip reordering flags that start with -" logic if it doesn't break anything.

I also hasten to point out that urfave/cli has actually had this behaviour for a very long time and we (speaking collectively in the containers community) have misunderstood how urfave/cli handles this case. I discussed this in #1152 (which I've marked as a duplicate of this) and I've had to work around this in umoci (see opencontainers/umoci#328 for how I ended up hotfixing this -- you just disable argument reordering). Since we want to avoid breaking other users of urfave/cli we might have to migrate runc, umoci, and containerd away from urfave/cli (maybe to spf13/cobra which is what Docker uses)...

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A possible patch to do the above might look like the following. This change does pass all of the existing tests...

diff --git a/command.go b/command.go
index f02d3589ff3a..3c8e971a1c53 100644
--- a/command.go
+++ b/command.go
@@ -223,7 +223,7 @@ func (c *Command) useShortOptionHandling() bool {
 func reorderArgs(commandFlags []Flag, args []string) []string {
        var remainingArgs, reorderedArgs []string
 
-       nextIndexMayContainValue := false
+       nextIndexIsValue := false
        for i, arg := range args {
 
                // dont reorder any args after a --
@@ -233,17 +233,29 @@ func reorderArgs(commandFlags []Flag, args []string) []string {
                        remainingArgs = append(remainingArgs, args[i:]...)
                        break
 
-                       // checks if this arg is a value that should be re-ordered next to its associated flag
-               } else if nextIndexMayContainValue && !strings.HasPrefix(arg, "-") {
-                       nextIndexMayContainValue = false
+                       // if this argument is a value then it isn't a flag and should be
+                       // kept with its associated flag (which was the last flag
+                       // reordered)
+               } else if nextIndexIsValue {
+                       nextIndexIsValue = false
                        reorderedArgs = append(reorderedArgs, arg)
 
                        // checks if this is an arg that should be re-ordered
-               } else if argIsFlag(commandFlags, arg) {
+               } else if flag := findFlagForArg(commandFlags, arg); flag != nil {
                        // we have determined that this is a flag that we should re-order
                        reorderedArgs = append(reorderedArgs, arg)
-                       // if this arg does not contain a "=", then the next index may contain the value for this flag
-                       nextIndexMayContainValue = !strings.Contains(arg, "=")
+
+                       // if the flag takes an argument and doesn't contain a "=" then the
+                       // next index contains a value for this flag -- unfortunately we
+                       // have to depend on the Flag implementing DocGenerationFlag for
+                       // TakesValue().
+                       if docFlag, ok := (*flag).(DocGenerationFlag); ok {
+                               nextIndexIsValue = docFlag.TakesValue() && !strings.Contains(arg, "=")
+                       } else {
+                               // assume that flags take arguments
+                               // XXX: This is quite ugly and I'm not sure what we should do here.
+                               nextIndexIsValue = !strings.Contains(arg, "=")
+                       }
 
                        // simply append any remaining args
                } else {
@@ -254,15 +266,16 @@ func reorderArgs(commandFlags []Flag, args []string) []string {
        return append(reorderedArgs, remainingArgs...)
 }
 
-// argIsFlag checks if an arg is one of our command flags
-func argIsFlag(commandFlags []Flag, arg string) bool {
+// findFlagForArg checks if an arg is one of our command flags and returns the
+// Flag if the arg is a flag (nil otherwise).
+func findFlagForArg(commandFlags []Flag, arg string) *Flag {
        // checks if this is just a `-`, and so definitely not a flag
        if arg == "-" {
-               return false
+               return nil
        }
        // flags always start with a -
        if !strings.HasPrefix(arg, "-") {
-               return false
+               return nil
        }
        // this line turns `--flag` into `flag`
        if strings.HasPrefix(arg, "--") {
@@ -279,12 +292,12 @@ func argIsFlag(commandFlags []Flag, arg string) bool {
                for _, key := range strings.Split(flag.GetName(), ",") {
                        key := strings.TrimSpace(key)
                        if key == arg {
-                               return true
+                               return &flag
                        }
                }
        }
-       // return false if this arg was not one of our flags
-       return false
+       // couldn't find the flag
+       return nil
 }
 
 // Names returns the names including short names and aliases.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@cyphar I see you commented on the first commit; did you check with the second commit as well?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(I was looking at the requested changes to preserve some of the existing behaviour)

Copy link

@cyphar cyphar Jul 3, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, this was just talking about the behaviour of the first change (and not the fun -- semantics that you quizzed everyone on in May). I /think/ that it should be fairly straight-forward (move the -- check after the "is this an option-value check" as in your current patch) but I agree with both of you that that particular change looks quite scary.

I think the --opt -a case is far more clear-cut.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So, in your example case (runc run --bundle --some-real-runc-flag) both --bundle and --some-real-runc-flag are "known" flags to runc, but the second one should be treated as value for --bundle (so --bundle="--some-real-runc-flag") or as flag? In the former case, wouldn't it have to be used as runc run --bundle -- --some-real-runc-flag? 🤔)

Copy link

@cyphar cyphar Jul 3, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The former (--some-real-runc-flag treated as a avalue for --bundle). The motivating use-case is (aside from POSIX-correctness and compatibility with getopt(3)) similar to --:

% runc run --bundle --foo ctr # where ./--foo is a directory containing an OCI bundle

(Well, for umoci it's more like umoci config --image foo:bar --config.cmd sh --config.cmd -c but that's a slightly more complicated example. But the root cause is the same as your original --memory-limit -1 issue.)

Should mean that --foo is a value for --bundle regardless of whether --foo is a known command-line flag. Currently (and with your patches), urfave/cli will not reorder --bundle --foo (where --foo is not a valid flag for runc) but it will reorder the flags if you do --bundle --systemd-cgroup (where --systemd-cgroup is a supported flag for runc). Even more strangely, --bundle=--systemd-cgroup works without issue because the arguments don't get reordered.

The patch I posted above should make it so that --bundle=--foo acts exactly like --bundle --foo regardless of whether --foo is a valid runc flag.

In the former case, wouldn't it have to be used as runc run --bundle -- --some-real-runc-flag?

I disagree (especially if we take the interpretation that -- should be an option-value in that case). If you use getopt:

% getopt -o '' -l bundle:,systemd-cgroup: -- --bundle --systemd-cgroup container-name
 --bundle '--systemd-cgroup' -- 'container-name'

But also it's the obvious behaviour if you just inject =s:

 getopt -o '' -l bundle:,systemd-cgroup: -- --bundle=--systemd-cgroup container-name
 --bundle '--systemd-cgroup' -- 'container-name'

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@thaJeztah PTAL?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ping @thaJeztah 🙏

@coilysiren
Copy link
Member

👀

@cyphar
Copy link

cyphar commented Jan 29, 2021

I think my earlier comment (#1135 (comment)) still stands. I can cook up a proper PR based on the patch I posted above which handles --flagA --flagB as "--flagB" is the value of --flagA regardless of whether --flagB is a known flag, if you'd still like that change.

@brandond
Copy link

@cyphar is this still on your radar?

@stale
Copy link

stale bot commented Jun 26, 2021

This issue or PR has been automatically marked as stale because it has not had recent activity. Please add a comment bumping this if you're still interested in it's resolution! Thanks for your help, please let us know if you need anything else.

@stale stale bot added the status/stale stale due to the age of it's last update label Jun 26, 2021
@stale
Copy link

stale bot commented Jul 26, 2021

Closing this as it has become stale.

@stale stale bot closed this Jul 26, 2021
@cyphar
Copy link

cyphar commented Sep 12, 2021

I think on paper this is still something we'd like to have fixed for runc and umoci, but disabling argument reordering fixed the issue well enough that it isn't bothering us at the moment (though it does mean that some of the other argument reordering features now don't work because we've disabled them). I can take a crack at implementing the behaviour I mentioned above (I think I might still have the code on my laptop) but I'd like to know whether there is interest in taking it -- you've mentioned before that touching this stuff can be quite hairy and it's not clear whether fixing a bug for one user will go on to cause bugs for another five (and as a fellow maintainer I get where you're coming from!).

@meatballhat meatballhat reopened this Apr 19, 2022
@meatballhat meatballhat added area/v1 relates to / is being considered for v1 and removed status/stale stale due to the age of it's last update labels Apr 21, 2022
@meatballhat meatballhat changed the title [V1] Fix regression in handling flag-values starting with hyphen Fix regression in handling flag-values starting with hyphen Apr 21, 2022
@meatballhat meatballhat added the kind/bug describes or fixes a bug label Apr 21, 2022
@meatballhat meatballhat added this to the Release 1.22.x milestone Apr 21, 2022
@meatballhat
Copy link
Member

Superseded by #1356

kolyshkin added a commit to kolyshkin/runc that referenced this pull request Apr 22, 2022
This finally fixes the regression of not allowing -1 as an argument,
which is reported in urfave/cli#1135.

Signed-off-by: Kir Kolyshkin <kolyshkin@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
area/v1 relates to / is being considered for v1 kind/bug describes or fixes a bug
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

6 participants