-- Reproducer: tdigest_in() accepts out-of-range numbers, because sscanf() has -- no way to report a range error. -- -- This one does not crash either, it silently accepts input it should reject -- and stores something different from what was written. -- -- tdigest_in() parses the whole value with sscanf() (tdigest.c around lines -- 2646 and 2694) and only checks that the parsed values make sense relative to -- each other. But sscanf() has undefined behaviour when the number does not -- fit the target type (C99 7.21.6.2p10), and glibc's implementation does not -- report it: -- -- %lld saturates: "99999999999999999999" -> INT64_MAX -- "-99999999999999999999" -> INT64_MIN -- %d wraps: "2147483648" -> -2147483648 -- "4294967296" -> 0 -- "4294967306" -> 10 -- "99999999999" -> 1215752191 -- -- The count overflow checks added in d030f1f run on the parsed (already -- mangled) values, so they do not catch any of this. \set VERBOSITY terse -- 1) the counts saturate to INT64_MAX. Both the total and the centroid count -- saturate to the same value, so they still agree with each other and the -- digest is accepted. Note the output does not match the input. SELECT 'flags 1 count 99999999999999999999 compression 10 centroids 1 (1, 99999999999999999999)'::tdigest; ERROR: count of a t-digest is out of range for bigint at character 8 -- 2) worse, the compression is parsed with %d and wraps modulo 2^32. 4294967306 -- is 2^32 + 10, so this is accepted and stored as compression 10 - well inside -- the allowed range, so the range check that follows sees nothing wrong. SELECT 'flags 1 count 3 compression 4294967306 centroids 1 (1, 3)'::tdigest; ERROR: compression of a t-digest is out of range for integer at character 8 -- and 2^33 + 10 gives the same thing SELECT 'flags 1 count 3 compression 8589934602 centroids 1 (1, 3)'::tdigest; ERROR: compression of a t-digest is out of range for integer at character 8 -- this means the text output of tdigest_in() is not the text that was fed to -- it, i.e. the type is not round-trip stable and a dump/restore of such a -- value produces a different digest SELECT 'flags 1 count 3 compression 4294967306 centroids 1 (1, 3)'::tdigest::text = 'flags 1 count 3 compression 4294967306 centroids 1 (1.000000, 3)' AS round_trips; ERROR: compression of a t-digest is out of range for integer at character 8 -- 3) values that happen to wrap to something outside the allowed range are -- still rejected, but by the range check and with a misleading message - the -- parse itself never fails SELECT 'flags 1 count 3 compression 4294967296 centroids 1 (1, 3)'::tdigest; ERROR: compression of a t-digest is out of range for integer at character 8 SELECT 'flags 1 count 3 compression 2147483648 centroids 1 (1, 3)'::tdigest; ERROR: compression of a t-digest is out of range for integer at character 8 -- 4) negative counts saturate to INT64_MIN, and are then caught by the -- "count value for the t-digest must be positive" check SELECT 'flags 1 count -99999999999999999999 compression 10 centroids 1 (1, 3)'::tdigest; ERROR: count of a t-digest is out of range for bigint at character 8 -- subnormal means should be accepted (and treated as 0.0) SELECT 'flags 1 count 2 compression 10 centroids 2 (1e-320, 1) (2e-320, 1)'::tdigest; tdigest -------------------------------------------------------------------------------------------------- flags 1 count 2 compression 10 centroids 2 (9.99988867182683e-321, 1) (1.99997773436537e-320, 1) (1 row)