Bug #121126 Prepared Cast Unsigned Parameter Truncation
Submitted: 19 Aug 11:43
Reporter: xingwang Verdant Email Updates:
Status: Open Impact on me:
None 
Category:MySQL Server Severity:S3 (Non-critical)
Version:8.4.10 OS:Any
Assigned to: CPU Architecture:Any

[19 Aug 11:43] xingwang Verdant
Description:
# MySQL prepared CAST-AS-UNSIGNED truncation

Date: 2026-08-18
Status: exact MySQL 8.4.10 Debug+ASan and official Release reproduced; no
public causal match found, pending vendor adjudication.

## Root cause

`Item_typecast_unsigned::resolve_type()` propagates
`MYSQL_TYPE_LONGLONG` through an overload whose `Type_properties` constructor
defaults numeric signedness to false. The trailing booleans in that call are
`pin` and `inherit`, not signedness. Consequently, a placeholder that the SQL
manual says derives its type from `CAST(... AS UNSIGNED)` is resolved as a
signed BIGINT.

When EXECUTE supplies a string above `LLONG_MAX`, `Item_param::convert_value()`
cannot accept it through the signed integer fast path and stores DECIMAL.
`Item_param::val_int()` then calls `my_decimal2int()` with the resolved signed
flag and saturates to `9223372036854775807`. The outer unsigned cast sees an
already-numeric argument and does not reparse the original string as unsigned.
Direct string literals do take the unsigned parser, so they return the entire
`0..ULLONG_MAX` domain correctly.

## Boundary and effect

- `9223372036854775807`: direct and prepared results agree.
- `9223372036854775808`: direct returns the input; prepared returns
  `9223372036854775807` with warning 1292.
- `18446744073709551615`: direct returns the input; prepared returns the same
  wrong signed maximum on both executions.
- Non-strict prepared CTAS creates `BIGINT UNSIGNED` but persists the wrong
  signed-maximum value. `CHECK TABLE` remains OK.

Both exact Debug+ASan and official Release reproduce all observations 2/2.
All servers remain live, no run is OOM-killed, and ASan emits no report.

How to repeat:
SET sql_mode='STRICT_ALL_TABLES';
SELECT 'direct-control' AS phase,CAST('9223372036854775807' AS UNSIGNED) AS v;
SELECT 'direct-edge' AS phase,CAST('9223372036854775808' AS UNSIGNED) AS v;
SELECT 'direct-max' AS phase,CAST('18446744073709551615' AS UNSIGNED) AS v;

PREPARE s FROM 'SELECT CAST(? AS UNSIGNED) AS v';
SET @p='9223372036854775807';
SELECT 'prepared-control' AS phase; EXECUTE s USING @p;
SET @p='9223372036854775808';
SELECT 'prepared-edge' AS phase; EXECUTE s USING @p;
SET @p='18446744073709551615';
SELECT 'prepared-max' AS phase; EXECUTE s USING @p;
EXECUTE s USING @p;
DEALLOCATE PREPARE s;

DROP DATABASE IF EXISTS h97f;
CREATE DATABASE h97f;
USE h97f;
SET sql_mode='';
PREPARE c FROM 'CREATE TABLE persisted AS SELECT CAST(? AS UNSIGNED) AS v';
SET @p='18446744073709551615';
EXECUTE c USING @p;
DEALLOCATE PREPARE c;
SHOW CREATE TABLE persisted;
SELECT 'persisted-max' AS phase,v FROM persisted;
CHECK TABLE persisted;
DROP DATABASE h97f;