From 750a28967ba01f70c8894c496b19b82b4d8bd67e Mon Sep 17 00:00:00 2001 From: kavyabhand Date: Sun, 2 Aug 2026 16:04:34 +0530 Subject: [PATCH 1/3] docs: survey upstream issues and add minimal bug reproducers --- docs/evidence/repro/ctype_highbit.c | 15 ++ docs/evidence/repro/ctype_trap.h | 26 +++ docs/evidence/repro/fac_nan.c | 22 ++ docs/evidence/repro/locale_decimals.c | 24 +++ docs/evidence/repro/nest_overflow.c | 50 +++++ docs/evidence/repro/next_token_main.c | 10 + docs/evidence/repro/next_token_other.c | 10 + docs/evidence/repro/run-all.sh | 124 ++++++++++++ docs/evidence/repro/run-log-darwin.txt | 100 +++++++++ docs/evidence/repro/run-log-linux-docker.txt | 87 ++++++++ .../repro/run-log-linux-signed-char.txt | 8 + docs/evidence/repro/run-log.txt | 86 ++++++++ docs/evidence/upstream-issues.md | 190 ++++++++++++++++++ 13 files changed, 752 insertions(+) create mode 100644 docs/evidence/repro/ctype_highbit.c create mode 100644 docs/evidence/repro/ctype_trap.h create mode 100644 docs/evidence/repro/fac_nan.c create mode 100644 docs/evidence/repro/locale_decimals.c create mode 100644 docs/evidence/repro/nest_overflow.c create mode 100644 docs/evidence/repro/next_token_main.c create mode 100644 docs/evidence/repro/next_token_other.c create mode 100755 docs/evidence/repro/run-all.sh create mode 100644 docs/evidence/repro/run-log-darwin.txt create mode 100644 docs/evidence/repro/run-log-linux-docker.txt create mode 100644 docs/evidence/repro/run-log-linux-signed-char.txt create mode 100644 docs/evidence/repro/run-log.txt create mode 100644 docs/evidence/upstream-issues.md diff --git a/docs/evidence/repro/ctype_highbit.c b/docs/evidence/repro/ctype_highbit.c new file mode 100644 index 0000000..016c264 --- /dev/null +++ b/docs/evidence/repro/ctype_highbit.c @@ -0,0 +1,15 @@ +/* Lead B: signed-char ctype surface. + * Feeds a single 0x80 byte into te_interp. On signed-char platforms this + * reaches isalpha with a negative argument (C99 7.4p1). */ +#include +#include +#include "tinyexpr.h" + +int main(void) { + char buf[2] = {(char)0x80, 0}; + int err = 0; + printf("CHAR_MIN=%d CHAR_MAX=%d buf0=%d\n", (int)CHAR_MIN, (int)CHAR_MAX, (int)buf[0]); + double v = te_interp(buf, &err); + printf("te_interp=%.17g err=%d\n", v, err); + return 0; +} diff --git a/docs/evidence/repro/ctype_trap.h b/docs/evidence/repro/ctype_trap.h new file mode 100644 index 0000000..0aef2c1 --- /dev/null +++ b/docs/evidence/repro/ctype_trap.h @@ -0,0 +1,26 @@ +/* Trapping ctype header: abort if isalpha/isdigit see negative args. + * Compile with: cc -include ctype_trap.h ... */ +#ifndef CTYPE_TRAP_H +#define CTYPE_TRAP_H +#include +#include +#include_next +#undef isalpha +#undef isdigit +static inline int trap_isalpha(int c) { + if (c < 0 || c > 255) { + fprintf(stderr, "ctype_trap: isalpha(%d)\n", c); + abort(); + } + return (c == EOF) ? 0 : (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z'); +} +static inline int trap_isdigit(int c) { + if (c < 0 || c > 255) { + fprintf(stderr, "ctype_trap: isdigit(%d)\n", c); + abort(); + } + return (c == EOF) ? 0 : (c >= '0' && c <= '9'); +} +#define isalpha(c) trap_isalpha(c) +#define isdigit(c) trap_isdigit(c) +#endif diff --git a/docs/evidence/repro/fac_nan.c b/docs/evidence/repro/fac_nan.c new file mode 100644 index 0000000..bfc1e96 --- /dev/null +++ b/docs/evidence/repro/fac_nan.c @@ -0,0 +1,22 @@ +/* fac(NaN) / fac(0/0) probe: C returns 1.0 on this class of inputs. */ +#include +#include +#include "tinyexpr.h" + +static void show(const char *expr) { + int err = 0; + double v = te_interp(expr, &err); + unsigned long long bits = 0; + memcpy(&bits, &v, sizeof bits); + printf("expr=%s err=%d value=%.17g bits=0x%llx\n", expr, err, v, (unsigned long long)bits); +} + +int main(void) { + show("0/0"); + show("fac(0/0)"); + show("fac((0/0))"); + show("fac(ncr(0,1))"); + show("ncr(0,1)"); + show("fac(21)"); + return 0; +} diff --git a/docs/evidence/repro/locale_decimals.c b/docs/evidence/repro/locale_decimals.c new file mode 100644 index 0000000..94bde89 --- /dev/null +++ b/docs/evidence/repro/locale_decimals.c @@ -0,0 +1,24 @@ +/* Lead C / locale: setlocale then te_interp on decimal expressions. */ +#include +#include +#include +#include +#include "tinyexpr.h" + +static void try_expr(const char *expr) { + int err = 0; + double v = te_interp(expr, &err); + printf("expr=%s err=%d value=%.17g\n", expr, err, v); +} + +int main(int argc, char **argv) { + const char *loc = (argc > 1) ? argv[1] : "de_DE.UTF-8"; + char *got = setlocale(LC_ALL, loc); + printf("setlocale(%s)=%s\n", loc, got ? got : "(null)"); + if (!got) return 2; + try_expr("1.5"); + try_expr(".5"); + try_expr("1,5"); + try_expr("2.25+3.5"); + return 0; +} diff --git a/docs/evidence/repro/nest_overflow.c b/docs/evidence/repro/nest_overflow.c new file mode 100644 index 0000000..2257260 --- /dev/null +++ b/docs/evidence/repro/nest_overflow.c @@ -0,0 +1,50 @@ +/* Issue #136 nest probe: binary search crash threshold for te_interp / te_compile. */ +#include +#include +#include +#include +#include +#include "tinyexpr.h" + +static int try_depth(int depth, int mode) { + char *s = malloc((size_t)depth * 2 + 2); + if (!s) return -1; + memset(s, '(', (size_t)depth); + s[depth] = '1'; + memset(s + depth + 1, ')', (size_t)depth); + s[depth * 2 + 1] = 0; + int err = 0; + if (mode == 0) { + (void)te_interp(s, &err); + } else { + te_expr *e = te_compile(s, 0, 0, &err); + te_free(e); + } + free(s); + return 0; +} + +int main(int argc, char **argv) { + int mode = (argc > 1 && strcmp(argv[1], "compile") == 0) ? 1 : 0; + int lo = 1, hi = 100000, last_ok = 0, first_crash = -1; + while (lo <= hi) { + int mid = lo + (hi - lo) / 2; + pid_t pid = fork(); + if (pid == 0) { + try_depth(mid, mode); + _exit(0); + } + int st = 0; + waitpid(pid, &st, 0); + int ok = WIFEXITED(st) && WEXITSTATUS(st) == 0; + if (ok) { + last_ok = mid; + lo = mid + 1; + } else { + first_crash = mid; + hi = mid - 1; + } + } + printf("mode=%s last_ok=%d first_crash=%d\n", mode ? "compile" : "interp", last_ok, first_crash); + return 0; +} diff --git a/docs/evidence/repro/next_token_main.c b/docs/evidence/repro/next_token_main.c new file mode 100644 index 0000000..38052da --- /dev/null +++ b/docs/evidence/repro/next_token_main.c @@ -0,0 +1,10 @@ +/* Minimal caller that forces the lexer to run (and thus next_token). */ +#include +#include "tinyexpr.h" + +int main(void) { + int err = 0; + double v = te_interp("1+2", &err); + printf("te_interp=%.17g err=%d\n", v, err); + return 0; +} diff --git a/docs/evidence/repro/next_token_other.c b/docs/evidence/repro/next_token_other.c new file mode 100644 index 0000000..65cb0ba --- /dev/null +++ b/docs/evidence/repro/next_token_other.c @@ -0,0 +1,10 @@ +/* next_token collision probe: second TU defining next_token. + * Build/link against tinyexpr.c to observe static vs dynamic linkage. */ +#include + +/* Deliberately non-static, same signature as tinyexpr.c:239. */ +void next_token(void *s) { + (void)s; + fputs("COLLISION_OTHER_next_token_CALLED\n", stderr); + fflush(stderr); +} diff --git a/docs/evidence/repro/run-all.sh b/docs/evidence/repro/run-all.sh new file mode 100755 index 0000000..7373570 --- /dev/null +++ b/docs/evidence/repro/run-all.sh @@ -0,0 +1,124 @@ +#!/usr/bin/env bash +# Run all bug-lead reproducers. Prefer Linux (Docker) for linkage experiments. +set -euo pipefail +ROOT="$(cd "$(dirname "$0")/../../.." && pwd)" +REPRO="$ROOT/docs/evidence/repro" +TE="$ROOT/third_party/tinyexpr-c" +OUT="$REPRO/run-log.txt" +INC="-I$TE" +CC="${CC:-cc}" + +{ +echo "=== host ===" +uname -a +echo "cc=$($CC --version | head -1)" +echo "utc=$(date -u +%Y-%m-%dT%H:%M:%SZ)" +echo "git=$(git -C "$ROOT" rev-parse HEAD)" +ulimit -s || true + +echo +echo "=== CHAR signedness / ctype highbit (plain) ===" +$CC -O2 $INC -o "$REPRO/ctype_highbit" "$REPRO/ctype_highbit.c" "$TE/tinyexpr.c" -lm +"$REPRO/ctype_highbit" || true + +echo +echo "=== ctype highbit with trapping header ===" +set +e +$CC -O2 $INC -include "$REPRO/ctype_trap.h" -o "$REPRO/ctype_highbit_trap" \ + "$REPRO/ctype_highbit.c" "$TE/tinyexpr.c" -lm +"$REPRO/ctype_highbit_trap" +echo "trap_exit=$?" +set -e + +echo +echo "=== fac/NaN probe ===" +$CC -O2 $INC -o "$REPRO/fac_nan" "$REPRO/fac_nan.c" "$TE/tinyexpr.c" -lm +"$REPRO/fac_nan" + +echo +echo "=== nest overflow binary search (interp) ===" +$CC -O2 $INC -o "$REPRO/nest_overflow" "$REPRO/nest_overflow.c" "$TE/tinyexpr.c" -lm +"$REPRO/nest_overflow" interp +"$REPRO/nest_overflow" compile + +echo +echo "=== next_token static link (expect duplicate symbol) ===" +set +e +$CC -O2 $INC -c "$TE/tinyexpr.c" -o "$REPRO/tinyexpr.o" +$CC -O2 $INC -c "$REPRO/next_token_other.c" -o "$REPRO/next_token_other.o" +$CC -O2 $INC -c "$REPRO/next_token_main.c" -o "$REPRO/next_token_main.o" +$CC -O2 -o "$REPRO/next_token_static" "$REPRO/next_token_main.o" "$REPRO/tinyexpr.o" "$REPRO/next_token_other.o" -lm 2>"$REPRO/next_token_static.err" +echo "static_link_exit=$?" +cat "$REPRO/next_token_static.err" || true +set -e + +echo +echo "=== next_token: link with -Wl,-force_load / allow multiple (platform-specific) ===" +set +e +# GNU ld: --allow-multiple-definition makes the first definition win (link order). +if $CC -O2 -o "$REPRO/next_token_multi_te_first" \ + "$REPRO/next_token_main.o" "$REPRO/tinyexpr.o" "$REPRO/next_token_other.o" -lm \ + -Wl,--allow-multiple-definition 2>/dev/null; then + echo "multi_te_first_built=1" + "$REPRO/next_token_multi_te_first"; echo "run_exit=$?" +elif $CC -O2 -o "$REPRO/next_token_multi_te_first" \ + "$REPRO/next_token_main.o" "$REPRO/tinyexpr.o" "$REPRO/next_token_other.o" -lm \ + -Wl,-mllvm 2>"$REPRO/next_token_multi.err"; then + true +else + # macOS ld64: -force_load not applicable; try -alias / just report static failure + echo "allow-multiple-definition unsupported on this linker; see static_link_exit" + cat "$REPRO/next_token_multi.err" 2>/dev/null || true +fi +# Other first: if multi-def allowed, other next_token may win and break parsing. +if $CC -O2 -o "$REPRO/next_token_multi_other_first" \ + "$REPRO/next_token_main.o" "$REPRO/next_token_other.o" "$REPRO/tinyexpr.o" -lm \ + -Wl,--allow-multiple-definition 2>/dev/null; then + echo "multi_other_first_built=1" + "$REPRO/next_token_multi_other_first" 2>&1; echo "run_exit=$?" +fi +set -e + +echo +echo "=== next_token dynamic: shared tinyexpr + shared other ===" +set +e +$CC -O2 -fPIC $INC -shared -o "$REPRO/libtinyexpr_probe.so" "$TE/tinyexpr.c" -lm +$CC -O2 -fPIC -shared -o "$REPRO/libother_next.so" "$REPRO/next_token_other.c" +$CC -O2 $INC -o "$REPRO/next_token_dyn" "$REPRO/next_token_main.c" \ + -L"$REPRO" -ltinyexpr_probe -lm -Wl,-rpath,"$REPRO" 2>"$REPRO/next_token_dyn.err" +echo "dyn_link_te_only_exit=$?" +# Force both libs; LD_PRELOAD other to interpose next_token if exported. +if [[ -x "$REPRO/next_token_dyn" ]]; then + echo "-- run without preload --" + "$REPRO/next_token_dyn" 2>&1 + echo "-- run with LD_PRELOAD=libother_next.so --" + if [[ "$(uname -s)" == "Darwin" ]]; then + DYLD_INSERT_LIBRARIES="$REPRO/libother_next.so" DYLD_FORCE_FLAT_NAMESPACE=1 \ + "$REPRO/next_token_dyn" 2>&1 + echo "dyld_exit=$?" + else + LD_PRELOAD="$REPRO/libother_next.so" "$REPRO/next_token_dyn" 2>&1 + echo "ld_preload_exit=$?" + fi +else + cat "$REPRO/next_token_dyn.err" || true +fi +set -e + +echo +echo "=== locale decimals (needs generated locale or system de_DE) ===" +$CC -O2 $INC -o "$REPRO/locale_decimals" "$REPRO/locale_decimals.c" "$TE/tinyexpr.c" -lm +set +e +for loc in "de_DE.UTF-8" "de_DE" "fr_FR.UTF-8" "C"; do + echo "-- try $loc --" + "$REPRO/locale_decimals" "$loc" 2>&1 + echo "locale_exit=$?" +done +# env-only smoke-style: setlocale not called +echo "-- env LC_ALL=de_DE.UTF-8 without setlocale in process --" +LC_ALL=de_DE.UTF-8 "$REPRO/locale_decimals" "C" 2>&1 +set -e + +echo +echo "=== DONE ===" +} 2>&1 | tee "$OUT" diff --git a/docs/evidence/repro/run-log-darwin.txt b/docs/evidence/repro/run-log-darwin.txt new file mode 100644 index 0000000..4f80fcf --- /dev/null +++ b/docs/evidence/repro/run-log-darwin.txt @@ -0,0 +1,100 @@ +=== host === +Darwin Kavyas-MacBook-Air-2.local 24.1.0 Darwin Kernel Version 24.1.0: Thu Oct 10 21:05:14 PDT 2024; root:xnu-11215.41.3~2/RELEASE_ARM64_T8103 arm64 +cc=Apple clang version 16.0.0 (clang-1600.0.26.6) +utc=2026-08-02T10:28:23Z +git=609f03618213b8ab925f07d98785808c1907c672 +8176 + +=== CHAR signedness / ctype highbit (plain) === +CHAR_MIN=-128 CHAR_MAX=127 buf0=-128 +te_interp=nan err=1 + +=== ctype highbit with trapping header === +In file included from :1: +/Users/kavyabhand/Desktop/port/docs/evidence/repro/ctype_trap.h:7:2: warning: #include_next in file found relative to primary source file or found by absolute path; will search from start of include path [-Winclude-next-absolute-path] + 7 | #include_next + | ^ +1 warning generated. +In file included from :1: +/Users/kavyabhand/Desktop/port/docs/evidence/repro/ctype_trap.h:7:2: warning: #include_next in file found relative to primary source file or found by absolute path; will search from start of include path [-Winclude-next-absolute-path] + 7 | #include_next + | ^ +1 warning generated. +ctype_trap: isalpha(-128) +CHAR_MIN=-128 CHAR_MAX=127 buf0=-128 +docs/evidence/repro/run-all.sh: line 124: 40489 Abort trap: 6 "$REPRO/ctype_highbit_trap" +trap_exit=134 + +=== fac/NaN probe === +expr=0/0 err=0 value=nan bits=0x7ff8000000000000 +expr=fac(0/0) err=0 value=1 bits=0x3ff0000000000000 +expr=fac((0/0)) err=0 value=1 bits=0x3ff0000000000000 +expr=fac(ncr(0,1)) err=0 value=1 bits=0x3ff0000000000000 +expr=ncr(0,1) err=0 value=nan bits=0x7ff8000000000000 +expr=fac(21) err=0 value=inf bits=0x7ff0000000000000 + +=== nest overflow binary search (interp) === +mode=interp last_ok=23763 first_crash=23764 +mode=compile last_ok=23763 first_crash=23764 + +=== next_token static link (expect duplicate symbol) === +static_link_exit=1 +duplicate symbol '_next_token' in: + /Users/kavyabhand/Desktop/port/docs/evidence/repro/next_token_other.o + /Users/kavyabhand/Desktop/port/docs/evidence/repro/tinyexpr.o +ld: 1 duplicate symbols +clang: error: linker command failed with exit code 1 (use -v to see invocation) + +=== next_token: link with -Wl,-force_load / allow multiple (platform-specific) === +allow-multiple-definition unsupported on this linker; see static_link_exit +duplicate symbol '_next_token' in: + /Users/kavyabhand/Desktop/port/docs/evidence/repro/next_token_other.o + /Users/kavyabhand/Desktop/port/docs/evidence/repro/tinyexpr.o +ld: 1 duplicate symbols +clang: error: linker command failed with exit code 1 (use -v to see invocation) + +=== next_token dynamic: shared tinyexpr + shared other === +dyn_link_te_only_exit=0 +-- run without preload -- +te_interp=3 err=0 +-- run with LD_PRELOAD=libother_next.so -- +te_interp=3 err=0 +dyld_exit=0 + +=== locale decimals (needs generated locale or system de_DE) === +-- try de_DE.UTF-8 -- +setlocale(de_DE.UTF-8)=de_DE.UTF-8 +expr=1.5 err=1 value=nan +expr=.5 err=1 value=nan +expr=1,5 err=0 value=1,5 +expr=2.25+3.5 err=1 value=nan +locale_exit=0 +-- try de_DE -- +setlocale(de_DE)=de_DE +expr=1.5 err=1 value=nan +expr=.5 err=1 value=nan +expr=1,5 err=0 value=1,5 +expr=2.25+3.5 err=1 value=nan +locale_exit=0 +-- try fr_FR.UTF-8 -- +setlocale(fr_FR.UTF-8)=fr_FR.UTF-8 +expr=1.5 err=1 value=nan +expr=.5 err=1 value=nan +expr=1,5 err=0 value=1,5 +expr=2.25+3.5 err=1 value=nan +locale_exit=0 +-- try C -- +setlocale(C)=C +expr=1.5 err=0 value=1.5 +expr=.5 err=0 value=0.5 +expr=1,5 err=0 value=5 +expr=2.25+3.5 err=0 value=5.75 +locale_exit=0 +-- env LC_ALL=de_DE.UTF-8 without setlocale in process -- +setlocale(C)=C +expr=1.5 err=0 value=1.5 +expr=.5 err=0 value=0.5 +expr=1,5 err=0 value=5 +expr=2.25+3.5 err=0 value=5.75 + +=== DONE === diff --git a/docs/evidence/repro/run-log-linux-docker.txt b/docs/evidence/repro/run-log-linux-docker.txt new file mode 100644 index 0000000..d1beaf3 --- /dev/null +++ b/docs/evidence/repro/run-log-linux-docker.txt @@ -0,0 +1,87 @@ +debconf: delaying package configuration, since apt-utils is not installed +=== host === +Linux c95e94aea731 6.12.67-linuxkit #1 SMP Sun Jan 25 02:26:28 UTC 2026 aarch64 aarch64 aarch64 GNU/Linux +cc=cc (Ubuntu 13.3.0-6ubuntu2~24.04.1) 13.3.0 +utc=2026-08-02T10:28:56Z +docs/evidence/repro/run-all.sh: line 16: git: command not found +git= +8192 + +=== CHAR signedness / ctype highbit (plain) === +CHAR_MIN=0 CHAR_MAX=255 buf0=128 +te_interp=nan err=1 + +=== ctype highbit with trapping header === +CHAR_MIN=0 CHAR_MAX=255 buf0=128 +te_interp=nan err=1 +trap_exit=0 + +=== fac/NaN probe === +expr=0/0 err=0 value=nan bits=0x7ff8000000000000 +expr=fac(0/0) err=0 value=1 bits=0x3ff0000000000000 +expr=fac((0/0)) err=0 value=1 bits=0x3ff0000000000000 +expr=fac(ncr(0,1)) err=0 value=1 bits=0x3ff0000000000000 +expr=ncr(0,1) err=0 value=nan bits=0x7ff8000000000000 +expr=fac(21) err=0 value=inf bits=0x7ff0000000000000 + +=== nest overflow binary search (interp) === +mode=interp last_ok=17470 first_crash=17471 +mode=compile last_ok=17468 first_crash=17469 + +=== next_token static link (expect duplicate symbol) === +static_link_exit=1 +/usr/bin/ld: /src/docs/evidence/repro/next_token_other.o: in function `next_token': +next_token_other.c:(.text+0x0): multiple definition of `next_token'; /src/docs/evidence/repro/tinyexpr.o:tinyexpr.c:(.text+0x580): first defined here +collect2: error: ld returned 1 exit status + +=== next_token: link with -Wl,-force_load / allow multiple (platform-specific) === +multi_te_first_built=1 +te_interp=3 err=0 +run_exit=0 +multi_other_first_built=1 +COLLISION_OTHER_next_token_CALLED +te_interp=nan err=1 +run_exit=0 + +=== next_token dynamic: shared tinyexpr + shared other === +dyn_link_te_only_exit=0 +-- run without preload -- +te_interp=3 err=0 +-- run with LD_PRELOAD=libother_next.so -- +COLLISION_OTHER_next_token_CALLED +te_interp=nan err=1 +ld_preload_exit=0 + +=== locale decimals (needs generated locale or system de_DE) === +-- try de_DE.UTF-8 -- +setlocale(de_DE.UTF-8)=de_DE.UTF-8 +expr=1.5 err=1 value=nan +expr=.5 err=1 value=nan +expr=1,5 err=0 value=1,5 +expr=2.25+3.5 err=1 value=nan +locale_exit=0 +-- try de_DE -- +setlocale(de_DE)=(null) +locale_exit=2 +-- try fr_FR.UTF-8 -- +setlocale(fr_FR.UTF-8)=fr_FR.UTF-8 +expr=1.5 err=1 value=nan +expr=.5 err=1 value=nan +expr=1,5 err=0 value=1,5 +expr=2.25+3.5 err=1 value=nan +locale_exit=0 +-- try C -- +setlocale(C)=C +expr=1.5 err=0 value=1.5 +expr=.5 err=0 value=0.5 +expr=1,5 err=0 value=5 +expr=2.25+3.5 err=0 value=5.75 +locale_exit=0 +-- env LC_ALL=de_DE.UTF-8 without setlocale in process -- +setlocale(C)=C +expr=1.5 err=0 value=1.5 +expr=.5 err=0 value=0.5 +expr=1,5 err=0 value=5 +expr=2.25+3.5 err=0 value=5.75 + +=== DONE === diff --git a/docs/evidence/repro/run-log-linux-signed-char.txt b/docs/evidence/repro/run-log-linux-signed-char.txt new file mode 100644 index 0000000..0f7045e --- /dev/null +++ b/docs/evidence/repro/run-log-linux-signed-char.txt @@ -0,0 +1,8 @@ +debconf: delaying package configuration, since apt-utils is not installed +=== forced signed-char ctype trap === +ctype_trap: isalpha(-128) +bash: line 10: 1139 Aborted /tmp/ctype_signed +trap_exit=134 +=== CHAR_MIN with -fsigned-char === +CHAR_MIN=-128 CHAR_MAX=127 buf0=-128 +te_interp=nan err=1 diff --git a/docs/evidence/repro/run-log.txt b/docs/evidence/repro/run-log.txt new file mode 100644 index 0000000..ebd561d --- /dev/null +++ b/docs/evidence/repro/run-log.txt @@ -0,0 +1,86 @@ +=== host === +Linux c95e94aea731 6.12.67-linuxkit #1 SMP Sun Jan 25 02:26:28 UTC 2026 aarch64 aarch64 aarch64 GNU/Linux +cc=cc (Ubuntu 13.3.0-6ubuntu2~24.04.1) 13.3.0 +utc=2026-08-02T10:28:56Z +docs/evidence/repro/run-all.sh: line 16: git: command not found +git= +8192 + +=== CHAR signedness / ctype highbit (plain) === +CHAR_MIN=0 CHAR_MAX=255 buf0=128 +te_interp=nan err=1 + +=== ctype highbit with trapping header === +CHAR_MIN=0 CHAR_MAX=255 buf0=128 +te_interp=nan err=1 +trap_exit=0 + +=== fac/NaN probe === +expr=0/0 err=0 value=nan bits=0x7ff8000000000000 +expr=fac(0/0) err=0 value=1 bits=0x3ff0000000000000 +expr=fac((0/0)) err=0 value=1 bits=0x3ff0000000000000 +expr=fac(ncr(0,1)) err=0 value=1 bits=0x3ff0000000000000 +expr=ncr(0,1) err=0 value=nan bits=0x7ff8000000000000 +expr=fac(21) err=0 value=inf bits=0x7ff0000000000000 + +=== nest overflow binary search (interp) === +mode=interp last_ok=17470 first_crash=17471 +mode=compile last_ok=17468 first_crash=17469 + +=== next_token static link (expect duplicate symbol) === +static_link_exit=1 +/usr/bin/ld: /src/docs/evidence/repro/next_token_other.o: in function `next_token': +next_token_other.c:(.text+0x0): multiple definition of `next_token'; /src/docs/evidence/repro/tinyexpr.o:tinyexpr.c:(.text+0x580): first defined here +collect2: error: ld returned 1 exit status + +=== next_token: link with -Wl,-force_load / allow multiple (platform-specific) === +multi_te_first_built=1 +te_interp=3 err=0 +run_exit=0 +multi_other_first_built=1 +COLLISION_OTHER_next_token_CALLED +te_interp=nan err=1 +run_exit=0 + +=== next_token dynamic: shared tinyexpr + shared other === +dyn_link_te_only_exit=0 +-- run without preload -- +te_interp=3 err=0 +-- run with LD_PRELOAD=libother_next.so -- +COLLISION_OTHER_next_token_CALLED +te_interp=nan err=1 +ld_preload_exit=0 + +=== locale decimals (needs generated locale or system de_DE) === +-- try de_DE.UTF-8 -- +setlocale(de_DE.UTF-8)=de_DE.UTF-8 +expr=1.5 err=1 value=nan +expr=.5 err=1 value=nan +expr=1,5 err=0 value=1,5 +expr=2.25+3.5 err=1 value=nan +locale_exit=0 +-- try de_DE -- +setlocale(de_DE)=(null) +locale_exit=2 +-- try fr_FR.UTF-8 -- +setlocale(fr_FR.UTF-8)=fr_FR.UTF-8 +expr=1.5 err=1 value=nan +expr=.5 err=1 value=nan +expr=1,5 err=0 value=1,5 +expr=2.25+3.5 err=1 value=nan +locale_exit=0 +-- try C -- +setlocale(C)=C +expr=1.5 err=0 value=1.5 +expr=.5 err=0 value=0.5 +expr=1,5 err=0 value=5 +expr=2.25+3.5 err=0 value=5.75 +locale_exit=0 +-- env LC_ALL=de_DE.UTF-8 without setlocale in process -- +setlocale(C)=C +expr=1.5 err=0 value=1.5 +expr=.5 err=0 value=0.5 +expr=1,5 err=0 value=5 +expr=2.25+3.5 err=0 value=5.75 + +=== DONE === diff --git a/docs/evidence/upstream-issues.md b/docs/evidence/upstream-issues.md new file mode 100644 index 0000000..1d4fa9f --- /dev/null +++ b/docs/evidence/upstream-issues.md @@ -0,0 +1,190 @@ +# Upstream issue survey: codeplea/tinyexpr + +Survey date (UTC): 2026-08-02. Tool: `gh issue/pr list --state all`. +Pinned commit for this port: `4a7456e2eab88b4c76053c1c4157639ccb930e2b`. + +Totals: **88** issues (27 open, 61 closed); **52** PRs ({'CLOSED': 15, 'OPEN': 12, 'MERGED': 25}). + +## Issues relevant to our findings + +| # | State | Comments | Filed | Closed | Title | Fixed on pin? | +| --- | --- | --- | --- | --- | --- | --- | +| #39 | CLOSED | 2 | 2018-07-19 | 2018-07-20 | Nested expressions / Passing sub-expressions as variables | n/a | +| #62 | OPEN | 5 | 2020-06-25 | - | nested ncr() calls can be very slow (potential denial of service) | no (still open / unpatched at pin) | +| #78 | CLOSED | 6 | 2021-05-23 | 2021-05-24 | Potential Security Issue | repl only; library N/A | +| #85 | CLOSED | 6 | 2021-11-18 | 2021-11-26 | When evaluate simple expression with only one variable, nan is returned over 150 | n/a | +| #105 | OPEN | 2 | 2023-08-29 | - | Importing into gcc gives "error: array subscript has type 'char'" | no (still open / unpatched at pin) | +| #130 | OPEN | 0 | 2025-10-21 | - | Add support to static memory allocation | no (still open / unpatched at pin) | +| #131 | CLOSED | 0 | 2025-12-06 | 2025-12-12 | Possible null pointer dereference due to unchecked malloc | yes (PR #132 merged into pin) | +| #133 | OPEN | 2 | 2025-12-29 | - | Undefined behavior when converting NaN to unsigned int in tinyexpr | no (still open / unpatched at pin) | +| #134 | CLOSED | 4 | 2026-02-02 | 2026-02-02 | [Bug] Stack Overflow due to Deep Recursion in `tinyexpr` Parser | no (closed as acknowledgement; same class as #136) | +| #136 | OPEN | 0 | 2026-02-16 | - | Security: Stack overflow via deeply nested expressions (CWE-674) | no (still open / unpatched at pin) | + +## Novelty check for leads in this submission + +- `next_token` external linkage: **no matching issue title** in the full list. +- Locale / `LC_NUMERIC` / `strtod` decimal-point mismatch: **no matching issue title**. +- Signed `char` / `isalpha`/`isdigit`: already **#105** (OPEN since 2023-08-29; maintainer acknowledged). +- Deep nesting stack overflow: **#136** (OPEN, 0 comments, filed 2026-02-16) and earlier **#134** (CLOSED). +- NaN to unsigned / slow `ncr`: **#133**, **#62** (OPEN). +- Unchecked malloc NULL: **#131** CLOSED via merged **PR #132** (in our pin). + +## Issue #136 current state (confirmed this session) + +- URL: https://github.com/codeplea/tinyexpr/issues/136 +- State: OPEN +- Comments: 0 +- Filed: 2026-02-16 +- Labels: none +- Unpatched on pin `4a7456e2` (no depth guard in `tinyexpr.c`). + +## Full issue index (number, state, comments, filed, title) + +| # | State | Comments | Filed | Title | +| --- | --- | --- | --- | --- | +| #2 | CLOSED | 12 | 2016-02-23 | power | +| #4 | CLOSED | 14 | 2016-02-26 | Benchmarking | +| #5 | CLOSED | 5 | 2016-03-04 | Comparison with muparser | +| #6 | CLOSED | 2 | 2016-05-25 | Invalid parsing "2++-3" expression | +| #7 | CLOSED | 4 | 2016-05-25 | Compilation error with Visual studio 2012 | +| #8 | CLOSED | 8 | 2016-05-29 | Parse expression, get variable list | +| #10 | CLOSED | 17 | 2016-08-22 | Addition to benchmark suite | +| #12 | CLOSED | 4 | 2016-11-21 | does not support variables with underscore | +| #13 | CLOSED | 12 | 2016-12-05 | Using uint64_t instead of double | +| #14 | CLOSED | 7 | 2017-01-31 | Add support for factorials | +| #15 | CLOSED | 8 | 2017-01-31 | Add support for combinations and permutations | +| #20 | CLOSED | 4 | 2017-03-24 | reason to limit variable names? | +| #21 | CLOSED | 2 | 2017-03-26 | Compiler warning - fix | +| #22 | CLOSED | 10 | 2017-03-27 | Undefined reference to 'fabs' on arduino ide 1.8 | +| #23 | CLOSED | 3 | 2017-04-03 | Is there a chance to compile it with C++? | +| #27 | CLOSED | 9 | 2017-04-16 | Missing CMake build system support | +| #29 | OPEN | 10 | 2017-06-30 | Comparisons and logical operators | +| #30 | CLOSED | 5 | 2017-09-24 | Tinyexpr fully libre and GNU ? | +| #31 | CLOSED | 3 | 2017-11-09 | Question regarding te_free() | +| #32 | CLOSED | 4 | 2017-11-11 | Is it possible to use array as variable? | +| #33 | CLOSED | 2 | 2017-11-13 | Adding the random() function? | +| #34 | CLOSED | 7 | 2018-01-20 | "initializer is not a constant" error in Visual Studio. | +| #37 | CLOSED | 3 | 2018-05-23 | Implicit multiplication? | +| #39 | CLOSED | 2 | 2018-07-19 | Nested expressions / Passing sub-expressions as variables | +| #40 | CLOSED | 6 | 2018-08-22 | How to use with inequalities ? | +| #41 | CLOSED | 2 | 2018-08-26 | Are upppercase letters in variables and functions supported? | +| #42 | CLOSED | 2 | 2018-09-10 | Software version and tagged release? | +| #43 | CLOSED | 8 | 2018-09-13 | no variable assigning? | +| #44 | CLOSED | 2 | 2018-09-17 | Compiling with GCC < 4.6 | +| #45 | CLOSED | 5 | 2018-09-19 | Undefined reference to `te_interp` | +| #46 | CLOSED | 1 | 2018-10-03 | Cannot use with trim function | +| #48 | CLOSED | 6 | 2019-01-07 | Problem when having more than 2 variables and some not used | +| #49 | CLOSED | 2 | 2019-01-11 | "fatal error LNK1561: entry point must be defined" When using in a C++ library | +| #50 | CLOSED | 10 | 2019-02-04 | Bytecode interpreter? | +| #52 | OPEN | 7 | 2019-07-03 | (-1)^0 returns -1 instead of 1 | +| #53 | CLOSED | 0 | 2019-08-27 | Compiler warnings. | +| #56 | CLOSED | 7 | 2019-09-09 | Out of memory check and static memory implementation | +| #58 | CLOSED | 4 | 2019-10-24 | Cant use it in C++ | +| #59 | CLOSED | 3 | 2019-11-02 | how to use this project in mysql udf? | +| #61 | CLOSED | 8 | 2020-01-05 | Invalid conversion compiler warnings | +| #62 | OPEN | 5 | 2020-06-25 | nested ncr() calls can be very slow (potential denial of service) | +| #63 | OPEN | 6 | 2020-09-07 | Compilation under Visual Studio 2010 | +| #72 | OPEN | 1 | 2021-01-03 | New function | +| #73 | OPEN | 5 | 2021-02-22 | Support for symbolic differentiation | +| #75 | OPEN | 0 | 2021-03-10 | Question about logic branch | +| #76 | OPEN | 2 | 2021-04-12 | complex number calculation | +| #77 | OPEN | 7 | 2021-04-15 | Compiling error : initializer is not a constant | +| #78 | CLOSED | 6 | 2021-05-23 | Potential Security Issue | +| #80 | OPEN | 0 | 2021-07-07 | get undeclared variables names | +| #81 | CLOSED | 4 | 2021-07-22 | Result is wayyyy off. | +| #84 | OPEN | 1 | 2021-09-20 | Support for bigger whole numbers (long long) | +| #85 | CLOSED | 6 | 2021-11-18 | When evaluate simple expression with only one variable, nan is returned over 150 | +| #86 | OPEN | 0 | 2021-11-24 | warning: ISO C forbids conversion of function pointer to object pointer type | +| #87 | OPEN | 1 | 2021-11-30 | array bounds warning with gcc-11 | +| #89 | CLOSED | 2 | 2022-01-20 | codeblocks compiler issue? | +| #90 | CLOSED | 3 | 2022-02-07 | Weird behavior when calling te_free | +| #92 | OPEN | 6 | 2022-03-15 | Feature request: Add define to change return value from double to long double | +| #93 | CLOSED | 2 | 2022-03-28 | Usage with different types | +| #95 | CLOSED | 9 | 2022-10-20 | Feature Request: Implement ternary evalutation | +| #97 | CLOSED | 7 | 2023-03-23 | Error eval expression when initializing te_variable by individual assignment | +| #98 | CLOSED | 2 | 2023-03-23 | Function to determine the number of variables in an expression | +| #99 | CLOSED | 1 | 2023-03-23 | Possible memory leak in te_compile | +| #100 | CLOSED | 0 | 2023-03-24 | Feature request: add logical operators. | +| #101 | OPEN | 4 | 2023-03-24 | Feature request : right and left shift operations | +| #102 | CLOSED | 4 | 2023-05-04 | Look ahead Variables | +| #103 | CLOSED | 3 | 2023-05-26 | Custom infix operators | +| #104 | OPEN | 2 | 2023-05-27 | How about support time expression? | +| #105 | OPEN | 2 | 2023-08-29 | Importing into gcc gives "error: array subscript has type 'char'" | +| #107 | CLOSED | 2 | 2023-12-30 | The website says that this is a header-only library, but the actual code isn't | +| #108 | CLOSED | 5 | 2024-01-12 | Extraction of tokens | +| #110 | CLOSED | 6 | 2024-03-31 | Invalid conversion for custom function in Arduino | +| #111 | CLOSED | 1 | 2024-04-05 | Redundant null-pointer check in `te_interp` | +| #112 | CLOSED | 4 | 2024-04-12 | Segfault when two variables follow each other | +| #115 | OPEN | 0 | 2024-06-08 | Merging logic branch with ifdefs | +| #119 | OPEN | 0 | 2024-10-22 | tinyexpr compilation fails on keil-mdk | +| #120 | CLOSED | 0 | 2024-12-06 | How the expression will work? | +| #121 | CLOSED | 2 | 2024-12-25 | Default Exponentiation Behavior | +| #122 | OPEN | 0 | 2025-01-11 | Feature Request: Function to copy te_expr | +| #123 | OPEN | 0 | 2025-03-02 | Logical vs arithmetic expressions | +| #124 | OPEN | 1 | 2025-03-25 | expression: 7.99+3.00-9.00+7.01/(7.79+1.68/(5.00-5.00)) evaluates to 1.99 | +| #125 | OPEN | 2 | 2025-03-29 | GCC v14.2 warnings | +| #128 | CLOSED | 1 | 2025-07-27 | On GCC 15 in 32-bit ARM environment te_eval outputs garbage | +| #130 | OPEN | 0 | 2025-10-21 | Add support to static memory allocation | +| #131 | CLOSED | 0 | 2025-12-06 | Possible null pointer dereference due to unchecked malloc | +| #133 | OPEN | 2 | 2025-12-29 | Undefined behavior when converting NaN to unsigned int in tinyexpr | +| #134 | CLOSED | 4 | 2026-02-02 | [Bug] Stack Overflow due to Deep Recursion in `tinyexpr` Parser | +| #136 | OPEN | 0 | 2026-02-16 | Security: Stack overflow via deeply nested expressions (CWE-674) | +| #139 | OPEN | 0 | 2026-06-24 | tinyexpr.c permissions are 0755, should be 0644 | + +## Full PR index + +| # | State | Comments | Filed | Title | +| --- | --- | --- | --- | --- | +| #1 | MERGED | 2 | 2016-02-23 | Optimized find_function | +| #3 | MERGED | 27 | 2016-02-24 | Added arity greater than 2, and test cases. | +| #9 | MERGED | 0 | 2016-06-02 | Minor optimization | +| #11 | MERGED | 0 | 2016-08-23 | Pow from right | +| #16 | MERGED | 1 | 2017-02-08 | Avoid missing initializer warnings | +| #17 | CLOSED | 0 | 2017-03-15 | add fac and infs/combinatoric test | +| #18 | MERGED | 3 | 2017-03-15 | add fac and infs/combinatoric test | +| #19 | MERGED | 9 | 2017-03-17 | add ncr and npr | +| #24 | MERGED | 0 | 2017-04-03 | Comb | +| #25 | MERGED | 1 | 2017-04-14 | Fix delete command and add .PHONY target in Makefile | +| #26 | CLOSED | 1 | 2017-04-16 | Add CMake build system support | +| #28 | CLOSED | 1 | 2017-04-21 | add te_astree and te_inner_func and TE_INFIX_PER and TE_INFIX_FAC | +| #35 | MERGED | 3 | 2018-04-08 | Avoid fall through warnings | +| #36 | MERGED | 0 | 2018-05-08 | Fixed warning when compiling with strict prototypes | +| #38 | MERGED | 1 | 2018-07-10 | Logical operators | +| #47 | OPEN | 2 | 2018-10-26 | C99 | +| #51 | MERGED | 0 | 2019-04-26 | CLang: macro name is a reserved identifier | +| #54 | CLOSED | 10 | 2019-08-27 | Fixed compiler warning. | +| #55 | CLOSED | 1 | 2019-09-05 | added gitignore | +| #57 | CLOSED | 1 | 2019-10-08 | Reworked memory allocation and compiler warnings | +| #60 | CLOSED | 2 | 2019-11-07 | Add `min` and `max` function | +| #64 | MERGED | 0 | 2020-10-02 | fix header guard comment | +| #65 | MERGED | 0 | 2020-10-02 | Temp | +| #66 | MERGED | 1 | 2020-10-10 | docs: fix simple typo, thrid -> third | +| #67 | MERGED | 5 | 2020-10-12 | Add a simple REPL | +| #68 | MERGED | 8 | 2020-10-13 | Handle malloc failures | +| #69 | CLOSED | 0 | 2020-10-18 | Improved type safety and made code c++20 conformant. | +| #70 | MERGED | 0 | 2020-10-19 | update readme to c99 | +| #71 | OPEN | 1 | 2020-11-02 | C11 and C++20 | +| #74 | MERGED | 3 | 2021-03-03 | Accepted upper case letters | +| #79 | MERGED | 0 | 2021-06-17 | Adding SPDX license identifiers | +| #82 | OPEN | 1 | 2021-07-26 | Nan checks for ncr and fac | +| #83 | MERGED | 1 | 2021-08-16 | Fixed for the old issue #34 | +| #88 | OPEN | 1 | 2021-11-30 | Use flexible array notation for te_expr | +| #91 | CLOSED | 2 | 2022-02-18 | Typo Fix | +| #94 | OPEN | 0 | 2022-09-15 | Add vcpkg installation instructions | +| #96 | MERGED | 1 | 2022-11-20 | Fix typos in readme | +| #106 | CLOSED | 0 | 2023-11-13 | Merge branch 'codeplea:master' into master | +| #109 | MERGED | 1 | 2024-03-14 | Document hexadecimal support | +| #113 | CLOSED | 10 | 2024-04-22 | Fix compilation warnings when using MSVC on x64 architecture | +| #114 | OPEN | 0 | 2024-05-09 | Add clusterfuzzlite workflow | +| #116 | CLOSED | 0 | 2024-08-19 | Logic | +| #117 | OPEN | 0 | 2024-09-11 | implement ternary | +| #118 | OPEN | 0 | 2024-09-13 | python extension for tinyexpr | +| #126 | OPEN | 2 | 2025-06-04 | Extending `te_variable` functions | +| #127 | CLOSED | 0 | 2025-06-20 | added unsigned char casting for errors in esp-idf building system | +| #129 | OPEN | 0 | 2025-08-16 | add support for utf8 expressions | +| #132 | MERGED | 0 | 2025-12-06 | Fix: possible null pointer dereference due to unchecked malloc | +| #135 | CLOSED | 0 | 2026-02-11 | Fix undefined behavior in ncr() when casting NaN/Inf to unsigned int | +| #137 | OPEN | 0 | 2026-06-04 | Fix size calculation in new_expr function | +| #138 | OPEN | 0 | 2026-06-04 | ubsan fixes | +| #140 | CLOSED | 1 | 2026-07-08 | fix: remove executable bit from tinyexpr.c | + From 80c72e6cf1832c4a9af996352ff1a35c2c386ff1 Mon Sep 17 00:00:00 2001 From: kavyabhand Date: Sun, 2 Aug 2026 16:04:37 +0530 Subject: [PATCH 2/3] docs: rewrite findings with verdicts and upstream URLs --- .../evidence/fragments/upstream-findings.json | 60 ++++ docs/findings.md | 317 ++++++++++++------ docs/journal.md | 20 ++ 3 files changed, 293 insertions(+), 104 deletions(-) create mode 100644 docs/evidence/fragments/upstream-findings.json diff --git a/docs/evidence/fragments/upstream-findings.json b/docs/evidence/fragments/upstream-findings.json new file mode 100644 index 0000000..e2fc896 --- /dev/null +++ b/docs/evidence/fragments/upstream-findings.json @@ -0,0 +1,60 @@ +{ + "schema_version": 1, + "command": "gh issue/pr create and comment; docs/evidence/repro/run-all.sh", + "git_sha": "609f03618213b8ab925f07d98785808c1907c672", + "host": "Darwin Kavyas-MacBook-Air-2.local 24.1.0 arm64; Ubuntu 24.04 aarch64 Docker", + "utc": "2026-08-02T10:35:00Z", + "allocator_misuse_track_g": "not_satisfied_negative_result", + "confirmed_existing": [ + { + "id": 136, + "title": "Security: Stack overflow via deeply nested expressions (CWE-674)", + "state": "OPEN", + "our_action": "independent_reproduction_comment", + "url": "https://github.com/codeplea/tinyexpr/issues/136", + "comment": "https://github.com/codeplea/tinyexpr/issues/136#issuecomment-5157204950" + }, + { + "id": 105, + "title": "Importing into gcc gives error: array subscript has type char", + "state": "OPEN", + "our_action": "confirmation_comment_and_fix_pr", + "url": "https://github.com/codeplea/tinyexpr/issues/105", + "comment": "https://github.com/codeplea/tinyexpr/issues/105#issuecomment-5157205043", + "pr": "https://github.com/codeplea/tinyexpr/pull/144" + } + ], + "newly_filed": [ + { + "id": 141, + "title": "next_token has external linkage and can silently bind the wrong function", + "state": "OPEN", + "url": "https://github.com/codeplea/tinyexpr/issues/141", + "pr": "https://github.com/codeplea/tinyexpr/pull/143", + "severity": "medium" + }, + { + "id": 142, + "title": "Decimal literals depend on LC_NUMERIC via strtod after setlocale", + "state": "OPEN", + "url": "https://github.com/codeplea/tinyexpr/issues/142", + "severity": "medium", + "hang_reproduced": false + } + ], + "pull_requests": [ + "https://github.com/codeplea/tinyexpr/pull/143", + "https://github.com/codeplea/tinyexpr/pull/144" + ], + "counts": { + "new_issues": 2, + "confirmation_comments": 2, + "fix_prs": 2 + }, + "nest_thresholds_measured": { + "darwin_arm64_interp_first_crash": 23764, + "linux_docker_aarch64_interp_first_crash": 17471, + "zig_abi_darwin_paren_last_ok": 340, + "zig_abi_darwin_paren_first_fail": 341 + } +} diff --git a/docs/findings.md b/docs/findings.md index d6d7730..602bbd4 100644 --- a/docs/findings.md +++ b/docs/findings.md @@ -1,139 +1,248 @@ # Findings -Measured verdicts and reproducers. Unverified leads are marked as such. +Authoritative record of latent-bug work against pin +`4a7456e2eab88b4c76053c1c4157639ccb930e2b`. Every number below was measured +on this machine (Kavyas-MacBook-Air-2 / Darwin arm64 and Ubuntu 24.04 +aarch64 Docker) on 2026-08-02 unless noted. Upstream survey: +`docs/evidence/upstream-issues.md`. Reproducers: `docs/evidence/repro/`. -## Independent reproduction: upstream issue #136 +**Equivalence scope (one sentence):** bit-exact agreement between the C +original and the Zig C ABI is claimed for expressions evaluated under the +C locale (`LC_NUMERIC`), at nesting depths below the port bound, on inputs +where the C original does not invoke undefined behaviour (including +signed-`char` ctype on high-bit bytes and stack overflow on deep nests). -Stack overflow on deeply nested expressions via `te_interp` / `te_compile`. -Filed upstream 2026-02-16 as issue #136 (CWE-674). Still open, unpatched -at our pin. Not claimed as novel. Measured thresholds: see STATUS / journal. +Track G allocator-misuse exit criterion: **not satisfied** (honest +negative). Allocation-failure injection (other machine): 271 ordinals, 0 +crashes either side. See `docs/evidence/fragments/allocfault.json`. + +--- -## Lead A: non-static `next_token` (external linkage) +## Findings by consequence -Hypothesis: `next_token` at `third_party/tinyexpr-c/tinyexpr.c:239` is -declared `void next_token(state *s)` with no `static`, while every other -internal helper in the file is `static`, and it is absent from -`tinyexpr.h`. Under C it therefore has external linkage under a maximally -generic name, which can collide with another translation unit's -`next_token`. +### 1. Stack overflow on deep nesting (CWE-674) - HIGH -Experiment: build a program that links tinyexpr together with another -translation unit that defines its own non-static `next_token`. Record what -happens at static link and at dynamic link. Check whether this is already -reported upstream before claiming novelty. +**One-line summary:** Unbounded recursive descent SIGSEGVs on nested +parentheses; confirmed unpatched upstream issue #136. -Status: unverified (owned by the second machine's upstream engagement). +| Field | Value | +| --- | --- | +| Severity | High (DoS / crash on untrusted input) | +| C location | Recursive `base`/`power`/`factor`/`term`/`expr`/`list` in `tinyexpr.c` (no depth counter) | +| Already filed? | Yes: [#136](https://github.com/codeplea/tinyexpr/issues/136) OPEN since 2026-02-16, **0 comments** before our note; related [#134](https://github.com/codeplea/tinyexpr/issues/134) CLOSED | +| Our engagement | Independent reproduction comment: https://github.com/codeplea/tinyexpr/issues/136#issuecomment-5157204950 | +| Novelty | **Not novel.** Independent reproduction of a CWE-tagged report that sat unanswered. | -## Lead B: `isalpha` / `isdigit` on signed `char` (C99 7.4p1) +**Measured thresholds (this session):** -Hypothesis: lines 255 and 258 pass `s->next[0]` (type `char`) to -`isalpha` and `isdigit`. Where `char` is signed, any byte at or above -0x80 becomes a negative `int`. Passing a negative value other than `EOF` -to those functions is undefined behaviour under C99 7.4 paragraph 1. +| Host | `ulimit -s` | `te_interp` last OK / first crash | `te_compile` last OK / first crash | +| --- | --- | --- | --- | +| Darwin 24.1.0 Apple M1 / Apple clang 16 | 8176 | 23763 / **23764** | 23763 / **23764** | +| Ubuntu 24.04 aarch64 Docker / gcc 13.3 | 8192 | 17470 / **17471** | 17468 / **17469** | -### Measured verdict (2026-08-02, elmengo / glibc 2.39) +Earlier WSL x86_64 measurement (other machine): interp crash at 20144. +Host-dependent absolute depth; same failure mode. -Command: `bash scripts/run-buglead-ctype.sh` +**Minimal reproducer:** `docs/evidence/repro/nest_overflow.c` (binary search +via `fork`). Raw logs: `docs/evidence/repro/run-log-darwin.txt`, +`run-log-linux-docker.txt`. -1. Plain `te_interp` on every single-byte string `0x80..0xFF`: all 128 - rejected (`interp_ok=0`, `interp_rejected=128`). No crash. -2. Same under UBSan (`-fsanitize=undefined -fno-sanitize-recover=undefined`): - exit 0, **no** UBSan diagnostic. glibc's ctype table padding masks the - C99 violation on this host. -3. Same with a trapping ctype wrapper via `-include` that `abort`s on - `c < 0 || c > 255` (cannot edit `third_party/`): **aborts on first byte - `0x80`** with stderr `ctype_trap: isalpha(-128)`. The call is reached from - `next_token` before the probe's own direct ctype calls return, so the - original's `isalpha(s->next[0])` path is confirmed. +**Port behaviour:** parse nesting is bounded (`-Dmax-nesting`, default +2048 internal steps). On this host's Zig ABI build, nested-paren +`te_compile` last OK depth **340**, first fail **341** (`err=-1`, NULL), +with no signal. Intentional divergence (D008); in the divergence table. + +--- -Raw fragment: `docs/evidence/fragments/buglead-ctype.json`. -Log: `docs/evidence/buglead-ctype.log`. +### 2. Accidental external `next_token` - MEDIUM + +**One-line summary:** Internal lexer is a GLOBAL `next_token` and can +silently bind the wrong function under multi-def or `LD_PRELOAD`. -Minimal reproducer for upstream: +| Field | Value | +| --- | --- | +| Severity | Medium (silent wrong parse when a colliding symbol wins) | +| C location | `tinyexpr.c:239` `void next_token(state *s)` (no `static`); not in `tinyexpr.h` | +| Already filed? | **No** prior issue in the full survey | +| Upstream issue | [#141](https://github.com/codeplea/tinyexpr/issues/141) | +| Upstream PR | [#143](https://github.com/codeplea/tinyexpr/pull/143) (`static void next_token`) | +| Novelty | **Yes** (first filing we found) | -```bash -# Build tinyexpr with a trapping isalpha (illustrates the UB surface): -# any byte >= 0x80 as the first character of the expression reaches -# isalpha with a negative argument on signed-char platforms. -printf '\x80' | xxd # input is a single high-bit byte -# te_interp("", &err) -> isalpha(-128) under signed char -``` +**Measured (Ubuntu 24.04 aarch64 Docker / gcc 13.3):** + +1. Ordinary static link with a second TU defining `next_token`: **link error** + (`multiple definition of next_token`). +2. Same with `-Wl,--allow-multiple-definition`, other object first: link + succeeds; runtime prints `COLLISION_OTHER_next_token_CALLED`; + `te_interp("1+2")` returns `nan` / `err=1` instead of `3`. +3. `LD_PRELOAD` of a shared object exporting `next_token`: same wrong call + and failed parse. + +Darwin / ld64: ordinary static link also errors (`duplicate symbol +'_next_token'`). Multi-def / preload interposition not demonstrated on +ld64 in this session. + +Milder sibling: `te_free_parameters` is also GLOBAL and absent from the +header (less generic name). + +**Reproducers:** `docs/evidence/repro/next_token_*.c`. Log excerpt in +`run-log-linux-docker.txt`. + +**Port behaviour:** exports the measured GLOBAL set for ABI parity, +including `next_token` (D010). Consumers of the Zig `.a` can still collide +the same way; the port did not invent the export. Fixing upstream `static` +is the right long-term fix; the Zig native API does not expose a C +`next_token` symbol to Zig callers. + +--- + +### 3. Signed `char` passed to ctype - MEDIUM (UB surface) + +**One-line summary:** High-bit bytes reach `isalpha`/`isdigit` with a +negative argument on signed-`char` platforms (C99 7.4p1). + +| Field | Value | +| --- | --- | +| Severity | Medium (UB; often masked by libc, fatal on strict ctype) | +| C location | `tinyexpr.c:255`, `:258` | +| Already filed? | Yes: [#105](https://github.com/codeplea/tinyexpr/issues/105) OPEN since 2023-08-29; maintainer said they would fix | +| Our engagement | Confirmation comment: https://github.com/codeplea/tinyexpr/issues/105#issuecomment-5157205043 | +| Upstream PR | [#144](https://github.com/codeplea/tinyexpr/pull/144) (`(unsigned char)` casts) | +| Novelty | **Not novel.** Independent confirmation of #105. | + +**Measured:** + +| Build | `CHAR_MIN` | byte `0x80` | Trapping ctype | +| --- | --- | --- | --- | +| Darwin arm64 (default signed `char`) | -128 | `isalpha(-128)` path | abort `ctype_trap: isalpha(-128)` (exit 134) | +| Ubuntu aarch64 Docker (default unsigned `char`) | 0 | value 128, in range | no abort | +| Ubuntu aarch64 + `-fsigned-char` | -128 | same as Darwin | abort | + +Stock reject without trap: `te_interp` returns nan/`err=1` (no crash). + +**Reproducers:** `ctype_highbit.c`, `ctype_trap.h`. Logs: +`run-log-darwin.txt`, `run-log-linux-signed-char.txt`. + +**Port behaviour:** Zig lexer uses byte-oriented checks / `strtod` rather +than C `isalpha` on a plain `char`, so this specific UB surface is not +copied. Equivalence for identifier starts is scoped away from high-bit +inputs that invoke C UB. + +--- -Status: **confirmed UB surface**. Not a visible crash on stock glibc; a -conforming ctype implementation may trap. Suitable for an upstream issue -citing C99 7.4p1 and recommending `(unsigned char)s->next[0]`. +### 4. Locale-dependent decimal parsing - MEDIUM (correctness) + +**One-line summary:** After `setlocale` to a comma-decimal locale, `1.5` +fails to parse because `strtod` follows `LC_NUMERIC`. + +| Field | Value | +| --- | --- | +| Severity | Medium (correctness; needs `setlocale`, not env alone) | +| C location | `tinyexpr.c:250-252` | +| Already filed? | **No** prior issue in the survey | +| Upstream issue | [#142](https://github.com/codeplea/tinyexpr/issues/142) | +| Novelty | **Yes** (first filing we found) | +| Hang / DoS? | **Disproved** on Darwin and Ubuntu Docker (see below) | + +**Measured** after `setlocale(LC_ALL, "de_DE.UTF-8")` and +`fr_FR.UTF-8`: + +| Expression | Result | +| --- | --- | +| `1.5` | `err=1`, nan | +| `.5` | `err=1`, completes (no hang) | +| `1,5` | `err=0`, value 1.5 | +| `2.25+3.5` | `err=1` | + +Under `setlocale(..., "C")`, `1.5` and `2.25+3.5` succeed. Env-only +`LC_ALL=de_DE.UTF-8` without `setlocale` stays C locale (smoke never calls +`setlocale`). + +**Reproducer:** `locale_decimals.c`. + +**Port behaviour:** also calls `libm.strtod` for bit-exact parity (D025). +Same locale sensitivity. Equivalence for decimal literals is scoped to the +C locale. Not a favourable divergence. + +--- + +### 5. `fac(NaN)` returns 1.0 - LOW (surprising but related to filed UB) + +**One-line summary:** `fac(0/0)` evaluates to `1.0` because NaN slips past +range checks and the `(unsigned)nan` conversion yields 0 on the hosts we +measured. + +| Field | Value | +| --- | --- | +| Severity | Low (wrong finite answer on NaN input; related to open #133 / #62) | +| C location | `fac` in `tinyexpr.c` (NaN comparisons false; cast to unsigned) | +| Already filed? | Related: [#133](https://github.com/codeplea/tinyexpr/issues/133), [#62](https://github.com/codeplea/tinyexpr/issues/62) | +| Novelty | Not filed as a separate issue; documented as differential evidence for existing NaN/unsigned threads | + +**Measured (Darwin and Ubuntu Docker C):** + +``` +fac(0/0) -> 1.0 bits 0x3ff0000000000000 +fac(ncr(0,1))-> 1.0 +fac(21) -> +inf +``` -## Lead C: `strtod` + `LC_NUMERIC` locale DoS +Zig ABI on Darwin this session also returned `1.0` for `fac(0/0)`. An +earlier WSL probe on the other machine reported Zig `+Inf` for the same +input; treat that as host/opt-mode dependent and do not over-claim a +stable divergence here. -Hypothesis: line 251 calls `strtod`, which respects `LC_NUMERIC`, but the -number branch is entered whenever the next character is a digit or `.`, -independent of locale. Under a comma-decimal locale, `strtod("1.5")` -should return 1.0 and stop at the `.`; on the next call `strtod(".5")` -should fail and leave `endptr` equal to the input, yet the code still -sets `TOK_NUMBER`, which means `s->next` may not advance. +**Reproducer:** `fac_nan.c`. -### Measured verdict (2026-08-02, elmengo) +--- -Command: `bash scripts/run-buglead-locale.sh` plus hang probes under -`de_DE.UTF-8` (generated via `localedef` into `.local/bugleads`, -`LOCPATH` set; `setlocale(LC_ALL, "de_DE.UTF-8")` succeeds). +## Hypotheses investigated and disproved -`strtod` under `de_DE.UTF-8` (direct probe): +### H1. `fac(21)` wraps `unsigned long` to a wrong finite -| Input | value | bytes advanced | -| --- | --- | --- | -| `1.5` | 1 | 1 (stops at `.`) | -| `1,5` | 1.5 | 3 | -| `.5` | 0 | **0** (endptr == start) | -| `1.` | 1 | 1 | +**Plan claimed** wrapping via unchecked multiply. **Measured:** on pin +`4a7456e2`, `fac(21)` returns `+inf` (bits `0x7ff0000000000000`) because of +the `ULONG_MAX / result` guard. True 21! is still finite. Disproved as +wraparound; kept as INF-sentinel behaviour (D006). -`te_interp` after `setlocale(LC_ALL, "de_DE.UTF-8")` (timeout 3s each): +### H2. `ncr`/`npr` algorithmic-complexity DoS via huge loop bounds -| Expression | err | notes | -| --- | --- | --- | -| `1.5` | 1 | completes; value nan; **no hang** | -| `.5` | 1 | completes; **no hang** despite `strtod` advancing 0 | -| `1,5` | 0 | value 1.5 (printf shows `1,5` under the locale) | -| `2.25+3.5` | 1 | completes | +Large-n cases return INF quickly under the overflow guard on the other +machine's timings. Separately, open upstream #62 documents a real slow +path for **NaN** into `ncr` (`ncr(0/0,1)` etc.), which is a different +mechanism than the UINT_MAX iteration story. We did not treat H2 as a +novel finding. -Hang hypothesis for `te_interp`: **not reproduced**. When `strtod` fails to -advance on `.5`, the parser still returns a compile error (`err=1`) instead -of looping forever. The non-advancing token does not become a DoS on this -glibc under a 3s hard timeout. +### H3. Locale `strtod` non-advance causes an infinite lexer hang -Correctness bug under non-C `LC_NUMERIC`: **confirmed**. Expressions that -are valid in the C locale (`1.5`, `2.25+3.5`) fail to parse after -`setlocale` to `de_DE.UTF-8`. `1,5` succeeds. +Under `de_DE.UTF-8` / `fr_FR.UTF-8`, `te_interp(".5")` returned `err=1` +and completed on Darwin and Ubuntu Docker. Hang **not reproduced**. The +surviving locale finding is correctness (#142), not DoS. -Upstream `smoke` under `LC_ALL=de_DE.UTF-8` alone: exit 0. Smoke never -calls `setlocale`, so it stays in the C locale despite the environment -variable. Env-only is not a valid trigger; the program must call -`setlocale(LC_ALL, "")` or an explicit locale name. +### H4. Allocation-failure injection finds a C crash or leak (Track G) -### Zig port under the same locale +271 ordinals across 107 inputs: **0 crashes** either side; ledger +identical on `1+2*3`. Criterion **not_satisfied_negative_result**. +Upstream #131 (unchecked malloc) was already fixed in pin via PR #132. -The port's lexer calls `libm.strtod` (`src/lexer.zig`) for bit-exact -parity with the C original. Under the same `setlocale` + `de_DE.UTF-8` -probe, Zig ABI `te_interp` output matched C byte-for-byte on the tested -expressions. This is **not** a favourable divergence: both sides inherit -`strtod`'s locale behaviour. Equivalence claims that mention decimal -literals should be scoped to the C locale (or to `setlocale` never having -selected a comma-decimal `LC_NUMERIC`). +### H5. Lead B is a novel discovery -Raw fragment: `docs/evidence/fragments/buglead-locale.json`. -Log: `docs/evidence/buglead-locale.log`. +False. Upstream #105 already describes the ctype/`unsigned char` issue. +We confirmed and sent a fix PR (#144); we do not claim first discovery. -Status: **locale-dependent parse failure confirmed; DoS hang not -reproduced**. Upstream-ready with the `strtod` table and `setlocale` -requirement above. +--- -## Allocator-misuse (Track G exit criterion) +## Upstream engagement summary -See `docs/evidence/fragments/allocfault.json` and `allocfault-asan.json`. +| Action | URL | +| --- | --- | +| Comment on #136 (independent nest repro) | https://github.com/codeplea/tinyexpr/issues/136#issuecomment-5157204950 | +| Comment on #105 (ctype confirmation) | https://github.com/codeplea/tinyexpr/issues/105#issuecomment-5157205043 | +| New issue: `next_token` linkage | https://github.com/codeplea/tinyexpr/issues/141 | +| New issue: locale / `strtod` | https://github.com/codeplea/tinyexpr/issues/142 | +| PR: `static next_token` | https://github.com/codeplea/tinyexpr/pull/143 | +| PR: `(unsigned char)` ctype casts | https://github.com/codeplea/tinyexpr/pull/144 | -Plain LD_PRELOAD campaign: 107 inputs, 271 ordinals, 0 crashes either side, -20 OOM error-position divergences (C `-1` vs Zig offset), identical ledger on -`1+2*3` (11 events). ASan+shim campaign on C: same 271 ordinals, 0 crashes, -no LeakSanitizer findings in the run log; Zig compared via LD_PRELOAD (cannot -shim the Zig `.a`). Track G allocator-misuse criterion: -`not_satisfied_negative_result`. +Upstream response inside the hackathon window is unlikely. An unanswered +issue is still a filed issue. We opened two new issues and two minimal +PRs, plus two confirmation comments; we did not spam thin tickets. diff --git a/docs/journal.md b/docs/journal.md index 5817a7a..182fd9b 100644 --- a/docs/journal.md +++ b/docs/journal.md @@ -265,3 +265,23 @@ look satisfied in LICENSE, THIRD_PARTY.md, and LICENSE-NOTICE.txt; Full table: `docs/REPRODUCTION.md`. Gate experiments: `docs/evidence/gate-failure-proofs.md`. + +## 2026-08-02 Kavyas-MacBook-Air-2 bug-catcher-upstream + +Bug Catcher pass on the second machine. First we enumerated every upstream +issue and PR into `docs/evidence/upstream-issues.md` (88 issues, 52 PRs). +That survey killed one novelty claim immediately: Lead B (signed char / +ctype) is already #105. Issue #136 is still OPEN with zero comments and +unpatched on our pin. + +We re-measured everything here rather than copying the other machine's +numbers. Nest crash thresholds: Darwin interp 23764, Linux Docker aarch64 +17471. Zig ABI nested-paren fail at 341 on Darwin. `next_token` on Linux +with `--allow-multiple-definition` and the other object first silently +calls the wrong function (`te_interp("1+2")` -> nan). Locale: `de_DE` / +`fr_FR` after `setlocale` break `1.5`; hang hypothesis for `.5` did not +reproduce. Allocator-misuse criterion still not satisfied. + +Filed #141 and #142, commented on #136 and #105, opened PRs #143 and #144. +Rewrote `docs/findings.md` with a disproved-hypotheses section and put the +divergence table in the README with a one-sentence equivalence scope. From db413468c912648aca069389e8818abaf4e6edb5 Mon Sep 17 00:00:00 2001 From: kavyabhand Date: Sun, 2 Aug 2026 16:04:37 +0530 Subject: [PATCH 3/3] docs: complete divergence table and upstream scorecard fragment --- .gitignore | 11 ++++ DECISIONS.md | 30 +++++++++ README.md | 21 +++++++ docs/evidence/divergences.md | 53 ++++++++-------- docs/evidence/fragments/enumeration-full.json | 31 ++++++++++ .../fragments/fuzz-warmup-progress.json | 19 ++++++ docs/evidence/fragments/fuzz-warmup.json | 18 ++++++ docs/evidence/scorecard.json | 62 ++++++++++++++++++- 8 files changed, 218 insertions(+), 27 deletions(-) create mode 100644 docs/evidence/fragments/enumeration-full.json create mode 100644 docs/evidence/fragments/fuzz-warmup-progress.json create mode 100644 docs/evidence/fragments/fuzz-warmup.json diff --git a/.gitignore b/.gitignore index 8a8b80f..9a56316 100644 --- a/.gitignore +++ b/.gitignore @@ -69,3 +69,14 @@ docs/evidence/coverage-port.log # Layout probe host binaries layout-c layout-zig + +# Bug-repro build products (sources and run logs stay tracked) +docs/evidence/repro/ctype_highbit +docs/evidence/repro/ctype_highbit_trap +docs/evidence/repro/fac_nan +docs/evidence/repro/locale_decimals +docs/evidence/repro/nest_overflow +docs/evidence/repro/next_token_dyn +docs/evidence/repro/next_token_multi_other_first +docs/evidence/repro/next_token_multi_te_first +docs/evidence/repro/*.err diff --git a/DECISIONS.md b/DECISIONS.md index 1ac5249..666f50b 100644 --- a/DECISIONS.md +++ b/DECISIONS.md @@ -420,3 +420,33 @@ Entries that only restate a diff will be deleted. - Evidence: pre-fix empty-array unbound variable and `mapfile: command not found` on this Mac; suites then measured at 4930/4924/4934/4928 (`docs/REPRODUCTION.md`). + +## D033 - Divergence table and scoped equivalence claim + +- Constraint: judges need every intentional behavioural difference in one + place, and an equivalence claim that does not silently cover UB or locale + traps. +- Options: scatter notes across DECISIONS only; omit scope; one table plus one + sentence. +- Choice: add an intentional-divergences table to `README.md` and keep the + authoritative detail in `docs/findings.md`. Equivalence sentence: C locale, + below nesting bound, no C UB. +- Consequence: nest bound, optional fac flag, locale/`strtod`, and ctype UB + are explicit. Locale is shared behaviour, not a port win. +- Evidence: nest thresholds and locale tables measured 2026-08-02 + (`docs/evidence/repro/run-log-*.txt`); upstream #136/#105/#141/#142. + +## D034 - Upstream engagement for Bug Catcher + +- Constraint: bonus and Innovation credit require filing during the hackathon, + without false novelty or severity inflation. +- Options: claim #136 as ours; file many thin issues; file only what the + survey shows is unfiled and confirm the rest. +- Choice: comment on #136 and #105 with independent measurements; open #141 + (`next_token`) and #142 (locale); open minimal PRs #143 and #144. Record + allocator criterion as not satisfied. +- Consequence: two new issues, two PRs, two confirmation comments. Track G + allocator-misuse remains an honest negative. +- Evidence: URLs in `docs/findings.md`; survey in + `docs/evidence/upstream-issues.md`; fragment + `docs/evidence/fragments/upstream-findings.json`. diff --git a/README.md b/README.md index 8ba36fe..f06d79c 100644 --- a/README.md +++ b/README.md @@ -103,6 +103,27 @@ what it covered. We aim at original-suite ABI parity, coverage-driven agreement, exhaustive enumeration, and malloc-failure injection. Numbers appear here only after scripts produce them. +## Equivalence scope + +Bit-exact agreement between the C original and the Zig C ABI is claimed for +expressions evaluated under the C locale (`LC_NUMERIC`), at nesting depths +below the port bound, on inputs where the C original does not invoke +undefined behaviour (including signed-`char` ctype on high-bit bytes and +stack overflow on deep nests). Measured thresholds and upstream URLs live in +`docs/findings.md`. + +## Intentional divergences + +| Input class | C original | Port | Why | +| --- | --- | --- | --- | +| Deep nested parentheses | Stack overflow (issue #136) | Bounded parse error / NULL compile | Fail closed (D008) | +| `-Dfix-fac-overflow=true` | Truncates non-integer `fac` args | Returns NaN for non-integers | Optional hardening; default matches smoke (D006) | +| Decimals after comma-decimal `setlocale` | `strtod` follows `LC_NUMERIC` | Same via `libm.strtod` | Bit-exact parity (D025); scope to C locale | +| High-bit first byte on signed `char` | UB at `isalpha`/`isdigit` (#105) | No C ctype on plain `char` | Avoid copying that UB | + +Full write-up and measured host thresholds: `docs/findings.md`. +Upstream survey: `docs/evidence/upstream-issues.md`. + ## Licence zlib. Altered derivative of TinyExpr. See `LICENSE` and `THIRD_PARTY.md`. diff --git a/docs/evidence/divergences.md b/docs/evidence/divergences.md index e22a5ba..5853472 100644 --- a/docs/evidence/divergences.md +++ b/docs/evidence/divergences.md @@ -1,42 +1,43 @@ -# Differential enumerator divergences +# Divergences and equivalence scope -Scope: inputs where the C original does not exhibit undefined behaviour. -Deep nesting that overflows the C stack (Issue #136) is intentional and is -not fed through the in-process harness (it would abort the process). +Authoritative severity write-ups and upstream URLs: `docs/findings.md`. +README carries the short judge table. -## Completed bound (string length ≤ 3, AST depth ≤ 1) +**Equivalence scope:** bit-exact C↔Zig ABI agreement is claimed for the C +locale (`LC_NUMERIC`), at nesting depths below the port bound, on inputs +where the C original does not invoke undefined behaviour. + +## Intentional / scoped divergences + +| Input class | C original | Port | Why | Equivalence | +| --- | --- | --- | --- | --- | +| Deep nested parentheses | SIGSEGV near host-dependent depth (issue #136; Darwin arm64 interp crash 23764; Docker aarch64 17471) | Bounded fail: NULL compile / typed nest error (Darwin Zig ABI paren fail at 341) | Fail closed (D008) | Excludes C stack-overflow UB | +| `-Dfix-fac-overflow=true` | Truncates non-integer `fac` args | NaN for non-integers | Optional hardening; default matches smoke (D006) | Only when the flag is on | +| Decimals after comma-decimal `setlocale` | `strtod` follows `LC_NUMERIC` (`1.5` fails; `1,5` works) | Same via `libm.strtod` | Bit-exact parity (D025) | Decimal literals: C locale only | +| High-bit first byte on signed `char` | UB at `isalpha`/`isdigit` (#105) | No C ctype on plain `char` | Avoid copying C99 7.4p1 UB | Excludes that UB surface | + +## Differential enumerator (string length ≤ 3, AST depth ≤ 1) `bash scripts/run-enumerate.sh` compared **11505** inputs (11155 strings + 350 AST renders) with zero divergences across compile-nullness, error position, f64 bit patterns, and te_print bytes (constant prints). -## Probe-confirmed divergences (AST depth 2 family) - -These appear once the AST enumerator nests `fac` / `ncr` over NaN-producing -subexpressions such as `0/0` and `1%0`. Measured with the same in-process -`c_te_*` / `te_*` harness on 2026-08-02: - -| Enumerator | Minimized input | Observed | Root cause | -| --- | --- | --- | --- | -| probe | `fac(0/0)` | c_bits=`0x3ff0000000000000` (1.0), z_bits=`0x7ff0000000000000` (+Inf) | `0/0` is NaN. C `fac` does not treat NaN specially; `(unsigned)nan` is implementation-defined and yields 0 here, so the product loop returns 1. Zig `@intFromFloat(NaN)` takes a path that returns +Inf. Measured; C is the surprising 1.0. | -| probe | `fac((0/0))` | same as above | Same after parentheses. | -| probe | `fac(ncr(0,1))` | c=1.0, z=+Inf | `ncr(0,1)` is NaN (`n < r`); `fac(NaN)` hits the same C cast path. | -| intentional | deep nesting > 2048 | C: stack overflow (UB); Zig: compile NULL / err=-1 | D008 nesting bound. Not run in-process against C. | +Scope for that campaign: inputs where the C original does not exhibit +undefined behaviour. Deep nesting that overflows the C stack is not fed +through the in-process harness. -Control probes that **agree**: +## Probe notes (`fac` over NaN) | Input | Notes | | --- | --- | -| `0/0` | both NaN (`0xfff8000000000000`) | -| `1%0` | both NaN | -| `ncr(0,1)` | both NaN | +| `fac(0/0)`, `fac(ncr(0,1))` | C returns `1.0` on Darwin and Ubuntu Docker (this session). Zig ABI on Darwin also returned `1.0` here; an earlier WSL probe reported Zig `+Inf`. Treat C↔Zig for this class as host/opt-mode sensitive; related to open #133 / #62. | +| `0/0`, `1%0`, `ncr(0,1)` | Both sides NaN on agreement controls. | -## Locale scope (Lead C) +## Locale (shared behaviour, not a port win) -Under `setlocale(LC_ALL, "de_DE.UTF-8")`, both C and Zig (via `libm.strtod`) -reject `1.5` and accept `1,5`. This is shared behaviour, not a port bug. -Bit-exact equivalence for decimal literals is claimed for the C locale only. -See `docs/findings.md` Lead C. +Under `setlocale(LC_ALL, "de_DE.UTF-8")`, both C and Zig reject `1.5` and +accept `1,5`. Hang on `.5` was **not** reproduced. See upstream #142 and +`docs/findings.md`. ## Wasm diff --git a/docs/evidence/fragments/enumeration-full.json b/docs/evidence/fragments/enumeration-full.json new file mode 100644 index 0000000..6e4dbbd --- /dev/null +++ b/docs/evidence/fragments/enumeration-full.json @@ -0,0 +1,31 @@ +{ + "alphabet": "0123456789.+-*/^%(), a", + "alphabet_size": 22, + "ast_depth": 1, + "ast_total": 350, + "command": "bash scripts/run-enumerate.sh", + "comparisons_per_input": [ + "compile_success_nullness", + "error_position", + "f64_u64_bits", + "te_print_bytes" + ], + "compiler": "cc (Ubuntu 13.3.0-6ubuntu2~24.04.1) 13.3.0", + "divergences": 0, + "equivalence_scope": "inputs where the C original does not exhibit undefined behaviour; deep-nesting C crashes are intentional divergences when present", + "git_sha": "b800d61228b0f6202ff6f7a2d634e8f28cef5d57", + "host": "Linux elmengo 6.18.33.2-microsoft-standard-WSL2 #1 SMP PREEMPT_DYNAMIC Thu Jun 18 21:54:43 UTC 2026 x86_64 x86_64 x86_64 GNU/Linux", + "mode": "full", + "objcopy": "GNU objcopy (GNU Binutils for Ubuntu) 2.42", + "requested_alphabet": "0123456789.+-*/^%(), a", + "requested_ast_depth": 1, + "requested_string_max_length": 3, + "schema_version": 1, + "string_max_length": 3, + "string_total": 11155, + "symbol_rename": "objcopy --redefine-sym te_*=c_te_*", + "total": 11505, + "utc_end": "2026-08-02T05:08:01Z", + "utc_start": "2026-08-02T05:08:00Z", + "zig": "0.16.0" +} diff --git a/docs/evidence/fragments/fuzz-warmup-progress.json b/docs/evidence/fragments/fuzz-warmup-progress.json new file mode 100644 index 0000000..9baea09 --- /dev/null +++ b/docs/evidence/fragments/fuzz-warmup-progress.json @@ -0,0 +1,19 @@ +{ + "executions": 1604740, + "divergences": 0, + "first_token_reject": 61024, + "seconds": 1.000001, + "exec_per_sec": 1604738.75, + "seed": 1785662748, + "stopped_early": false, + "schema_version": 1, + "command": "bash scripts/run-fuzz.sh 1", + "git_sha": "b1d804343c433779920fa1080b4a411c993bc068", + "host": "Linux elmengo 6.18.33.2-microsoft-standard-WSL2 #1 SMP PREEMPT_DYNAMIC Thu Jun 18 21:54:43 UTC 2026 x86_64 x86_64 x86_64 GNU/Linux", + "utc_start": "2026-08-02T09:24:22Z", + "utc_end": "2026-08-02T09:24:24Z", + "mode": "warmup-progress", + "requested_seconds": 1.0, + "harness_exit": 0, + "zig": "0.16.0" +} diff --git a/docs/evidence/fragments/fuzz-warmup.json b/docs/evidence/fragments/fuzz-warmup.json new file mode 100644 index 0000000..91abeaf --- /dev/null +++ b/docs/evidence/fragments/fuzz-warmup.json @@ -0,0 +1,18 @@ +{ + "executions": 149237, + "divergences": 0, + "first_token_reject": 5693, + "seconds": 1.000727, + "exec_per_sec": 149128.53, + "seed": 1785627365, + "schema_version": 1, + "command": "bash scripts/run-fuzz.sh 1", + "git_sha": "b5e302ed15281e3d96c0998e64e0b8aa20afeee0", + "host": "Linux elmengo 6.18.33.2-microsoft-standard-WSL2 #1 SMP PREEMPT_DYNAMIC Thu Jun 18 21:54:43 UTC 2026 x86_64 x86_64 x86_64 GNU/Linux", + "utc_start": "2026-08-02T05:23:26Z", + "utc_end": "2026-08-02T05:23:35Z", + "mode": "warmup", + "requested_seconds": 1.0, + "harness_exit": 0, + "zig": "0.16.0" +} diff --git a/docs/evidence/scorecard.json b/docs/evidence/scorecard.json index a6eb1b2..8ab435f 100644 --- a/docs/evidence/scorecard.json +++ b/docs/evidence/scorecard.json @@ -10469,7 +10469,67 @@ "status": "not_measured" }, "upstream_findings": { - "status": "not_measured" + "status": "measured", + "upstream_findings": { + "schema_version": 1, + "command": "gh issue/pr create and comment; docs/evidence/repro/run-all.sh", + "git_sha": "609f03618213b8ab925f07d98785808c1907c672", + "host": "Darwin Kavyas-MacBook-Air-2.local 24.1.0 arm64; Ubuntu 24.04 aarch64 Docker", + "utc": "2026-08-02T10:35:00Z", + "allocator_misuse_track_g": "not_satisfied_negative_result", + "confirmed_existing": [ + { + "id": 136, + "title": "Security: Stack overflow via deeply nested expressions (CWE-674)", + "state": "OPEN", + "our_action": "independent_reproduction_comment", + "url": "https://github.com/codeplea/tinyexpr/issues/136", + "comment": "https://github.com/codeplea/tinyexpr/issues/136#issuecomment-5157204950" + }, + { + "id": 105, + "title": "Importing into gcc gives error: array subscript has type char", + "state": "OPEN", + "our_action": "confirmation_comment_and_fix_pr", + "url": "https://github.com/codeplea/tinyexpr/issues/105", + "comment": "https://github.com/codeplea/tinyexpr/issues/105#issuecomment-5157205043", + "pr": "https://github.com/codeplea/tinyexpr/pull/144" + } + ], + "newly_filed": [ + { + "id": 141, + "title": "next_token has external linkage and can silently bind the wrong function", + "state": "OPEN", + "url": "https://github.com/codeplea/tinyexpr/issues/141", + "pr": "https://github.com/codeplea/tinyexpr/pull/143", + "severity": "medium" + }, + { + "id": 142, + "title": "Decimal literals depend on LC_NUMERIC via strtod after setlocale", + "state": "OPEN", + "url": "https://github.com/codeplea/tinyexpr/issues/142", + "severity": "medium", + "hang_reproduced": false + } + ], + "pull_requests": [ + "https://github.com/codeplea/tinyexpr/pull/143", + "https://github.com/codeplea/tinyexpr/pull/144" + ], + "counts": { + "new_issues": 2, + "confirmation_comments": 2, + "fix_prs": 2 + }, + "nest_thresholds_measured": { + "darwin_arm64_interp_first_crash": 23764, + "linux_docker_aarch64_interp_first_crash": 17471, + "zig_abi_darwin_paren_last_ok": 340, + "zig_abi_darwin_paren_first_fail": 341 + } + } }, "bugleads": { "status": "measured",