From cc8d8ea01db997572dd1872967f95dffe4ec40a3 Mon Sep 17 00:00:00 2001 From: Lidong Yan <502024330056@smail.nju.edu.cn> Date: Tue, 6 May 2025 15:44:25 +0800 Subject: [PATCH] fix xstrdup leak in parse_short_opt --- Makefile | 1 + parse-options.c | 17 +++++++++++++- parse-options.h | 12 ++++++++++ t/helper/meson.build | 1 + t/helper/test-free-unknown-options.c | 35 ++++++++++++++++++++++++++++ t/helper/test-tool.c | 1 + t/helper/test-tool.h | 1 + t/t0040-parse-options.sh | 14 +++++++++++ 8 files changed, 81 insertions(+), 1 deletion(-) create mode 100644 t/helper/test-free-unknown-options.c diff --git a/Makefile b/Makefile index 8a7f1c76543fa6..af8ea677b820fb 100644 --- a/Makefile +++ b/Makefile @@ -822,6 +822,7 @@ TEST_BUILTINS_OBJS += test-online-cpus.o TEST_BUILTINS_OBJS += test-pack-mtimes.o TEST_BUILTINS_OBJS += test-parse-options.o TEST_BUILTINS_OBJS += test-parse-pathspec-file.o +TEST_BUILTINS_OBJS += test-free-unknown-options.o TEST_BUILTINS_OBJS += test-partial-clone.o TEST_BUILTINS_OBJS += test-path-utils.o TEST_BUILTINS_OBJS += test-path-walk.o diff --git a/parse-options.c b/parse-options.c index a9a39ecaef6c36..4279dfe4d3138f 100644 --- a/parse-options.c +++ b/parse-options.c @@ -638,6 +638,16 @@ static int has_subcommands(const struct option *options) return 0; } +static void set_strdup_fn(struct parse_opt_ctx_t *ctx, const struct option *options) { + for (; options->type != OPTION_END; options++) + ; + if (options->value && options->strdup_fn) { + ctx->unknown_opts = options->value; + ctx->strdup_fn = options->strdup_fn; + return; + } +} + static void parse_options_start_1(struct parse_opt_ctx_t *ctx, int argc, const char **argv, const char *prefix, const struct option *options, @@ -655,6 +665,7 @@ static void parse_options_start_1(struct parse_opt_ctx_t *ctx, ctx->cpidx = ((flags & PARSE_OPT_KEEP_ARGV0) != 0); ctx->flags = flags; ctx->has_subcommands = has_subcommands(options); + set_strdup_fn(ctx, options); if (!ctx->has_subcommands && (flags & PARSE_OPT_SUBCOMMAND_OPTIONAL)) BUG("Using PARSE_OPT_SUBCOMMAND_OPTIONAL without subcommands"); if (ctx->has_subcommands) { @@ -981,7 +992,11 @@ enum parse_opt_result parse_options_step(struct parse_opt_ctx_t *ctx, * * This is leaky, too bad. */ - ctx->argv[0] = xstrdup(ctx->opt - 1); + if (ctx->unknown_opts && ctx->strdup_fn) { + ctx->argv[0] = ctx->strdup_fn(ctx->unknown_opts, ctx->opt - 1); + } else { + ctx->argv[0] = xstrdup(ctx->opt - 1); + } *(char *)ctx->argv[0] = '-'; goto unknown; case PARSE_OPT_NON_OPTION: diff --git a/parse-options.h b/parse-options.h index 91c3e3c29b3dda..07019db21f630b 100644 --- a/parse-options.h +++ b/parse-options.h @@ -77,6 +77,8 @@ typedef enum parse_opt_result parse_opt_ll_cb(struct parse_opt_ctx_t *ctx, typedef int parse_opt_subcommand_fn(int argc, const char **argv, const char *prefix, struct repository *repo); +typedef char *parse_opt_strdup_fn(void *value, const char *s); + /* * `type`:: * holds the type of the option, you must have an OPTION_END last in your @@ -165,6 +167,7 @@ struct option { parse_opt_ll_cb *ll_callback; intptr_t extra; parse_opt_subcommand_fn *subcommand_fn; + parse_opt_strdup_fn *strdup_fn; }; #define OPT_BIT_F(s, l, v, h, b, f) { \ @@ -388,6 +391,12 @@ static char *parse_options_noop_ignored_value MAYBE_UNUSED; } #define OPT_SUBCOMMAND(l, v, fn) OPT_SUBCOMMAND_F((l), (v), (fn), 0) +#define OPT_UNKNOWN(v, fn) { \ + .type = OPTION_END, \ + .value = (v), \ + .strdup_fn = (fn), \ +} + /* * parse_options() will filter out the processed options and leave the * non-option arguments in argv[]. argv0 is assumed program name and @@ -496,6 +505,9 @@ struct parse_opt_ctx_t { const char *prefix; const char **alias_groups; /* must be in groups of 3 elements! */ struct parse_opt_cmdmode_list *cmdmode_list; + + void *unknown_opts; + parse_opt_strdup_fn *strdup_fn; }; void parse_options_start(struct parse_opt_ctx_t *ctx, diff --git a/t/helper/meson.build b/t/helper/meson.build index d2cabaa2bcfcc9..476e32781761e8 100644 --- a/t/helper/meson.build +++ b/t/helper/meson.build @@ -39,6 +39,7 @@ test_tool_sources = [ 'test-pack-mtimes.c', 'test-parse-options.c', 'test-parse-pathspec-file.c', + 'test-free-unknown-options.c', 'test-partial-clone.c', 'test-path-utils.c', 'test-path-walk.c', diff --git a/t/helper/test-free-unknown-options.c b/t/helper/test-free-unknown-options.c new file mode 100644 index 00000000000000..0be2df37c3c03d --- /dev/null +++ b/t/helper/test-free-unknown-options.c @@ -0,0 +1,35 @@ +#include "git-compat-util.h" +#include "parse-options.h" +#include "setup.h" +#include "strvec.h" + +static const char *const free_unknown_options_usage[] = { + "test-tool free-unknown-options", + NULL +}; + +int cmd__free_unknown_options(int argc, const char **argv) { + struct strvec *unknown_opts = xmalloc(sizeof(struct strvec)); + strvec_init(unknown_opts); + const char *prefix = setup_git_directory(); + + bool a, b; + struct option options[] = { + OPT_BOOL('a', "test-a", &a, N_("option a, only for test use")), + OPT_BOOL('b', "test-b", &b, N_("option b, only for test use")), + OPT_UNKNOWN(unknown_opts, (parse_opt_strdup_fn *)&strvec_push), + }; + + parse_options(argc, argv, prefix, options, + free_unknown_options_usage, PARSE_OPT_KEEP_UNKNOWN_OPT); + + printf("a = %s\n", a? "true": "false"); + printf("b = %s\n", b? "true": "false"); + + int i; + for (i = 0; i < unknown_opts->nr; i++) { + printf("free unknown option: %s\n", unknown_opts->v[i]); + } + strvec_clear(unknown_opts); + free(unknown_opts); +} \ No newline at end of file diff --git a/t/helper/test-tool.c b/t/helper/test-tool.c index 50dc4dac4ed625..79ec4f9cda076a 100644 --- a/t/helper/test-tool.c +++ b/t/helper/test-tool.c @@ -51,6 +51,7 @@ static struct test_cmd cmds[] = { { "parse-options-flags", cmd__parse_options_flags }, { "parse-pathspec-file", cmd__parse_pathspec_file }, { "parse-subcommand", cmd__parse_subcommand }, + { "free-unknown-options", cmd__free_unknown_options}, { "partial-clone", cmd__partial_clone }, { "path-utils", cmd__path_utils }, { "path-walk", cmd__path_walk }, diff --git a/t/helper/test-tool.h b/t/helper/test-tool.h index 6d62a5b53d9596..33fa7828b9f65f 100644 --- a/t/helper/test-tool.h +++ b/t/helper/test-tool.h @@ -44,6 +44,7 @@ int cmd__parse_options(int argc, const char **argv); int cmd__parse_options_flags(int argc, const char **argv); int cmd__parse_pathspec_file(int argc, const char** argv); int cmd__parse_subcommand(int argc, const char **argv); +int cmd__free_unknown_options(int argc, const char **argv); int cmd__partial_clone(int argc, const char **argv); int cmd__path_utils(int argc, const char **argv); int cmd__path_walk(int argc, const char **argv); diff --git a/t/t0040-parse-options.sh b/t/t0040-parse-options.sh index ca55ea8228c378..773f54103fd3f4 100755 --- a/t/t0040-parse-options.sh +++ b/t/t0040-parse-options.sh @@ -822,4 +822,18 @@ test_expect_success 'u16 limits range' ' test_grep "value 65536 for option .u16. not in range \[0,65535\]" err ' +cat >expect <<\EOF +a = true +b = true +free unknown option: -c +free unknown option: -d +EOF + +test_expect_success 'free unknown options' ' + test-tool free-unknown-options -ac -bd \ + >output 2>output.err && + test_cmp expect output && + test_must_be_empty output.err +' + test_done