Bug #121510 CONV() truncates from_base and to_base to 32 bits: CONV('11', 4294967298, 10) = '3' instead of NULL
Submitted: 15 Oct 5:01 Modified: 20 Oct 4:27
Reporter: Ke Han Email Updates:
Status: Verified Impact on me:
None 
Category:MySQL Server: Data Types Severity:S3 (Non-critical)
Version:8.0.46, 8.4.11, 9.7.2, 26.07 OS:Any
Assigned to: CPU Architecture:Any

[15 Oct 5:01] Ke Han
Description:
The manual says of CONV(N, from_base, to_base): "The minimum base is 2 and
the maximum base is 36", and CONV() returns NULL for a base outside that
range, e.g. 37.  But both bases are first assigned to an int, so the range
check sees only the low 32 bits:

  - from_base or to_base = 4294967298 (2^32 + 2) is accepted as base 2;
  - -4294967294 is accepted as base 2 too;
  - to_base = 4294967286 (2^32 - 10) becomes -10, i.e. a *negative* base,
    which CONV() treats as "signed output": CONV('-1', 10, 4294967286)
    returns '-1', the result for to_base -10, where to_base 10 gives
    18446744073709551615;
  - a BIGINT column holding 4294967312 (2^32 + 16) is used as base 16.

No warning is given.  A value that does not fit in BIGINT at all
(18446744073709551618) does return NULL with a warning, so it is only the
range 2^31 .. 2^63-1 (and its negative) that is silently narrowed.

Cause: sql/item_strfunc.cc, Item_func_conv::val_str(), trunk 3b99be40:

    const int from_base = args[1]->val_int();
    ...
    const int to_base = args[2]->val_int();
    ...
    if (from_base == INT_MIN || to_base == INT_MIN || abs(to_base) > 36 ||
        abs(to_base) < 2 || abs(from_base) > 36 || abs(from_base) < 2 ||

val_int() returns a longlong; the implicit conversion to int keeps the low
32 bits, and the range check is applied to that.

How to repeat:
SELECT CONV('11', 2, 10) AS b2, CONV('11', 4294967298, 10) AS b2p32_2,
       CONV('11', -4294967294, 10) AS bm2p32_2, CONV('11', 37, 10) AS b37;
+------+---------+----------+------+
| b2   | b2p32_2 | bm2p32_2 | b37  |
+------+---------+----------+------+
| 3    | 3       | 3        | NULL |
+------+---------+----------+------+

SELECT CONV('3', 10, 2) AS t2, CONV('3', 10, 4294967298) AS t2p32_2,
       CONV('3', 10, 37) AS t37;
+------+---------+------+
| t2   | t2p32_2 | t37  |
+------+---------+------+
| 11   | 11      | NULL |
+------+---------+------+

SELECT CONV('-1', 10, 10) AS unsigned_out, CONV('-1', 10, -10) AS signed_out,
       CONV('-1', 10, 4294967286) AS t2p32_m10;
+----------------------+------------+-----------+
| unsigned_out         | signed_out | t2p32_m10 |
+----------------------+------------+-----------+
| 18446744073709551615 | -1         | -1        |
+----------------------+------------+-----------+

CREATE TABLE t015 (b BIGINT);
INSERT INTO t015 VALUES (16), (4294967312);
SELECT b, CONV('ff', b, 10) AS v FROM t015 ORDER BY b;
+------------+------+
| b          | v    |
+------------+------+
|         16 | 255  |
| 4294967312 | 255  |
+------------+------+

Expected: NULL for every base whose absolute value is outside 2..36, as for
37.  For 4294967298, -4294967294, 4294967286 and 4294967312 the result
should be NULL.

Suggested fix:
Read both bases as longlong and range-check them before narrowing:

    const longlong from_base = args[1]->val_int();
    const longlong to_base = args[2]->val_int();
    if (from_base < -36 || from_base > 36 || (from_base > -2 && from_base < 2) ||
        to_base < -36 || to_base > 36 || (to_base > -2 && to_base < 2) || ...)

(an unsigned argument above LLONG_MAX also needs args[n]->unsigned_flag, or
it would wrap into the negative range).
[20 Oct 4:27] Chaithra Marsur Gopala Reddy
Hi Ke Han,

Thank you for the test case. Verified as described.