You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
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:
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.
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 Argparser variable arg option parsing fix/improvement. #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:
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 0int usage_return_code = EX_USAGE;
default_command is written in two places and cleared in none — ArgParser.cc#L163
(set_default_command) and ArgParser.cc#L825
(Command::set_default):
if (!_top_level_command.parse(ret, args)) {
// deal with default commandif (!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:
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.
--- 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;+}+
--- 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.
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.
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),
so the if (dir.empty()) { continue; } in the loop below at #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),
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.
Impact
ArgParserkeeps its parser-wide state in five file-scope variables that haveexternal linkage — they are not
staticand not in an anonymous namespace — sothey are exported as plain, unmangled symbols from
libtscore:Two consequences:
libtscoreexports_descriptionand_global_usage, namesgeneric 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.
set_default()leaks state into the rest of the process, becausedefault_commandis never cleared. This is what makes theret = Arguments{}guardadded in Argparser variable arg option parsing fix/improvement. #13570 untestable: a unit test that calls
set_default()changesdefault_commandfor every later test in the same binary, so the default-commandretry 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.
Proof
All five declarations are at file scope with no
staticand no enclosing anonymousnamespace —
src/tscore/ArgParser.cc#L34-L41:default_commandis written in two places and cleared in none —ArgParser.cc#L163(
set_default_command) andArgParser.cc#L825(
Command::set_default):The retry path it gates, at
ArgParser.cc#L195-L202:Every use of the five variables is inside
ArgParser.cc; the only other mention in thetree is a comment in the header, so nothing depends on the external linkage:
Found by inspection plus
nm; not reproduced as a runtime failure, because thesymbol 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.
Making them members of
ArgParserwould be the tidier fix, but they are read fromArgParser::Commandmethods as well, so that turns into a wider refactor with no extrabenefit 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 ""forat-most-one arity, while
-D ""and-d ""parse fine and are caught later byhas_empty_value()atCtrlCommands.cc#L60.Having
handle_argsreject an empty value token uniformly would centralise it, but itwould affect other consumers, so the split may be deliberate.
Dead branch.
has_empty_value(dir_args)returns early if any directive value isempty
(
CtrlCommands.cc#L621-L625),so the
if (dir.empty()) { continue; }in the loop below at#L628-L630is unreachable and can go.
Document the
--rule rather than change it. It is one rule: option recognitiongoes off for the remainder of that option's value collection. It only reads as three
behaviours because the arities differ —
-Dcollects to the end of the line,-cescapes exactly one token, a fixed arity lasts until its values are filled. Stating the
general rule in
doc/developer-guide/internal-libraries/ArgParser.en.rstwould stop thethree arities from looking like three separate features.
Comment worth adding to
is_registered_option(). It consults only the currentcommand's options
(
ArgParser.cc#L527),which is correct because
append_option_data()sweeps the remaining vector for theparent'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_ctltoday is on a leaf command (reload,invoke), so nothing reaches it — but the next person to add one will want to know.