]> git.ipfire.org Git - thirdparty/systemd.git/commitdiff
tree-wide: Replace exp10() with our own impl
authorDaan De Meyer <daan@amutable.com>
Fri, 15 May 2026 12:16:01 +0000 (12:16 +0000)
committerDaan De Meyer <daan.j.demeyer@gmail.com>
Wed, 20 May 2026 09:03:27 +0000 (11:03 +0200)
exp10() has a symbol version > 2.34 on latest glibc. To allow
dropping our baseline required glibc runtime version to <= 2.34,
let's add our own version to prevent pulling in the newer symbol
from glibc.

src/basic/math-util.c [new file with mode: 0644]
src/basic/math-util.h
src/basic/meson.build
src/libsystemd/sd-json/sd-json.c
src/shared/color-util.c
src/shared/dns-rr.c
src/test/test-math-util.c

diff --git a/src/basic/math-util.c b/src/basic/math-util.c
new file mode 100644 (file)
index 0000000..3d95e40
--- /dev/null
@@ -0,0 +1,24 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include "math-util.h"
+
+double xexp10i(int n) {
+        /* Powers of 10 up to 10^22 are exact in IEEE-754 binary64. */
+        static const double table[] = {
+                1e0,  1e1,  1e2,  1e3,  1e4,  1e5,  1e6,  1e7,  1e8,  1e9,  1e10, 1e11,
+                1e12, 1e13, 1e14, 1e15, 1e16, 1e17, 1e18, 1e19, 1e20, 1e21, 1e22,
+        };
+        bool negative = n < 0;
+
+        /* Cast before negation so n == INT_MIN doesn't invoke signed-overflow UB. Unsigned negation
+         * wraps to the magnitude we want. */
+        unsigned k = negative ? -(unsigned) n : (unsigned) n;
+
+        /* 10^309 already overflows binary64 to +Inf; anything beyond just stays there. */
+        k = MIN(k, 309u);
+        double r = k < ELEMENTSOF(table) ? table[k] : table[ELEMENTSOF(table) - 1];
+        for (unsigned i = ELEMENTSOF(table) - 1; i < k; i++)
+                r *= 10.0;
+
+        return negative ? 1.0 / r : r;
+}
index f3c471b1606ee3e85e7b552cbdd747aa37b033db..cac5fc31311d5b00e2745cbb3835220b6981698a 100644 (file)
@@ -12,3 +12,8 @@
 /* To avoid x == y and triggering compile warning -Wfloat-equal. This returns false if one of the argument is
  * NaN or infinity. One of the argument must be a floating point. */
 #define fp_equal(x, y) iszero_safe((x) - (y))
