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

   ### Impact
   
   `ArgParser` keeps its parser-wide state in five file-scope variables that 
have
   **external linkage** — they are not `static` and not in an anonymous 
namespace — so
   they are exported as plain, unmangled symbols from `libtscore`:
   
   ```
   $ nm -g build/src/tscore/libtscore.a | grep -E ' [SDB] 
_(description|default_command|global_usage|parser_program_name|usage_return_code)$'
   000000000010e880 S _default_command
   000000000010e850 S _description
   000000000010e838 S _global_usage
   000000000010e868 S _parser_program_name
   000000000001bcbc D _usage_return_code
   ```
   
   Two consequences:
   
   1. **Symbol leak.** `libtscore` exports `_description` and `_global_usage`, 
names
      generic enough to collide with any other translation unit or plugin that 
defines a
      global with the same name. Nothing in-tree collides today, so there is no 
known
      production impact — but the surface is real and it is trivially avoidable.
   2. **`set_default()` leaks state into the rest of the process**, because
      `default_command` is never cleared. This is what makes the `ret = 
Arguments{}` guard
      added in #13570 untestable: a unit test that calls `set_default()` changes
      `default_command` for every later test in the same binary, so the 
default-command
      retry path cannot be exercised in isolation.
   
   No operator-visible misbehaviour, and no workaround needed — this is a latent
   maintainability and testability defect, filed as follow-up to review 
feedback on
   #13570.
   
   ```
   Version:  master @ d0fb2834069, present since e677cebdf6 (2018-10-09, 
shipped in 10.0.0)
   Platform: any
   Config:   n/a
   ```
   
   ### Proof
   
   All five declarations are at file scope with no `static` and no enclosing 
anonymous
   namespace —
   
