From 558cc3d3ad3704ba84915f75124e0cd253ad89bb Mon Sep 17 00:00:00 2001 From: Ojus Chugh Date: Wed, 21 Oct 2026 00:27:15 +0530 Subject: [PATCH 1/2] Fix GH-16910: Fractional seconds truncated in prepared statements --- NEWS | 2 ++ ext/mysqli/tests/gh16910.phpt | 42 ++++++++++++++++++++++++++++++++++ ext/mysqlnd/mysqlnd_ps_codec.c | 18 ++++++++++----- 3 files changed, 56 insertions(+), 6 deletions(-) create mode 100644 ext/mysqli/tests/gh16910.phpt diff --git a/NEWS b/NEWS index 48afd654b2ef..b1252555b41f 100644 --- a/NEWS +++ b/NEWS @@ -58,6 +58,8 @@ PHP NEWS - MySQLnd: . Fixed bug GH-24293 (Segfault on connect with an empty password to a MariaDB PAM account). (lazerg) + . Fixed bug GH-16910 (Fractional seconds truncated in prepared statements + if decimals is AUTO_SEC_PART_DIGITS). (Ojus Chugh) - Opcache: . Fixed bug GH-20890 (Segfault in zval_undefined_cv with non-simple property diff --git a/ext/mysqli/tests/gh16910.phpt b/ext/mysqli/tests/gh16910.phpt new file mode 100644 index 000000000000..910f586d2f39 --- /dev/null +++ b/ext/mysqli/tests/gh16910.phpt @@ -0,0 +1,42 @@ +--TEST-- +GH-16910 (Fractional seconds truncated in prepared statements if decimals is AUTO_SEC_PART_DIGITS) +--EXTENSIONS-- +mysqli +--SKIPIF-- +prepare("SELECT FROM_UNIXTIME('1992.1')"); +if (!$stmt || $stmt->result_metadata()->fetch_field_direct(0)->decimals !== 39) + die("SKIP Server does not report AUTO_SEC_PART_DIGITS for FROM_UNIXTIME()"); +?> +--FILE-- +query("SET time_zone = '+00:00'"); + +$sql = "SELECT FROM_UNIXTIME('1992.1'), FROM_UNIXTIME('1992.0')"; +var_dump($link->query($sql)->fetch_row()); + +$stmt = $link->prepare($sql); +$stmt->execute(); +var_dump($stmt->get_result()->fetch_row()); +?> +--EXPECT-- +array(2) { + [0]=> + string(26) "1970-01-01 00:33:12.100000" + [1]=> + string(19) "1970-01-01 00:33:12" +} +array(2) { + [0]=> + string(26) "1970-01-01 00:33:12.100000" + [1]=> + string(19) "1970-01-01 00:33:12" +} diff --git a/ext/mysqlnd/mysqlnd_ps_codec.c b/ext/mysqlnd/mysqlnd_ps_codec.c index 787fe3b245cd..66f9d1a71b4d 100644 --- a/ext/mysqlnd/mysqlnd_ps_codec.c +++ b/ext/mysqlnd/mysqlnd_ps_codec.c @@ -264,6 +264,10 @@ ps_fetch_double(zval * zv, const MYSQLND_FIELD * const field, const unsigned int /* }}} */ +#ifndef AUTO_SEC_PART_DIGITS +# define AUTO_SEC_PART_DIGITS 39 +#endif + /* {{{ ps_fetch_time */ static void ps_fetch_time(zval * zv, const MYSQLND_FIELD * const field, const unsigned int pack_len, const zend_uchar ** row) @@ -305,10 +309,11 @@ ps_fetch_time(zval * zv, const MYSQLND_FIELD * const field, const unsigned int p t.time_type = MYSQLND_TIMESTAMP_TIME; } - if (field->decimals > 0 && field->decimals < 7) { + unsigned int decimals = field->decimals == AUTO_SEC_PART_DIGITS ? (t.second_part ? 6 : 0) : field->decimals; + if (decimals > 0 && decimals < 7) { ZVAL_STR(zv, zend_strpprintf(0, "%s%02u:%02u:%02u.%0*u", - (t.neg ? "-" : ""), t.hour, t.minute, t.second, field->decimals, - (uint32_t) (t.second_part / pow(10, 6 - field->decimals)))); + (t.neg ? "-" : ""), t.hour, t.minute, t.second, decimals, + (uint32_t) (t.second_part / pow(10, 6 - decimals)))); } else { ZVAL_STR(zv, zend_strpprintf(0, "%s%02u:%02u:%02u", (t.neg ? "-" : ""), t.hour, t.minute, t.second)); @@ -401,10 +406,11 @@ ps_fetch_datetime(zval * zv, const MYSQLND_FIELD * const field, const unsigned i t.time_type = MYSQLND_TIMESTAMP_DATETIME; } - if (field->decimals > 0 && field->decimals < 7) { + unsigned int decimals = field->decimals == AUTO_SEC_PART_DIGITS ? (t.second_part ? 6 : 0) : field->decimals; + if (decimals > 0 && decimals < 7) { ZVAL_STR(zv, zend_strpprintf(0, "%04u-%02u-%02u %02u:%02u:%02u.%0*u", - t.year, t.month, t.day, t.hour, t.minute, t.second, field->decimals, - (uint32_t) (t.second_part / pow(10, 6 - field->decimals)))); + t.year, t.month, t.day, t.hour, t.minute, t.second, decimals, + (uint32_t) (t.second_part / pow(10, 6 - decimals)))); } else { ZVAL_STR(zv, zend_strpprintf(0, "%04u-%02u-%02u %02u:%02u:%02u", t.year, t.month, t.day, t.hour, t.minute, t.second)); From be41b58628e82de9cdc4064dfdc9c7f779c572e7 Mon Sep 17 00:00:00 2001 From: Ojus Chugh Date: Wed, 21 Oct 2026 18:42:17 +0530 Subject: [PATCH 2/2] Treat every decimals value above 6 as unknown precision --- ext/mysqlnd/mysqlnd_ps_codec.c | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/ext/mysqlnd/mysqlnd_ps_codec.c b/ext/mysqlnd/mysqlnd_ps_codec.c index 66f9d1a71b4d..53ff394ebeb8 100644 --- a/ext/mysqlnd/mysqlnd_ps_codec.c +++ b/ext/mysqlnd/mysqlnd_ps_codec.c @@ -264,10 +264,6 @@ ps_fetch_double(zval * zv, const MYSQLND_FIELD * const field, const unsigned int /* }}} */ -#ifndef AUTO_SEC_PART_DIGITS -# define AUTO_SEC_PART_DIGITS 39 -#endif - /* {{{ ps_fetch_time */ static void ps_fetch_time(zval * zv, const MYSQLND_FIELD * const field, const unsigned int pack_len, const zend_uchar ** row) @@ -309,8 +305,8 @@ ps_fetch_time(zval * zv, const MYSQLND_FIELD * const field, const unsigned int p t.time_type = MYSQLND_TIMESTAMP_TIME; } - unsigned int decimals = field->decimals == AUTO_SEC_PART_DIGITS ? (t.second_part ? 6 : 0) : field->decimals; - if (decimals > 0 && decimals < 7) { + unsigned int decimals = field->decimals > 6 ? (t.second_part ? 6 : 0) : field->decimals; + if (decimals > 0) { ZVAL_STR(zv, zend_strpprintf(0, "%s%02u:%02u:%02u.%0*u", (t.neg ? "-" : ""), t.hour, t.minute, t.second, decimals, (uint32_t) (t.second_part / pow(10, 6 - decimals)))); @@ -406,8 +402,8 @@ ps_fetch_datetime(zval * zv, const MYSQLND_FIELD * const field, const unsigned i t.time_type = MYSQLND_TIMESTAMP_DATETIME; } - unsigned int decimals = field->decimals == AUTO_SEC_PART_DIGITS ? (t.second_part ? 6 : 0) : field->decimals; - if (decimals > 0 && decimals < 7) { + unsigned int decimals = field->decimals > 6 ? (t.second_part ? 6 : 0) : field->decimals; + if (decimals > 0) { ZVAL_STR(zv, zend_strpprintf(0, "%04u-%02u-%02u %02u:%02u:%02u.%0*u", t.year, t.month, t.day, t.hour, t.minute, t.second, decimals, (uint32_t) (t.second_part / pow(10, 6 - decimals))));