From: "Lidong Yan via GitGitGadget" <gitgitgadget@gmail.com>
To: git@vger.kernel.org
Cc: Patrick Steinhardt <ps@pks.im>,
Lidong Yan <502024330056@smail.nju.edu.cn>,
Lidong Yan <502024330056@smail.nju.edu.cn>
Subject: [PATCH v2] parse-options: fix xstrdup leak in parse_options_step parse-options:984
Date: Wed, 07 May 2025 13:24:53 +0000 [thread overview]
Message-ID: <pull.1954.v2.git.git.1746624294017.gitgitgadget@gmail.com> (raw)
In-Reply-To: <pull.1954.git.git.1746585203.gitgitgadget@gmail.com>
From: Lidong Yan <502024330056@smail.nju.edu.cn>
Most Git commands use parse_options to parse options, but parse_options
causes a memory leak when it encounters unknown short options. The leak
occurs at line 984 in parse-options.c, where git allocates some spaces
to store the unknown short options and will never release those spaces.
To address this issue, users can be allowed to provide a custom function
to allocate memory for unknown options. For example, users can use
strvec_push to allocate memory for unknown options and later call
strvec_clear at the end of the Git command to release the memory, thereby
avoiding the memory leak.
This commit allows users to provide a custom allocation function for
unknown options via the strdup_fn field in the last struct option. For
convenience, this commit also introduces a new macro, OPT_UNKNOWN, which
behaves like OPT_END but takes an additional argument for the allocation
function. parse_options could get the custom allocation function in struct
parse_opt_ctx_t by using set_strdup_fn. A simple example to use
OPT_UNKNOWN is put into t/helper/test-free-unknown-options.c
Signed-off-by: Lidong Yan <502024330056@smail.nju.edu.cn>
---
fix xstrdup leak in parse_short_opt
Pass a user defined strdup-like function in parse_opt_ctx to avoid
memory leak.
Published-As: https://github.com/gitgitgadget/git/releases/tag/pr-git-1954%2Fbrandb97%2Ffix-parse-option-leak-v2
Fetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-git-1954/brandb97/fix-parse-option-leak-v2
Pull-Request: https://github.com/git/git/pull/1954
Range-diff vs v1:
1: b34445d166c ! 1: e7b4465b83e fix xstrdup leak in parse_short_opt
@@ Metadata
Author: Lidong Yan <502024330056@smail.nju.edu.cn>
## Commit message ##
- fix xstrdup leak in parse_short_opt
+ parse-options: fix xstrdup leak in parse_options_step parse-options:984
+
+ Most Git commands use parse_options to parse options, but parse_options
+ causes a memory leak when it encounters unknown short options. The leak
+ occurs at line 984 in parse-options.c, where git allocates some spaces
+ to store the unknown short options and will never release those spaces.
+
+ To address this issue, users can be allowed to provide a custom function
+ to allocate memory for unknown options. For example, users can use
+ strvec_push to allocate memory for unknown options and later call
+ strvec_clear at the end of the Git command to release the memory, thereby
+ avoiding the memory leak.
+
+ This commit allows users to provide a custom allocation function for
+ unknown options via the strdup_fn field in the last struct option. For
+ convenience, this commit also introduces a new macro, OPT_UNKNOWN, which
+ behaves like OPT_END but takes an additional argument for the allocation
+ function. parse_options could get the custom allocation function in struct
+ parse_opt_ctx_t by using set_strdup_fn. A simple example to use
+ OPT_UNKNOWN is put into t/helper/test-free-unknown-options.c
Signed-off-by: Lidong Yan <502024330056@smail.nju.edu.cn>
@@ parse-options.c: 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) {
++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) {
@@ parse-options.c: enum parse_opt_result parse_options_step(struct parse_opt_ctx_t
*/
- 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);
++ ctx->argv[0] =
++ ctx->strdup_fn(ctx->unknown_opts,
++ ctx->opt -
++ 1);
+ } else {
-+ ctx->argv[0] = xstrdup(ctx->opt - 1);
++ ctx->argv[0] =
++ xstrdup(ctx->opt - 1);
+ }
*(char *)ctx->argv[0] = '-';
goto unknown;
@@ parse-options.h: 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), \
-+}
++#define OPT_UNKNOWN(v, fn) \
++ { \
++ .type = OPTION_END, \
++ .value = (v), \
++ .strdup_fn = (fn), \
++ }
+
/*
* parse_options() will filter out the processed options and leave the
@@ parse-options.h: 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;
};
@@ t/helper/test-free-unknown-options.c (new)
+#include "parse-options.h"
+#include "setup.h"
+#include "strvec.h"
++#include "test-tool.h"
+
+static const char *const free_unknown_options_usage[] = {
-+ "test-tool free-unknown-options",
-+ NULL
++ "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();
++static char *strvec_push_wrapper(void *value, const char *str)
++{
++ struct strvec *sv = value;
++ return (char *)strvec_push(sv, str);
++}
+
-+ bool a, b;
++int cmd__free_unknown_options(int argc, const char **argv)
++{
++ struct strvec *unknown_opts = xmalloc(sizeof(struct strvec));
++ const char *prefix = setup_git_directory();
++ int a = 0, b = 0;
++ size_t i;
+ 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),
++ OPT_BOOL('b', "test-b", &b, N_("option b, only for test use")),
++ OPT_UNKNOWN(unknown_opts, strvec_push_wrapper),
+ };
+
-+ parse_options(argc, argv, prefix, options,
-+ free_unknown_options_usage, PARSE_OPT_KEEP_UNKNOWN_OPT);
++ strvec_init(unknown_opts);
++ 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");
++ 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);
++ 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);
++
++ return 0;
+}
- \ No newline at end of file
## t/helper/test-tool.c ##
@@ t/helper/test-tool.c: 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},
++ { "free-unknown-options", cmd__free_unknown_options },
{ "partial-clone", cmd__partial_clone },
{ "path-utils", cmd__path_utils },
{ "path-walk", cmd__path_walk },
@@ t/t0040-parse-options.sh: 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 &&
++ cat >expect <<-\EOF &&
++ a = true
++ b = true
++ free unknown option: -c
++ free unknown option: -d
++ EOF
+ test_cmp expect output &&
+ test_must_be_empty output.err
+'
2: 7a5e2f29529 < -: ----------- fix: replace bug where int was incorrectly used as bool
3: a9cbca6bed3 < -: ----------- fix: use strvec_push_wrapper to prevent ubsan failure
Makefile | 1 +
parse-options.c | 23 ++++++++++++++-
parse-options.h | 12 ++++++++
t/helper/meson.build | 1 +
t/helper/test-free-unknown-options.c | 42 ++++++++++++++++++++++++++++
t/helper/test-tool.c | 1 +
t/helper/test-tool.h | 1 +
t/t0040-parse-options.sh | 13 +++++++++
8 files changed, 93 insertions(+), 1 deletion(-)
create mode 100644 t/helper/test-free-unknown-options.c
diff --git a/Makefile b/Makefile
index 8a7f1c76543..af8ea677b82 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 a9a39ecaef6..12721b83000 100644
--- a/parse-options.c
+++ b/parse-options.c
@@ -638,6 +638,18 @@ 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 +667,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 +994,15 @@ 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 91c3e3c29b3..57037bd381a 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,13 @@ 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 +506,8 @@ 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 d2cabaa2bcf..476e3278176 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 00000000000..7369dfe379d
--- /dev/null
+++ b/t/helper/test-free-unknown-options.c
@@ -0,0 +1,42 @@
+#include "git-compat-util.h"
+#include "parse-options.h"
+#include "setup.h"
+#include "strvec.h"
+#include "test-tool.h"
+
+static const char *const free_unknown_options_usage[] = {
+ "test-tool free-unknown-options", NULL
+};
+
+static char *strvec_push_wrapper(void *value, const char *str)
+{
+ struct strvec *sv = value;
+ return (char *)strvec_push(sv, str);
+}
+
+int cmd__free_unknown_options(int argc, const char **argv)
+{
+ struct strvec *unknown_opts = xmalloc(sizeof(struct strvec));
+ const char *prefix = setup_git_directory();
+ int a = 0, b = 0;
+ size_t i;
+ 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, strvec_push_wrapper),
+ };
+
+ strvec_init(unknown_opts);
+ 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");
+
+ 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);
+
+ return 0;
+}
diff --git a/t/helper/test-tool.c b/t/helper/test-tool.c
index 50dc4dac4ed..5af5acae1cb 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 6d62a5b53d9..33fa7828b9f 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 ca55ea8228c..df221be5d18 100755
--- a/t/t0040-parse-options.sh
+++ b/t/t0040-parse-options.sh
@@ -822,4 +822,17 @@ test_expect_success 'u16 limits range' '
test_grep "value 65536 for option .u16. not in range \[0,65535\]" err
'
+test_expect_success 'free unknown options' '
+ test-tool free-unknown-options -ac -bd \
+ >output 2>output.err &&
+ cat >expect <<-\EOF &&
+ a = true
+ b = true
+ free unknown option: -c
+ free unknown option: -d
+ EOF
+ test_cmp expect output &&
+ test_must_be_empty output.err
+'
+
test_done
base-commit: 6f84262c44a89851c3ae5a6e4c1a9d06b2068d75
--
gitgitgadget
next prev parent reply other threads:[~2025-05-07 13:24 UTC|newest]
Thread overview: 14+ messages / expand[flat|nested] mbox.gz Atom feed top
2025-05-07 2:33 [PATCH 0/3] fix xstrdup leak in parse_short_opt Lidong Yan via GitGitGadget
2025-05-07 2:33 ` [PATCH 1/3] " Lidong Yan via GitGitGadget
2025-05-07 7:54 ` Patrick Steinhardt
2025-05-07 2:33 ` [PATCH 2/3] fix: replace bug where int was incorrectly used as bool Lidong Yan via GitGitGadget
2025-05-07 7:54 ` Patrick Steinhardt
2025-05-07 2:33 ` [PATCH 3/3] fix: use strvec_push_wrapper to prevent ubsan failure Lidong Yan via GitGitGadget
2025-05-07 7:54 ` Patrick Steinhardt
2025-05-07 12:19 ` lidongyan
2025-05-07 13:24 ` Lidong Yan via GitGitGadget [this message]
2025-05-09 6:19 ` [PATCH v2] parse-options: fix xstrdup leak in parse_options_step parse-options:984 Patrick Steinhardt
2025-05-09 7:35 ` lidongyan
2025-05-09 13:08 ` Phillip Wood
2025-05-09 13:43 ` lidongyan
2025-05-09 14:28 ` [PATCH v3] parse-options: fix memory leak when calling `parse_options` Lidong Yan via GitGitGadget
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=pull.1954.v2.git.git.1746624294017.gitgitgadget@gmail.com \
--to=gitgitgadget@gmail.com \
--cc=502024330056@smail.nju.edu.cn \
--cc=git@vger.kernel.org \
--cc=ps@pks.im \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox;
as well as URLs for NNTP newsgroup(s).