brbzull0 opened a new issue, #13746:
URL: https://github.com/apache/trafficserver/issues/13746

   ### Impact
   
   `ArgParser` matches a command's options anywhere in the rest of the command 
line, and the `--` escape added in #13570 does not reach that scan. For the 
top-level command that is the whole argv, so a global option is recognised even 
when it appears as a subcommand's value, and even after `--`, which #13570 made 
the way to pass a value that starts with `-`. The same scan lets a global 
option silently shadow a subcommand option that has the same short name.
   
   What an operator sees, with `traffic_ctl` (it applies equally to 
`traffic_layout` and `traffic_cache_tool`):
   
   - A value spelled like a global option (`-f`, `-w`, `--run-root`, 
`--read-timeout-ms`, ...) is taken as that option: usage error, exit 64, and 
`--` does not help.
   - A value of `-h` or `-V` prints help or the version and exits **0**. The 
command never runs, but a script sees success.
   - `traffic_ctl config reload -w <seconds>` sets the global `--watch`, which 
nothing in the tree reads, instead of the documented `config reload 
--initial-wait, -w`.
   
   It takes a value that happens to match a global option name, so real-world 
impact is low. There is no workaround for such values. For the `-w` clash, 
`--initial-wait` works.
   
   ```
   Version:  master @ 738cd651b3
             top-level scan unchanged since d9b06a12acf (2018); the "--" escape 
added in d0fb283406 (#13570)
             only covers collecting an option's own values. The -w clash dates 
from 5bab268cb4 (#12892).
   Platform: Darwin 25.6.0, Apple clang 21.0.0
   Config:   n/a
   ```
   
   ### Proof
   
   `Command::parse()` runs `append_option_data()` on the command's own argv 
