parse-options: introduce precision handling for `OPTION_INTEGER`
The `OPTION_INTEGER` option type accepts a signed integer. The type of
the underlying integer is a simple `int`, which restricts the range of
values accepted by such options. But there is a catch: because the
caller provides a pointer to the value via the `.value` field, which is
a simple void pointer. This has two consequences:
- There is no check whether the passed value is sufficiently long to
store the entire range of `int`. This can lead to integer wraparound
in the best case and out-of-bounds writes in the worst case.
- Even when a caller knows that they want to store a value larger than
`INT_MAX` they don't have a way to do so.
In practice this doesn't tend to be a huge issue because users typically
don't end up passing huge values to most commands. But the parsing logic
is demonstrably broken, and it is too easy to get the calling convention
wrong.
Improve the situation by introducing a new `precision` field into the
structure. This field gets assigned automatically by `OPT_INTEGER_F()`
and tracks the size of the passed value. Like this it becomes possible
for the caller to pass arbitrarily-sized integers and the underlying
logic knows to handle it correctly by doing range checks. Furthermore,
convert the code to use `strtoimax()` intstead of `strtol()` so that we
can also parse values larger than `LONG_MAX`.
Note that we do not yet assert signedness of the passed variable, which
is another source of bugs. This will be handled in a subsequent commit.
Signed-off-by: Patrick Steinhardt <ps@pks.im>
Signed-off-by: Junio C Hamano <gitster@pobox.com>
Patrick Steinhardt committedApr 17, 2025 at 12:49 UTC09705696f763bac370ac74926bef137eb712c0c8
8 files changed+75-14
builtin/fmt-merge-msg.c
+2
index 240cdb474b..3b6aac2cf7 100644--- a/builtin/fmt-merge-msg.c+++ b/builtin/fmt-merge-msg.c@@ -24,6 +24,7 @@ int cmd_fmt_merge_msg(int argc, .type = OPTION_INTEGER, .long_name = "log", .value = &shortlog_len,+ .precision = sizeof(shortlog_len), .argh = N_("n"), .help = N_("populate log with at most <n> entries from shortlog"), .flags = PARSE_OPT_OPTARG,@@ -33,6 +34,7 @@ int cmd_fmt_merge_msg(int argc, .type = OPTION_INTEGER, .long_name = "summary", .value = &shortlog_len,+ .precision = sizeof(shortlog_len), .argh = N_("n"), .help = N_("alias for --log (deprecated)"), .flags = PARSE_OPT_OPTARG | PARSE_OPT_HIDDEN,
index dab37019d2..b549d8c3f5 100644--- a/builtin/show-branch.c+++ b/builtin/show-branch.c@@ -671,6 +671,7 @@ int cmd_show_branch(int ac, .type = OPTION_INTEGER, .long_name = "more", .value = &extra,+ .precision = sizeof(extra), .argh = N_("n"), .help = N_("show <n> more commits after the common ancestor"), .flags = PARSE_OPT_OPTARG,
builtin/tag.c
+1
index b266f12bb4..7597d93c71 100644--- a/builtin/tag.c+++ b/builtin/tag.c@@ -483,6 +483,7 @@ int cmd_tag(int argc, .type = OPTION_INTEGER, .short_name = 'n', .value = &filter.lines,+ .precision = sizeof(filter.lines), .argh = N_("n"), .help = N_("print <n> lines of each tag message"), .flags = PARSE_OPT_OPTARG,
parse-options.c
+39-13
index d23e587e98..768718a397 100644--- a/parse-options.c+++ b/parse-options.c@@ -172,25 +172,51 @@ static enum parse_opt_result do_get_value(struct parse_opt_ctx_t *p, return (*opt->ll_callback)(p, opt, p_arg, p_unset); } case OPTION_INTEGER:+ {+ intmax_t upper_bound = INTMAX_MAX >> (bitsizeof(intmax_t) - CHAR_BIT * opt->precision);+ intmax_t lower_bound = -upper_bound - 1;+ intmax_t value;+ if (unset) {- *(int *)opt->value = 0;- return 0;- }- if (opt->flags & PARSE_OPT_OPTARG && !p->opt) {- *(int *)opt->value = opt->defval;- return 0;- }- if (get_arg(p, opt, flags, &arg))+ value = 0;+ } else if (opt->flags & PARSE_OPT_OPTARG && !p->opt) {+ value = opt->defval;+ } else if (get_arg(p, opt, flags, &arg)) { return -1;- if (!*arg)+ } else if (!*arg) { return error(_("%s expects a numerical value"), optname(opt, flags));- if (!git_parse_int(arg, opt->value))- return error(_("%s expects an integer value"- " with an optional k/m/g suffix"),+ } else if (!git_parse_signed(arg, &value, upper_bound)) {+ if (errno == ERANGE)+ return error(_("value %s for %s not in range [%"PRIdMAX",%"PRIdMAX"]"),+ arg, optname(opt, flags), lower_bound, upper_bound);++ return error(_("%s expects an integer value with an optional k/m/g suffix"), optname(opt, flags));- return 0;+ }++ if (value < lower_bound)+ return error(_("value %s for %s not in range [%"PRIdMAX",%"PRIdMAX"]"),+ arg, optname(opt, flags), lower_bound, upper_bound);+ switch (opt->precision) {+ case 1:+ *(int8_t *)opt->value = value;+ return 0;+ case 2:+ *(int16_t *)opt->value = value;+ return 0;+ case 4:+ *(int32_t *)opt->value = value;+ return 0;+ case 8:+ *(int64_t *)opt->value = value;+ return 0;+ default:+ BUG("invalid precision for option %s",+ optname(opt, flags));+ }+ } case OPTION_UNSIGNED: if (unset) { *(unsigned long *)opt->value = 0;
parse-options.h
+6
index 14e4df1ee2..4c430c7273 100644--- a/parse-options.h+++ b/parse-options.h@@ -92,6 +92,10 @@ typedef int parse_opt_subcommand_fn(int argc, const char **argv, * `value`:: * stores pointers to the values to be filled. *+ * `precision`::+ * precision of the integer pointed to by `value` in number of bytes. Should+ * typically be its `sizeof()`.+ * * `argh`:: * token to explain the kind of argument this option wants. Does not * begin in capital letter, and does not end with a full stop.@@ -151,6 +155,7 @@ struct option { int short_name; const char *long_name; void *value;+ size_t precision; const char *argh; const char *help;@@ -214,6 +219,7 @@ struct option { .short_name = (s), \ .long_name = (l), \ .value = (v), \+ .precision = sizeof(*v), \ .argh = N_("n"), \ .help = (h), \ .flags = (f), \