Commit 89687719b40 for php.net
commit 89687719b40691a773bd57a20147288c2fa278fc
Author: Louis-Arnaud <la.catoire@gmail.com>
Date: Wed Sep 9 19:51:30 2026 +0200
ext/calendar: take zend_long in the *ToSdn() conversion helpers (#23555)
GregorianToSdn(), JulianToSdn(), JewishToSdn() and FrenchToSdn() took C
int parameters while every PHP_FUNCTION() calling them parses zend_long,
so the arguments were implicitly narrowed before any range check ran.
Values whose low 32 bits alias into the accepted range slipped past the
guards inside the helpers: gregoriantojd(1 + 2**32, 1, 1) returned the
Julian Day of month 1 instead of 0. cal_to_jd() and cal_days_in_month()
were affected the same way through cal_to_jd_func_t.
Widen the parameters so the existing guards see the value the user
passed. The typedef's parameter names were also in the wrong order and
are fixed to match the callees.
JewishToSdn() passes its year on to FindStartOfYear(), whose first two
outputs are int, so the year is narrowed once in JewishToSdn() itself,
right after the check that leaves both year and year + 1 within int
range. FindStartOfYear() keeps its int parameter and stays unaware of
bounds its caller enforces.
Widening JewishToSdn() additionally removes a signed overflow: `sdn =
tishri1 + day - 1` was computed in int, so year 5879542 returned a
negative Julian Day.
juliantojd_overflow.phpt asserted the aliased result for year 6000000000,
which is past the supported maximum enforced since GH-22602
(INT_MAX - 4800); it now returns 0, and the test covers the largest
accepted year instead.
diff --git a/NEWS b/NEWS
index 16faf437201..bacb68f387d 100644
--- a/NEWS
+++ b/NEWS
@@ -30,6 +30,10 @@ PHP NEWS
. Fixed bug GH-23242 (PHP development server does not support Expect
100-continue flow control). (Sjoerd Langkemper)
+- Calendar:
+ . Fixed *tojd() functions and cal_to_jd() truncating arguments outside the
+ int range instead of rejecting them. (lacatoire)
+
- DOM:
. Fixed NamedNodeMap::getNamedItemNS() with an empty URI not matching
the null namespace in spec-following mode. (Ilia Alshanetsky)
diff --git a/ext/calendar/calendar.c b/ext/calendar/calendar.c
index f26d611092d..14daf3a5e67 100644
--- a/ext/calendar/calendar.c
+++ b/ext/calendar/calendar.c
@@ -41,7 +41,7 @@ enum cal_name_type_t {
CAL_NUM_CALS
};
-typedef zend_long (*cal_to_jd_func_t) (int month, int day, int year);
+typedef zend_long (*cal_to_jd_func_t) (zend_long year, zend_long month, zend_long day);
typedef void (*cal_from_jd_func_t) (zend_long jd, int *year, int *month, int *day);
typedef char *(*cal_as_string_func_t) (int year, int month, int day);
diff --git a/ext/calendar/french.c b/ext/calendar/french.c
index 0707e27ed8d..91309832562 100644
--- a/ext/calendar/french.c
+++ b/ext/calendar/french.c
@@ -115,9 +115,9 @@ void SdnToFrench(
}
zend_long FrenchToSdn(
- int year,
- int month,
- int day)
+ zend_long year,
+ zend_long month,
+ zend_long day)
{
/* check for invalid dates */
if (year < 1 || year > 14 ||
diff --git a/ext/calendar/gregor.c b/ext/calendar/gregor.c
index eaee9c8c73c..4047051cf3e 100644
--- a/ext/calendar/gregor.c
+++ b/ext/calendar/gregor.c
@@ -200,12 +200,12 @@ void SdnToGregorian(
}
zend_long GregorianToSdn(
- int inputYear,
- int inputMonth,
- int inputDay)
+ zend_long inputYear,
+ zend_long inputMonth,
+ zend_long inputDay)
{
zend_long year;
- int month;
+ zend_long month;
/* check for invalid dates */
if (inputYear == 0 || inputYear < -4714 ||
diff --git a/ext/calendar/jewish.c b/ext/calendar/jewish.c
index b589c4cb261..c6aec67c854 100644
--- a/ext/calendar/jewish.c
+++ b/ext/calendar/jewish.c
@@ -696,9 +696,9 @@ void SdnToJewish(
* and compare with the original.
*/
zend_long JewishToSdn(
- int year,
- int month,
- int day)
+ zend_long year,
+ zend_long month,
+ zend_long day)
{
zend_long sdn;
int metonicCycle;
@@ -713,11 +713,14 @@ zend_long JewishToSdn(
if (year <= 0 || year >= INT_MAX - 1 || day <= 0 || day > 30) {
return (0);
}
+ /* The check above leaves both year and year + 1 within int range. */
+ int int_year = (int) year;
+
switch (month) {
case 1:
case 2:
/* It is Tishri or Heshvan - don't need the year length. */
- FindStartOfYear(year, &metonicCycle, &metonicYear,
+ FindStartOfYear(int_year, &metonicCycle, &metonicYear,
&moladDay, &moladHalakim, &tishri1);
if (month == 1) {
sdn = tishri1 + day - 1;
@@ -730,7 +733,7 @@ zend_long JewishToSdn(
/* It is Kislev - must find the year length. */
/* Find the start of the year. */
- FindStartOfYear(year, &metonicCycle, &metonicYear,
+ FindStartOfYear(int_year, &metonicCycle, &metonicYear,
&moladDay, &moladHalakim, &tishri1);
/* Find the end of the year. */
@@ -753,10 +756,10 @@ zend_long JewishToSdn(
case 6:
/* It is Tevet, Shevat or Adar I - don't need the year length. */
- FindStartOfYear(year + 1, &metonicCycle, &metonicYear,
+ FindStartOfYear(int_year + 1, &metonicCycle, &metonicYear,
&moladDay, &moladHalakim, &tishri1After);
- if (monthsPerYear[(year - 1) % 19] == 12) {
+ if (monthsPerYear[(int_year - 1) % 19] == 12) {
lengthOfAdarIAndII = 29;
} else {
lengthOfAdarIAndII = 59;
@@ -773,7 +776,7 @@ zend_long JewishToSdn(
default:
/* It is Adar II or later - don't need the year length. */
- FindStartOfYear(year + 1, &metonicCycle, &metonicYear,
+ FindStartOfYear(int_year + 1, &metonicCycle, &metonicYear,
&moladDay, &moladHalakim, &tishri1After);
switch (month) {
diff --git a/ext/calendar/julian.c b/ext/calendar/julian.c
index c24527941d3..d539e2b45bd 100644
--- a/ext/calendar/julian.c
+++ b/ext/calendar/julian.c
@@ -213,12 +213,12 @@ void SdnToJulian(
}
zend_long JulianToSdn(
- int inputYear,
- int inputMonth,
- int inputDay)
+ zend_long inputYear,
+ zend_long inputMonth,
+ zend_long inputDay)
{
zend_long year;
- int month;
+ zend_long month;
/* check for invalid dates */
if (inputYear == 0 || inputYear < -4713 ||
diff --git a/ext/calendar/sdncal.h b/ext/calendar/sdncal.h
index fa3aa7c641a..17202791c43 100644
--- a/ext/calendar/sdncal.h
+++ b/ext/calendar/sdncal.h
@@ -69,17 +69,17 @@
/* Gregorian calendar conversions. */
void SdnToGregorian(zend_long sdn, int *pYear, int *pMonth, int *pDay);
-zend_long GregorianToSdn(int year, int month, int day);
+zend_long GregorianToSdn(zend_long year, zend_long month, zend_long day);
extern const char * const MonthNameShort[13];
extern const char * const MonthNameLong[13];
/* Julian calendar conversions. */
void SdnToJulian(zend_long sdn, int *pYear, int *pMonth, int *pDay);
-zend_long JulianToSdn(int year, int month, int day);
+zend_long JulianToSdn(zend_long year, zend_long month, zend_long day);
/* Jewish calendar conversions. */
void SdnToJewish(zend_long sdn, int *pYear, int *pMonth, int *pDay);
-zend_long JewishToSdn(int year, int month, int day);
+zend_long JewishToSdn(zend_long year, zend_long month, zend_long day);
extern const char * const JewishMonthName[14];
extern const char * const JewishMonthNameLeap[14];
extern const char * const JewishMonthHebName[14];
@@ -88,7 +88,7 @@ extern const int monthsPerYear[19];
/* French republic calendar conversions. */
void SdnToFrench(zend_long sdn, int *pYear, int *pMonth, int *pDay);
-zend_long FrenchToSdn(int inputYear, int inputMonth, int inputDay);
+zend_long FrenchToSdn(zend_long inputYear, zend_long inputMonth, zend_long inputDay);
extern const char * const FrenchMonthName[14];
/* Islamic calendar conversions. */
diff --git a/ext/calendar/tests/juliantojd_overflow.phpt b/ext/calendar/tests/juliantojd_overflow.phpt
index cdf1b203d96..3422c6c7268 100644
--- a/ext/calendar/tests/juliantojd_overflow.phpt
+++ b/ext/calendar/tests/juliantojd_overflow.phpt
@@ -6,7 +6,11 @@
<?php if (PHP_INT_SIZE != 8) die("skip this test is for 64bit platform only"); ?>
--FILE--
<?php
+/* The largest year JulianToSdn() accepts, INT_MAX - 4800. */
+echo juliantojd(5, 5, 2147478847) . "\n";
+/* Anything above it is rejected rather than truncated. */
echo juliantojd(5, 5, 6000000000) . "\n";
?>
--EXPECT--
-622764916319
+784368370049
+0
diff --git a/ext/calendar/tests/tojd_out_of_int_range.phpt b/ext/calendar/tests/tojd_out_of_int_range.phpt
new file mode 100644
index 00000000000..aeb6b9c4b5c
--- /dev/null
+++ b/ext/calendar/tests/tojd_out_of_int_range.phpt
@@ -0,0 +1,57 @@
+--TEST--
+*tojd(): arguments outside the int range must not alias into the accepted range
+--EXTENSIONS--
+calendar
+--SKIPIF--
+<?php if (PHP_INT_SIZE != 8) die("skip this test is for 64bit platform only"); ?>
+--FILE--
+<?php
+// Values whose low 32 bits land in the accepted range must still be rejected.
+$bias = 2 ** 32;
+
+foreach (['gregoriantojd', 'juliantojd', 'jewishtojd', 'frenchtojd'] as $function) {
+ echo $function, "\n";
+ var_dump($function(1, 1, 1));
+ var_dump($function(1 + $bias, 1, 1));
+ var_dump($function(1, 1 + $bias, 1));
+ try {
+ var_dump($function(1, 1, 1 + $bias));
+ } catch (Throwable $e) {
+ echo $e::class, ': ', $e->getMessage(), "\n";
+ }
+}
+
+// cal_to_jd() and cal_days_in_month() reach the same helpers through
+// cal_to_jd_func_t, and have no lower bound on $year of their own.
+echo "cal_to_jd\n";
+var_dump(cal_to_jd(CAL_GREGORIAN, 1, 1, 1 - $bias));
+try {
+ var_dump(cal_days_in_month(CAL_GREGORIAN, 1, 1 - $bias));
+} catch (Throwable $e) {
+ echo $e::class, ': ', $e->getMessage(), "\n";
+}
+?>
+--EXPECT--
+gregoriantojd
+int(1721426)
+int(0)
+int(0)
+int(0)
+juliantojd
+int(1721424)
+int(0)
+int(0)
+int(0)
+jewishtojd
+int(347998)
+int(0)
+int(0)
+ValueError: jewishtojd(): Argument #3 ($year) must be between -2147483648 and 2147483647
+frenchtojd
+int(2375840)
+int(0)
+int(0)
+int(0)
+cal_to_jd
+int(0)
+ValueError: Invalid date