before recursing into subcommands, and `append_option_data()` walks every 
remaining token with no check for `--`:
   
   
[`src/tscore/ArgParser.cc#L737-L740`](https://github.com/apache/trafficserver/blob/738cd651b389bd32b4d0887f99d0a1960abc12b7/src/tscore/ArgParser.cc#L737-L740),
 called at 
[`#L861`](https://github.com/apache/trafficserver/blob/738cd651b389bd32b4d0887f99d0a1960abc12b7/src/tscore/ArgParser.cc#L861):
   
   ```cpp
   ArgParser::Command::append_option_data(Arguments &ret, AP_StrVec &args, int 
index)
   {
     std::map<std::string, unsigned> check_map;
     for (unsigned i = index; i < args.size(); i++) {
   ```
   
   `--` is only honoured inside `handle_args()`, once an option has been 
matched and is collecting its values 
([`#L564`](https://github.com/apache/trafficserver/blob/738cd651b389bd32b4d0887f99d0a1960abc12b7/src/tscore/ArgParser.cc#L564),
 
[`#L591`](https://github.com/apache/trafficserver/blob/738cd651b389bd32b4d0887f99d0a1960abc12b7/src/tscore/ArgParser.cc#L591),
 
[`#L620`](https://github.com/apache/trafficserver/blob/738cd651b389bd32b4d0887f99d0a1960abc12b7/src/tscore/ArgParser.cc#L620)).
 A `-h` anywhere prints help with exit code 0 
([`#L768-L778`](https://github.com/apache/trafficserver/blob/738cd651b389bd32b4d0887f99d0a1960abc12b7/src/tscore/ArgParser.cc#L768-L778)).
   
   `traffic_ctl` registers `-w` twice, globally for `--watch` 
([`src/traffic_ctl/traffic_ctl.cc#L95`](https://github.com/apache/trafficserver/blob/738cd651b389bd32b4d0887f99d0a1960abc12b7/src/traffic_ctl/traffic_ctl.cc#L95))
 and on `config reload` for `--initial-wait` 
([`#L172`](https://github.com/apache/trafficserver/blob/738cd651b389bd32b4d0887f99d0a1960abc12b7/src/traffic_ctl/traffic_ctl.cc#L172)).
 The `watch` key is never read.
   
   Reproduction, with no traffic_server running; the RPC connection error shows 
parsing got through:
   
   ```
   $ traffic_ctl config set proxy.config.diags.debug.tags -- -m      # -m is 
not a global option: passed through
   Error found:
   RPC Node Error: connect(attempts=1/5): Couldn't open connection with 
.../jsonrpc20.sock. Last error: No such file or directory(2)
   [exit 2]
   
   $ traffic_ctl config set proxy.config.diags.debug.tags -- -f      # -f is: 
taken as --format
   Error: 1 argument(s) expected by format
   [exit 64]
   
   $ traffic_ctl config set proxy.config.diags.debug.tags -- -f json
   Error: 2 argument(s) expected by set
   [exit 64]
   
   $ traffic_ctl config set proxy.config.diags.debug.tags -h         # help, 
exit 0, nothing set
   Usage: traffic_ctl [OPTIONS] CMD [ARGS ...]
   [exit 0]
   
   $ traffic_ctl config reload -w                                    # 
documented as --initial-wait
   Error: 1 argument(s) expected by watch
   [exit 64]
   ```
   
   JSON output form (the parse fails before any output is formatted, so it 
matches):
   
   ```
   $ traffic_ctl --format json config set proxy.config.diags.debug.tags -- -f 
json
   Error: 2 argument(s) expected by set
   [exit 64]
   ```
   
   Expected: after `--`, `-f` and `-h` are the value of 
`proxy.config.diags.debug.tags`, as `-m` already is.
   
   `--` itself works. It is the global scan that ignores it: `-m` is not a 
global option and passes, `-f` and `-h` are and do not.
   
   ### Proposed change
   
   `--` is scoped to one option today (#13570):
   - For a fixed or at-most-one option it escapes the next value, and options 
written after that value still parse: `server debug enable --tags -- -a`, 
`config get -c -- -weird.yaml proxy.config.x`.
   - For a variable-arity option it ends that option's own recognition: `config 
reload -D -- -m`.
   
   The parent levels just need to leave alone the token that `--` escapes. Let 
`append_option_data()` skip `--` and the token after it, and leave both in 
place for the option or command further down that owns them:
   
   ```diff
   --- a/src/tscore/ArgParser.cc
   +++ b/src/tscore/ArgParser.cc
   @@ -738,6 +738,11 @@ ArgParser::Command::append_option_data(Arguments &ret, 
AP_StrVec &args, int inde
    {
      std::map<std::string, unsigned> check_map;
      for (unsigned i = index; i < args.size(); i++) {
   +    // "--" escapes the token after it, which belongs to an option or 
command further down: leave both for it.
   +    if (args[i] == "--") {
   +      ++i;
   +      continue;
   +    }
        // find matches of the arg
        if (args[i][0] == '-' && args[i][1] == '-' && args[i].find('=') != 
std::string::npos) {
          // deal with --args=
   ```
   
   I tried it on the tree above. The escaped tokens are handed on:
   - `config set <rec> -- -f` and `config set <rec> -- -h` now reach the RPC.
   
   Every existing pattern keeps its behaviour, with exit codes and results 
identical to master:
   - `server debug enable --tags -- -a -f json`
   - `config get --cold -- -weird.yaml <rec> -f json`
   - `config reload -D -- -m -f json`
   - `config set <rec> -f json -- -x`
   
   The tscore unit tests pass (115/115). So do the AuTests that use `--`: 
`traffic_ctl_server_debug`, `traffic_ctl_cold_config`, 
`config_reload_directive_cli`, `traffic_ctl_config_output`, 
`traffic_ctl_config_reload`.
   
   I rejected the obvious alternative, stopping option recognition at `--` 
altogether (`break`). It turns `--` into an end-of-line marker at every level 
and breaks a global option written after an escaped value: `server debug enable 
--tags -- -a -f json` fails with `Unknown command, option or args: '-f' 'json'`.
   
   What skip-one does not cover:
   - After a variable-arity option's `--`, only the first value is protected 
from the global scan. In `config reload -D -- -a -f json`, `-f json` is still 
taken as the global `--format`, the same as today. That contradicts the 
`traffic_ctl.en.rst` note that options written after `--` are swallowed by 
`-D`. `--directive=-value` stays the way to pass several dash values.
   - The `-w` clash. The global `--watch` is undocumented and unused, so 
dropping it (or its short form) gives `config reload -w` back to 
`--initial-wait`.
   
   Still to do:
   - A unit test in `src/tscore/unit_tests/test_ArgParser.cc`: a subcommand 
value after `--` that spells a global option is left to the subcommand, and a 
global option after the escaped value is still recognised.
   - Optionally, reject at registration a subcommand option whose long or short 
name is already taken by an ancestor, so the next clash fails at startup 
instead of being silently shadowed.
   
   Related:
   - #13597: the other direction, where a parent option takes a subcommand's 
option as its value. Same root cause: every level scans the rest of argv.
   - #13570: added the `--` escape for option values.
   


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to