+
+/* 10^n. Exact for |n| ≤ 22; otherwise multiplies and may accumulate rounding error. Saturates to
+ * 0.0 or +Inf outside binary64's exponent range; large |n| is capped internally so untrusted
+ * inputs can't cause unbounded work. */
+double xexp10i(int n);
index 9007cff1781491dfc20554ab8f27f176812aff71..20c424b45bc51d5321d95f513b8ee85423d57a1f 100644 (file)
@@ -64,6 +64,7 @@ basic_sources = files(
         'log.c',
         'log-context.c',
         'login-util.c',
+        'math-util.c',
         'memfd-util.c',
         'memory-util.c',
         'mempool.c',
index 1ba07da6f9b4f5158d0dd92db37ec7f902fb97e1..dad7a3174d94a948cc5b0b981096bb7c6e777658 100644 (file)
@@ -2801,7 +2801,11 @@ static int json_parse_number(const char **p, JsonValue *ret) {
         *p = c;
 
         if (is_real) {
-                ret->real = ((negative ? -1.0 : 1.0) * (x + (y / shift))) * exp10((exponent_negative ? -1.0 : 1.0) * exponent);
+                /* Clamp before casting to int — a JSON input with an absurdly large exponent could
+                 * otherwise trigger undefined behaviour in the double→int conversion. xexp10i()
+                 * itself saturates anything beyond ~10^308, so clamping at INT_MAX is harmless. */
+                int e = exponent > (double) INT_MAX ? INT_MAX : (int) exponent;
+                ret->real = ((negative ? -1.0 : 1.0) * (x + (y / shift))) * xexp10i(exponent_negative ? -e : e);
                 return JSON_TOKEN_REAL;
         } else if (negative) {
                 ret->integer = i;
index f2add33b1d85b3293c7c6fb07c6d3d3771b6f23c..1d30732147f836362896fb8fba5bf3cec9ea12da 100644 (file)
@@ -1,8 +1,7 @@
 /* SPDX-License-Identifier: LGPL-2.1-or-later */
 
-#include <math.h>
-
 #include "color-util.h"
+#include "math-util.h"
 
 void rgb_to_hsv(double r, double g, double b,
                 double *ret_h, double *ret_s, double *ret_v) {
index 0ad01de88a684a2fff37c62af7c5960b44c7b63d..eb91910e835f819e706578266d39b68a4afef940 100644 (file)
@@ -1,7 +1,5 @@
 /* SPDX-License-Identifier: LGPL-2.1-or-later */
 
-#include <math.h>
-
 #include "alloc-util.h"
 #include "bitmap.h"
 #include "dns-answer.h"                 /* IWYU pragma: keep */
@@ -13,6 +11,7 @@
 #include "hash-funcs.h"
 #include "hexdecoct.h"
 #include "json-util.h"
+#include "math-util.h"
 #include "memory-util.h"
 #include "siphash24.h"
 #include "string-table.h"
@@ -773,9 +772,9 @@ static char* format_location(uint32_t latitude, uint32_t longitude, uint32_t alt
         int lat = latitude >= 1U<<31 ? (int) (latitude - (1U<<31)) : (int) ((1U<<31) - latitude);
         int lon = longitude >= 1U<<31 ? (int) (longitude - (1U<<31)) : (int) ((1U<<31) - longitude);
         double alt = altitude >= 10000000u ? altitude - 10000000u : -(double)(10000000u - altitude);
-        double siz = (size >> 4) * exp10((double) (size & 0xF));
-        double hor = (horiz_pre >> 4) * exp10((double) (horiz_pre & 0xF));
-        double ver = (vert_pre >> 4) * exp10((double) (vert_pre & 0xF));
+        double siz = (size >> 4) * xexp10i(size & 0xF);
+        double hor = (horiz_pre >> 4) * xexp10i(horiz_pre & 0xF);
+        double ver = (vert_pre >> 4) * xexp10i(vert_pre & 0xF);
 
         if (asprintf(&s, "%d %d %.3f %c %d %d %.3f %c %.2fm %.2fm %.2fm %.2fm",
                      (lat / 60000 / 60),
index 9771576fcbb6b939ae9c6504cf1daa09ad92d19f..3e8a9d5ba321ac04c39ac2cdcf0a4eda901cc10c 100644 (file)
@@ -107,4 +107,39 @@ TEST(fp_equal) {
         assert_se(!fp_equal(42 / INFINITY, INFINITY / INFINITY));
 }
 
+TEST(xexp10i) {
+        /* Table-lookup range: every value is exact in binary64 */
+        ASSERT_TRUE(fp_equal(xexp10i(0), 1.0));
+        ASSERT_TRUE(fp_equal(xexp10i(1), 10.0));
+        ASSERT_TRUE(fp_equal(xexp10i(2), 100.0));
+        ASSERT_TRUE(fp_equal(xexp10i(22), 1e22));
+
+        /* Negative exponents */
+        ASSERT_TRUE(fp_equal(xexp10i(-1), 0.1));
+        ASSERT_TRUE(fp_equal(xexp10i(-3), 0.001));
+
+        /* Beyond the table: result still matches a plain 10.0 multiplication chain (no precision
+         * claim beyond binary64) */
+        ASSERT_TRUE(xexp10i(23) > 0.9e23 && xexp10i(23) < 1.1e23);
+        ASSERT_TRUE(xexp10i(100) > 0.9e100 && xexp10i(100) < 1.1e100);
+
+        /* Overflow saturates to +Inf, underflow to 0 — matching glibc exp10() */
+        ASSERT_TRUE(isinf(xexp10i(400)));
+        ASSERT_TRUE(fp_equal(xexp10i(-400), 0.0));
+
+        /* Pathological inputs must still terminate quickly thanks to the internal cap */
+        ASSERT_TRUE(isinf(xexp10i(INT_MAX)));
+        ASSERT_TRUE(fp_equal(xexp10i(INT_MIN), 0.0));
+
+        /* Regression guard for the DBL_MAX round-trip described in math-util.c: 10^308 must be small
+         * enough that DBL_MAX's mantissa (≈ 1.7976931348623157) multiplied by it does not overflow
+         * to +Inf. Delegating to __builtin_powi(10.0, n) here breaks this — libgcc's __powidf2
+         * accumulates a few ULPs of error through repeated squaring, and the product spills over.
+         * Matching test-json's delta of 0.0001, the reconstructed value must land within 0.01% of
+         * DBL_MAX. */
+        double dbl_max_reconstructed = 1.7976931348623157 * xexp10i(308);
+        ASSERT_FALSE(isinf(dbl_max_reconstructed));
+        ASSERT_TRUE(ABS(1.0 - DBL_MAX / dbl_max_reconstructed) < 0.0001);
+}
+
 DEFINE_TEST_MAIN(LOG_DEBUG);