From fec08e56859410ea19f207cc0b4864081f8aaeaf Mon Sep 17 00:00:00 2001 From: Evgen Byelozorov Date: Fri, 14 Aug 2026 07:33:33 +0200 Subject: [PATCH] fix(collection): guard INT64_MIN count in take/drop against overflow UB MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit take and drop turn their raw i64 count into a magnitude with `n < 0 ? -n : n`, so a count of INT64_MIN (0Nl, or the literal -9223372036854775808) hit `-INT64_MIN` — signed-integer-overflow UB (UBSan: src/ops/collection.c). It fired in every scalar-count take branch (vector, string, char, scalar, list) and in ray_drop_fn. take: reject an INT64_MIN count once, up front, before dispatching to the per-shape branches — its magnitude is unrepresentable as int64 and could never be allocated (a range error, which is what the downstream negative-capacity check already produced for the vector path, minus the UB). drop: a drop-from-end of that magnitude removes the whole collection (|n| >= len), so treat it as cut == len (empty result) — matching (drop x -N) for any N >= len. Normal positive/negative takes and drops are unchanged, and the table/dict paths recurse through these kernels. Adds regressions to collection/take.rfl (all take shapes) and collection/drop_cut_rotate_cross.rfl. --- src/ops/collection.c | 11 ++++++++++- test/rfl/collection/drop_cut_rotate_cross.rfl | 3 +++ test/rfl/collection/take.rfl | 9 +++++++++ 3 files changed, 22 insertions(+), 1 deletion(-) diff --git a/src/ops/collection.c b/src/ops/collection.c index 798096d72..c25c45f96 100644 --- a/src/ops/collection.c +++ b/src/ops/collection.c @@ -1772,6 +1772,12 @@ ray_t* ray_take_fn(ray_t* vec, ray_t* n_obj) { return ray_error("type", "take: range take unsupported for %s", ray_type_name(vec->type)); } + /* Every scalar-count branch below turns the count into a magnitude with + * `n < 0 ? -n : n`; `-INT64_MIN` is signed-overflow UB and |INT64_MIN| is + * unallocatable anyway, so reject an INT64_MIN count (also the i64 null + * sentinel) once here, up front, for all of them. */ + if (ray_is_atom(n_obj) && is_numeric(n_obj) && as_i64(n_obj) == INT64_MIN) + return ray_error("range", "take: count magnitude out of range"); /* Char take: (take 'a' n) → string of n copies of char */ if (ray_is_atom(vec) && vec->type == -RAY_STR && ray_str_len(vec) == 1 && ray_is_atom(n_obj) && is_numeric(n_obj)) { int64_t n = as_i64(n_obj); @@ -2015,7 +2021,10 @@ ray_t* ray_drop_fn(ray_t* vec, ray_t* n_obj) { start = n < len ? n : len; amount = len - start; } else { - int64_t cut = -n; + /* `-INT64_MIN` is signed-overflow UB; a drop-from-end of that + * magnitude removes the whole collection (|n| >= len), so treat it as + * cut == len (amount 0) rather than negating. */ + int64_t cut = (n == INT64_MIN) ? len : -n; amount = cut < len ? len - cut : 0; } return collection_slice(vec, start, amount); diff --git a/test/rfl/collection/drop_cut_rotate_cross.rfl b/test/rfl/collection/drop_cut_rotate_cross.rfl index bbf5a3872..74b9538be 100644 --- a/test/rfl/collection/drop_cut_rotate_cross.rfl +++ b/test/rfl/collection/drop_cut_rotate_cross.rfl @@ -4,6 +4,9 @@ (drop [1 2 3 4 5] -2) -- [1 2 3] (drop [1 2 3] 10) -- [] (drop [1 2 3] -10) -- [] +;; INT64_MIN count: `-INT64_MIN` is signed-overflow UB (was tripped in +;; ray_drop_fn); |n| that large drops the whole collection. +(drop [1 2 3] -9223372036854775808) -- [] (drop "abcdef" 2) -- "cdef" (drop "abcdef" -2) -- "abcd" (drop ['a 'b 'c] 1) -- ['b 'c] diff --git a/test/rfl/collection/take.rfl b/test/rfl/collection/take.rfl index 92e1ef1d6..bb1a954a0 100644 --- a/test/rfl/collection/take.rfl +++ b/test/rfl/collection/take.rfl @@ -175,3 +175,12 @@ (count (take [1 2 3 4 5] [2 0])) -- 0 ;; range take with start = count → empty (count (take [1 2 3 4 5] [5 3])) -- 0 +;; INT64_MIN count: `-INT64_MIN` is signed-overflow UB (was tripped computing +;; the magnitude in every scalar-count take branch — vector, string, char, +;; scalar, list). Its magnitude is unrepresentable as int64, so it is a range +;; error, not UB followed by a bogus allocation. +(take [1 2 3] -9223372036854775808) !- range +(take "hello" -9223372036854775808) !- range +(take "a" -9223372036854775808) !- range +(take 42 -9223372036854775808) !- range +(take (list 1 2 3) -9223372036854775808) !- range