Re: Incorrect usage of strtol, atoi for non-numeric junk inputs

From: Michael Paquier <michael(at)paquier(dot)xyz>
To: Kyotaro Horiguchi <horikyota(dot)ntt(at)gmail(dot)com>
Cc: bharath(dot)rupireddyforpostgres(at)gmail(dot)com, alvherre(at)alvh(dot)no-ip(dot)org, pgsql-hackers(at)lists(dot)postgresql(dot)org
Subject: Re: Incorrect usage of strtol, atoi for non-numeric junk inputs
Date: 2021-07-13 00:28:30
Message-ID: YOzeLiDeSrpZ1UKG@paquier.xyz
Views: Raw Message | Whole Thread | Download mbox | Resend email
Thread:
Lists: pgsql-hackers

On Fri, Jul 09, 2021 at 04:50:28PM +0900, Kyotaro Horiguchi wrote:
> At Fri, 9 Jul 2021 10:29:07 +0900, Michael Paquier <michael(at)paquier(dot)xyz> wrote in
>> Er, wait. We've actually allowed negative values for pg_ctl
>> --timeout or the subcommand kill!?
>
> --timeout accepts values less than 1, which values cause the command
> ends with the timeout error before checking for the ending state. Not
> destructive but useless as a behavior. We have two choices here. One
> is simply inhibiting zero or negative timeouts, and another is
> allowing zero as timeout and giving it the same meaning to
> --no-wait. The former is strictly right behavior but the latter is
> casual and convenient. I took the former way in this version.

Yeah, that's not useful.

>> This one in pg_upgrade is incomplete. Perhaps the missing comment
>> should tell that negative job values are checked later on?
>
> Zero or negative job numbers mean non-parallel mode and don't lead to
> an error. If we don't need to preserve that behavior (I personally
> don't think it is ether useful and/or breaks someone's existing
> usage.), it is sensible to do range-check here.

Hmm. It would be good to preserve some compatibility here, but I can
see more benefits in being consistent between all the tools, and make
people aware that such commands are not generated more carefully.

> case 'j':
> - opts.jobs = atoi(optarg);
> - if (opts.jobs < 1)
> + errno = 0;
> + opts.jobs = strtoint(optarg, &endptr, 10);
> + if (*endptr || errno == ERANGE || opts.jobs < 1)
> {
> pg_log_error("number of parallel jobs must be at least 1");
> exit(1);

specifying a value that triggers ERANGE could be confusing for values
higher than INT_MAX with pg_amcheck --jobs:
$ pg_amcheck --jobs=4000000000
pg_amcheck: error: number of parallel jobs must be at least 1
I think that this should be reworded as "invalid number of parallel
jobs \"$s\"", or "number of parallel jobs must be in range %d..%d".
Perhaps we could have a combination of both? Using the first message
is useful with junk, non-numeric values or trailing characters. The
second is useful to make users aware that the value is numeric, but
not quite right.

> --- a/src/bin/pg_checksums/pg_checksums.c
> case 'f':
> - if (atoi(optarg) == 0)
> + errno = 0;
> + if (strtoint(optarg, &endptr, 10) == 0
> + || *endptr || errno == ERANGE)
> {
> pg_log_error("invalid filenode specification, must be numeric: %s", optarg);
> exit(1);

The confusion is equal here with pg_checksums -f:
$ ./pg_checksums --f 4000000000
pg_checksums: error: invalid filenode specification, must be numeric: 400000000
We could say "invalid file specification: \"%s\"". Another idea to be
crystal-clear about the range requirements is to use that:
"file specification must be in range %d..%d"

> @@ -587,8 +602,10 @@ main(int argc, char **argv)
>
> case 8:
> have_extra_float_digits = true;
> - extra_float_digits = atoi(optarg);
> - if (extra_float_digits < -15 || extra_float_digits > 3)
> + errno = 0;
> + extra_float_digits = strtoint(optarg, &endptr, 10);
> + if (*endptr || errno == ERANGE ||
> + extra_float_digits < -15 || extra_float_digits > 3)
> {
> pg_log_error("extra_float_digits must be in range -15..3");
> exit_nicely(1);

Should we take this occasion to reduce the burden of translators and
reword that as "%s must be in range %d..%d"? That could be a separate
patch.

> case 'p':
> - if ((old_cluster.port = atoi(optarg)) <= 0)
> - pg_fatal("invalid old port number\n");
> + errno = 0;
> + if ((old_cluster.port = strtoint(optarg, &endptr, 10)) <= 0 ||
> + *endptr || errno == ERANGE)
> + pg_fatal("invalid old port number %s\n", optarg);
> break;

You may want to use columns here, or specify the port range:
"invalid old port number: %s" or "old port number must be in range
%d..%d".

> case 'P':
> - if ((new_cluster.port = atoi(optarg)) <= 0)
> - pg_fatal("invalid new port number\n");
> + errno = 0;
> + if ((new_cluster.port = strtoint(optarg, &endptr, 10)) <= 0 ||
> + *endptr || errno == ERANGE)
> + pg_fatal("invalid new port number %s\n", optarg);
> break;

Ditto.

> + if (*endptr || errno == ERANGE || concurrentCons <= 0)
> {
> - pg_log_error("number of parallel jobs must be at least 1");
> + pg_log_error("number of parallel jobs must be at least 1: %s", optarg);
> exit(1);
> }

This one is also confusing with optorg > INT_MAX.

> + concurrentCons = strtoint(optarg, &endptr, 10);
> + if (*endptr || errno == ERANGE || concurrentCons <= 0)
> {
> pg_log_error("number of parallel jobs must be at least 1");
> exit(1);
> }
> break;

And ditto for all the ones of vacuumdb.
--
Michael

In response to

Responses

Browse pgsql-hackers by date

  From Date Subject
Next Message Michael Paquier 2021-07-13 00:47:15 Re: [PATCH] Pull general SASL framework out of SCRAM
Previous Message Alvaro Herrera 2021-07-13 00:17:55 Re: Fix comments of heap_prune_chain()