[`src/tscore/ArgParser.cc#L34-L41`](https://github.com/apache/trafficserver/blob/d0fb283406931e502ec0737e5af8ddf0dcc71c57/src/tscore/ArgParser.cc#L34-L41):
   
   ```cpp
   std::string global_usage;
   std::string description;
   std::string parser_program_name;
   std::string default_command;
   
   // by default return EX_USAGE(64) when usage is called.
   // if -h or --help is called specifically, return 0
   int usage_return_code = EX_USAGE;
   ```
   
   `default_command` is written in two places and cleared in none —
   
[`ArgParser.cc#L163`](https://github.com/apache/trafficserver/blob/d0fb283406931e502ec0737e5af8ddf0dcc71c57/src/tscore/ArgParser.cc#L163)
   (`set_default_command`) and
   
[`ArgParser.cc#L825`](https://github.com/apache/trafficserver/blob/d0fb283406931e502ec0737e5af8ddf0dcc71c57/src/tscore/ArgParser.cc#L825)
   (`Command::set_default`):
   
   ```cpp
   ArgParser::Command &
   ArgParser::Command::set_default()
   {
     default_command = _name;
     return *this;
   }
   ```
   
   The retry path it gates, at
   
[`ArgParser.cc#L195-L202`](https://github.com/apache/trafficserver/blob/d0fb283406931e502ec0737e5af8ddf0dcc71c57/src/tscore/ArgParser.cc#L195-L202):
   
   ```cpp
     if (!_top_level_command.parse(ret, args)) {
       // deal with default command
       if (!default_command.empty()) {
         args = _argv;
         args.insert(args.begin() + 1, default_command);
         // The pass that failed may have collected options before it gave up. 
Those values would
         // now accumulate on top of the ones the retry collects rather than be 
replaced.
         ret = Arguments{};
         _top_level_command.parse(ret, args);
       }
     };
   ```
   
   Every use of the five variables is inside `ArgParser.cc`; the only other 
mention in the
   tree is a comment in the header, so nothing depends on the external linkage:
   
   ```bash
   $ grep -rn 
'\bdefault_command\b\|\bparser_program_name\b\|\busage_return_code\b\|\bglobal_usage\b'
 \
       src/ include/ plugins/ --include='*.cc' --include='*.h' | grep -v 
src/tscore/ArgParser.cc
   include/tscore/ArgParser.h:312:  // Add the usage to global_usage for 
help_message(). Something like: traffic_blabla [--SWITCH [ARG]]
   ```
   
   Found by inspection plus `nm`; not reproduced as a runtime failure, because 
the
   symbol collision needs a second definition that does not currently exist 
in-tree.
   
   ### Proposed change
   
   Give the variables internal linkage and add a test-only reset so the retry 
path
   becomes testable.
   
   ```diff
   --- a/src/tscore/ArgParser.cc
   +++ b/src/tscore/ArgParser.cc
   @@ -31,6 +31,10 @@
    #include <utility>
    #include <sysexits.h>
    
   +// Internal linkage: these are parser-wide state for this translation unit 
only. At file
   +// scope without static they are exported from libtscore under names 
generic enough
   +// ("description", "global_usage") to collide with any other global of the 
same name.
   +namespace
   +{
    std::string global_usage;
    std::string description;
    std::string parser_program_name;
   @@ -39,6 +43,7 @@
    // by default return EX_USAGE(64) when usage is called.
    // if -h or --help is called specifically, return 0
    int usage_return_code = EX_USAGE;
   +} // namespace
    
    namespace ts
    {
   @@ -45,6 +50,17 @@ namespace ts
    bool ArgParser::_test_mode = false;
    
   +void
   +ArgParser::reset_global_state()
   +{
   +  global_usage.clear();
   +  description.clear();
   +  parser_program_name.clear();
   +  default_command.clear();
   +  usage_return_code = EX_USAGE;
   +}
   +
   ```
   
   ```diff
   --- a/include/tscore/ArgParser.h
   +++ b/include/tscore/ArgParser.h
   @@
   +  /// Clear the parser-wide state held at file scope in ArgParser.cc.
   +  ///
   +  /// Test-only. Production code builds one ArgParser per process and never 
reuses the
   +  /// state, but a unit test binary runs many parsers in sequence, and 
set_default()
   +  /// would otherwise leak default_command into every later test.
   +  static void reset_global_state();
   ```
   
   Making them members of `ArgParser` would be the tidier fix, but they are 
read from
   `ArgParser::Command` methods as well, so that turns into a wider refactor 
with no extra
   benefit for the symbol problem. Internal linkage plus a reset hook fixes 
both the export
   and the testability in a change that is easy to review.
   
   No API, config-key or metric-name change, so no release note or backport is 
implied.
   
   Still to do: a unit test for the default-command retry, using 
`reset_global_state()`,
   asserting that options collected by the failed first pass do not accumulate 
on top of
   the retry's values.
   
   ### Remaining review notes from #13570
   
   Recorded here so they are not lost. All were raised as explicitly 
non-blocking.
   
   - **Empty-value policy is split across two layers.** The parser rejects `-c 
""` for
     at-most-one arity, while `-D ""` and `-d ""` parse fine and are caught 
later by
     `has_empty_value()` at
     
[`CtrlCommands.cc#L60`](https://github.com/apache/trafficserver/blob/d0fb283406931e502ec0737e5af8ddf0dcc71c57/src/traffic_ctl/CtrlCommands.cc#L60).
     Having `handle_args` reject an empty value token uniformly would 
centralise it, but it
     would affect other consumers, so the split may be deliberate.
   
   - **Dead branch.** `has_empty_value(dir_args)` returns early if *any* 
directive value is
     empty
     
([`CtrlCommands.cc#L621-L625`](https://github.com/apache/trafficserver/blob/d0fb283406931e502ec0737e5af8ddf0dcc71c57/src/traffic_ctl/CtrlCommands.cc#L621-L625)),
     so the `if (dir.empty()) { continue; }` in the loop below at
     
[`#L628-L630`](https://github.com/apache/trafficserver/blob/d0fb283406931e502ec0737e5af8ddf0dcc71c57/src/traffic_ctl/CtrlCommands.cc#L628-L630)
     is unreachable and can go.
   
   - **Document the `--` rule rather than change it.** It is one rule: option 
recognition
     goes off for the remainder of *that option's* value collection. It only 
reads as three
     behaviours because the arities differ — `-D` collects to the end of the 
line, `-c`
     escapes exactly one token, a fixed arity lasts until its values are 
filled. Stating the
     general rule in `doc/developer-guide/internal-libraries/ArgParser.en.rst` 
would stop the
     three arities from looking like three separate features.
   
   - **Comment worth adding to `is_registered_option()`.** It consults only the 
current
     command's options
     
([`ArgParser.cc#L527`](https://github.com/apache/trafficserver/blob/d0fb283406931e502ec0737e5af8ddf0dcc71c57/src/tscore/ArgParser.cc#L527)),
     which is correct because `append_option_data()` sweeps the remaining 
vector for the
     parent's options before recursing into subcommands. It does mean a 
variable-arity option
     declared on a command that *has* subcommands would still swallow the 
subcommand name.
     Every variable-arity option in `traffic_ctl` today is on a leaf command 
(`reload`,
     `invoke`), so nothing reaches it — but the next person to add one will 
want to know.
   


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