From e7304606f58c0471b3998a9121c09d89088669ad Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 15 May 2026 07:55:07 +0000 Subject: [PATCH] Red tests for Int64 index bounds-check bypass on arrays TStaticArrayExpr.GetIndex and TDynamicArrayExpr.EvalAsXxx evaluate the index via EvalAsInteger (Int64) but store it into a 32-bit local `Integer` variable before performing the Cardinal-based range check. Any index whose low 32 bits land inside the array length silently bypasses the check and reads/writes the wrong element, so a[$100000000] reads a[0] a[$100000002] := v writes a[2] instead of raising "Upper bound exceeded". Add two ArrayPass scripts that assert the safe behaviour (out-of-bounds exception raised) so the suite goes red until the truncation is fixed at the bounds-check site. --- Test/ArrayPass/array_bounds_int64_dynamic.pas | 36 +++++++++++++++ Test/ArrayPass/array_bounds_int64_dynamic.txt | 2 + Test/ArrayPass/array_bounds_int64_static.pas | 45 +++++++++++++++++++ Test/ArrayPass/array_bounds_int64_static.txt | 4 ++ 4 files changed, 87 insertions(+) create mode 100644 Test/ArrayPass/array_bounds_int64_dynamic.pas create mode 100644 Test/ArrayPass/array_bounds_int64_dynamic.txt create mode 100644 Test/ArrayPass/array_bounds_int64_static.pas create mode 100644 Test/ArrayPass/array_bounds_int64_static.txt diff --git a/Test/ArrayPass/array_bounds_int64_dynamic.pas b/Test/ArrayPass/array_bounds_int64_dynamic.pas new file mode 100644 index 00000000..87660b03 --- /dev/null +++ b/Test/ArrayPass/array_bounds_int64_dynamic.pas @@ -0,0 +1,36 @@ +// Red test for a bounds-check bypass on dynamic arrays accessed through +// a non-variable base expression. +// +// TDynamicArrayExpr.EvalAsInteger / EvalAsString / ... (Source/dwsArrayExprs.pas +// around lines 1367..1459) declares the index as a 32-bit `Integer`, +// even though IndexExpr.EvalAsInteger returns an Int64. Any index whose +// low 32 bits land inside the array's length silently bypasses +// BoundsCheckPassed and indexes the wrong element. The function-call base +// here forces the non-`Var` code path so the bug manifests on every platform +// (the variable-base path adds a separate Cardinal-truncation issue that we +// keep out of this test to avoid platform-dependent access violations). +// +// Expected (safe) behaviour: an out-of-bounds exception is raised. +function MakeArr : array of Integer; +begin + Result := new Integer[5]; + for var i := 0 to 4 do + Result[i] := 100 + 10*i; +end; + +var base : Integer := 0; +base := base + 4294967296; // 2^32; low 32 bits are 0 + +try + PrintLn(MakeArr()[base]); // truncates to MakeArr()[0] today + PrintLn('FAIL no exception (MakeArr()[2^32])'); +except + on E: Exception do PrintLn('OK out-of-bounds raised for 2^32'); +end; + +try + PrintLn(MakeArr()[base + 4]); // truncates to MakeArr()[4] today + PrintLn('FAIL no exception (MakeArr()[2^32 + 4])'); +except + on E: Exception do PrintLn('OK out-of-bounds raised for 2^32 + 4'); +end; diff --git a/Test/ArrayPass/array_bounds_int64_dynamic.txt b/Test/ArrayPass/array_bounds_int64_dynamic.txt new file mode 100644 index 00000000..b6e616dd --- /dev/null +++ b/Test/ArrayPass/array_bounds_int64_dynamic.txt @@ -0,0 +1,2 @@ +OK out-of-bounds raised for 2^32 +OK out-of-bounds raised for 2^32 + 4 diff --git a/Test/ArrayPass/array_bounds_int64_static.pas b/Test/ArrayPass/array_bounds_int64_static.pas new file mode 100644 index 00000000..bbe46252 --- /dev/null +++ b/Test/ArrayPass/array_bounds_int64_static.pas @@ -0,0 +1,45 @@ +// Red test for a bounds-check bypass on static arrays. +// +// TStaticArrayExpr.GetIndex (Source/dwsArrayExprs.pas:1250) evaluates the +// index as Int64 but stores `EvalAsInteger(exec) - FLowBound` into a 32-bit +// `Result : Integer` before performing a Cardinal-based range check. Any +// index whose low 32 bits land inside the declared range silently bypasses +// the check and reads (or writes) the wrong element instead of raising +// `Upper bound exceeded`. +// +// Expected (safe) behaviour: an out-of-bounds exception is raised for every +// index >= Length even when bits above 2^31 are set. +var a : array[0..4] of Integer; +var i : Integer; +for i := 0 to 4 do + a[i] := 100 + i; + +// Build the indices at runtime so the compiler cannot constant-fold them +// into a static range error. +var base : Integer := 0; +base := base + 4294967296; // 2^32; low 32 bits are 0 + +try + PrintLn(a[base]); // truncates to a[0] today + PrintLn('FAIL no exception (a[2^32])'); +except + on E: Exception do PrintLn('OK out-of-bounds raised for 2^32'); +end; + +try + PrintLn(a[base + 3]); // truncates to a[3] today + PrintLn('FAIL no exception (a[2^32 + 3])'); +except + on E: Exception do PrintLn('OK out-of-bounds raised for 2^32 + 3'); +end; + +try + a[base + 2] := 999; // truncates write to a[2] today + PrintLn('FAIL no exception on write (a[2^32 + 2])'); +except + on E: Exception do PrintLn('OK out-of-bounds raised on write'); +end; + +// Witness corruption: if the bounds check was bypassed the previous write +// will have clobbered a[2]. +PrintLn(a[2]); diff --git a/Test/ArrayPass/array_bounds_int64_static.txt b/Test/ArrayPass/array_bounds_int64_static.txt new file mode 100644 index 00000000..eb1b901e --- /dev/null +++ b/Test/ArrayPass/array_bounds_int64_static.txt @@ -0,0 +1,4 @@ +OK out-of-bounds raised for 2^32 +OK out-of-bounds raised for 2^32 + 3 +OK out-of-bounds raised on write +102