Git fork

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>

authored by

Patrick Steinhardt and committed by
Junio C Hamano
09705696 785c17df

+75 -14
+2
builtin/fmt-merge-msg.c
··· 24 24 .type = OPTION_INTEGER, 25 25 .long_name = "log", 26 26 .value = &shortlog_len, 27 + .precision = sizeof(shortlog_len), 27 28 .argh = N_("n"), 28 29 .help = N_("populate log with at most <n> entries from shortlog"), 29 30 .flags = PARSE_OPT_OPTARG, ··· 33 34 .type = OPTION_INTEGER, 34 35 .long_name = "summary", 35 36 .value = &shortlog_len, 37 + .precision = sizeof(shortlog_len), 36 38 .argh = N_("n"), 37 39 .help = N_("alias for --log (deprecated)"), 38 40 .flags = PARSE_OPT_OPTARG | PARSE_OPT_HIDDEN,
+1
builtin/merge.c
··· 254 254 .type = OPTION_INTEGER, 255 255 .long_name = "log", 256 256 .value = &shortlog_len, 257 + .precision = sizeof(shortlog_len), 257 258 .argh = N_("n"), 258 259 .help = N_("add (at most <n>) entries from shortlog to merge commit message"), 259 260 .flags = PARSE_OPT_OPTARG,
+1
builtin/show-branch.c
··· 671 671 .type = OPTION_INTEGER, 672 672 .long_name = "more", 673 673 .value = &extra, 674 + .precision = sizeof(extra), 674 675 .argh = N_("n"), 675 676 .help = N_("show <n> more commits after the common ancestor"), 676 677 .flags = PARSE_OPT_OPTARG,
+1
builtin/tag.c
··· 483 483 .type = OPTION_INTEGER, 484 484 .short_name = 'n', 485 485 .value = &filter.lines, 486 + .precision = sizeof(filter.lines), 486 487 .argh = N_("n"), 487 488 .help = N_("print <n> lines of each tag message"), 488 489 .flags = PARSE_OPT_OPTARG,
+39 -13
parse-options.c
··· 172 172 return (*opt->ll_callback)(p, opt, p_arg, p_unset); 173 173 } 174 174 case OPTION_INTEGER: 175 + { 176 + intmax_t upper_bound = INTMAX_MAX >> (bitsizeof(intmax_t) - CHAR_BIT * opt->precision); 177 + intmax_t lower_bound = -upper_bound - 1; 178 + intmax_t value; 179 + 175 180 if (unset) { 176 - *(int *)opt->value = 0; 177 - return 0; 178 - } 179 - if (opt->flags & PARSE_OPT_OPTARG && !p->opt) { 180 - *(int *)opt->value = opt->defval; 181 - return 0; 182 - } 183 - if (get_arg(p, opt, flags, &arg)) 181 + value = 0; 182 + } else if (opt->flags & PARSE_OPT_OPTARG && !p->opt) { 183 + value = opt->defval; 184 + } else if (get_arg(p, opt, flags, &arg)) { 184 185 return -1; 185 - if (!*arg) 186 + } else if (!*arg) { 186 187 return error(_("%s expects a numerical value"), 187 188 optname(opt, flags)); 188 - if (!git_parse_int(arg, opt->value)) 189 - return error(_("%s expects an integer value" 190 - " with an optional k/m/g suffix"), 189 + } else if (!git_parse_signed(arg, &value, upper_bound)) { 190 + if (errno == ERANGE) 191 + return error(_("value %s for %s not in range [%"PRIdMAX",%"PRIdMAX"]"), 192 + arg, optname(opt, flags), lower_bound, upper_bound); 193 + 194 + return error(_("%s expects an integer value with an optional k/m/g suffix"), 191 195 optname(opt, flags)); 192 - return 0; 196 + } 197 + 198 + if (value < lower_bound) 199 + return error(_("value %s for %s not in range [%"PRIdMAX",%"PRIdMAX"]"), 200 + arg, optname(opt, flags), lower_bound, upper_bound); 193 201 202 + switch (opt->precision) { 203 + case 1: 204 + *(int8_t *)opt->value = value; 205 + return 0; 206 + case 2: 207 + *(int16_t *)opt->value = value; 208 + return 0; 209 + case 4: 210 + *(int32_t *)opt->value = value; 211 + return 0; 212 + case 8: 213 + *(int64_t *)opt->value = value; 214 + return 0; 215 + default: 216 + BUG("invalid precision for option %s", 217 + optname(opt, flags)); 218 + } 219 + } 194 220 case OPTION_UNSIGNED: 195 221 if (unset) { 196 222 *(unsigned long *)opt->value = 0;
+6
parse-options.h
··· 92 92 * `value`:: 93 93 * stores pointers to the values to be filled. 94 94 * 95 + * `precision`:: 96 + * precision of the integer pointed to by `value` in number of bytes. Should 97 + * typically be its `sizeof()`. 98 + * 95 99 * `argh`:: 96 100 * token to explain the kind of argument this option wants. Does not 97 101 * begin in capital letter, and does not end with a full stop. ··· 151 155 int short_name; 152 156 const char *long_name; 153 157 void *value; 158 + size_t precision; 154 159 const char *argh; 155 160 const char *help; 156 161 ··· 214 219 .short_name = (s), \ 215 220 .long_name = (l), \ 216 221 .value = (v), \ 222 + .precision = sizeof(*v), \ 217 223 .argh = N_("n"), \ 218 224 .help = (h), \ 219 225 .flags = (f), \
+3
t/helper/test-parse-options.c
··· 120 120 }; 121 121 struct string_list expect = STRING_LIST_INIT_NODUP; 122 122 struct string_list list = STRING_LIST_INIT_NODUP; 123 + int16_t i16 = 0; 123 124 124 125 struct option options[] = { 125 126 OPT_BOOL(0, "yes", &boolean, "get a boolean"), ··· 139 140 OPT_NEGBIT(0, "neg-or4", &boolean, "same as --no-or4", 4), 140 141 OPT_GROUP(""), 141 142 OPT_INTEGER('i', "integer", &integer, "get a integer"), 143 + OPT_INTEGER(0, "i16", &i16, "get a 16 bit integer"), 142 144 OPT_INTEGER('j', NULL, &integer, "get a integer, too"), 143 145 OPT_UNSIGNED('u', "unsigned", &unsigned_integer, "get an unsigned integer"), 144 146 OPT_SET_INT(0, "set23", &integer, "set integer to 23", 23), ··· 210 212 } 211 213 show(&expect, &ret, "boolean: %d", boolean); 212 214 show(&expect, &ret, "integer: %d", integer); 215 + show(&expect, &ret, "i16: %"PRIdMAX, (intmax_t) i16); 213 216 show(&expect, &ret, "unsigned: %lu", unsigned_integer); 214 217 show(&expect, &ret, "timestamp: %"PRItime, timestamp); 215 218 show(&expect, &ret, "string: %s", string ? string : "(not set)");
+22 -1
t/t0040-parse-options.sh
··· 22 22 23 23 -i, --[no-]integer <n> 24 24 get a integer 25 + --[no-]i16 <n> get a 16 bit integer 25 26 -j <n> get a integer, too 26 27 -u, --unsigned <n> get an unsigned integer 27 28 --[no-]set23 set integer to 23 ··· 138 139 cat >expect <<\EOF 139 140 boolean: 2 140 141 integer: 1729 142 + i16: 0 141 143 unsigned: 16384 142 144 timestamp: 0 143 145 string: 123 ··· 158 160 cat >expect <<\EOF 159 161 boolean: 2 160 162 integer: 1729 163 + i16: 9000 161 164 unsigned: 16384 162 165 timestamp: 0 163 166 string: 321 ··· 169 172 EOF 170 173 171 174 test_expect_success 'long options' ' 172 - test-tool parse-options --boolean --integer 1729 --unsigned 16k \ 175 + test-tool parse-options --boolean --integer 1729 --i16 9000 --unsigned 16k \ 173 176 --boolean --string2=321 --verbose --verbose --no-dry-run \ 174 177 --abbrev=10 --file fi.le --obsolete \ 175 178 >output 2>output.err && ··· 181 184 cat >expect <<-EOF && 182 185 boolean: 0 183 186 integer: 0 187 + i16: 0 184 188 unsigned: 0 185 189 timestamp: 0 186 190 string: (not set) ··· 255 259 cat >expect <<\EOF 256 260 boolean: 1 257 261 integer: 13 262 + i16: 0 258 263 unsigned: 0 259 264 timestamp: 0 260 265 string: 123 ··· 278 283 cat >expect <<\EOF 279 284 boolean: 0 280 285 integer: 2 286 + i16: 0 281 287 unsigned: 0 282 288 timestamp: 0 283 289 string: (not set) ··· 345 351 Callback: "four", 0 346 352 boolean: 5 347 353 integer: 4 354 + i16: 0 348 355 unsigned: 0 349 356 timestamp: 0 350 357 string: (not set) ··· 370 377 cat >expect <<\EOF 371 378 boolean: 1 372 379 integer: 23 380 + i16: 0 373 381 unsigned: 0 374 382 timestamp: 0 375 383 string: (not set) ··· 449 457 cat >expect <<\EOF 450 458 boolean: 0 451 459 integer: 0 460 + i16: 0 452 461 unsigned: 0 453 462 timestamp: 0 454 463 string: (not set) ··· 783 792 test_must_fail test-tool parse-options --unsigned m >out 2>err && 784 793 grep "non-negative integer" err && 785 794 test_must_be_empty out 795 + ' 796 + 797 + test_expect_success 'i16 limits range' ' 798 + test-tool parse-options --i16 32767 >out && 799 + test_grep "i16: 32767" out && 800 + test_must_fail test-tool parse-options --i16 32768 2>err && 801 + test_grep "value 32768 for option .i16. not in range \[-32768,32767\]" err && 802 + 803 + test-tool parse-options --i16 -32768 >out && 804 + test_grep "i16: -32768" out && 805 + test_must_fail test-tool parse-options --i16 -32769 2>err && 806 + test_grep "value -32769 for option .i16. not in range \[-32768,32767\]" err 786 807 ' 787 808 788 809 test_done