From 5ff9baa25e917e55c50c2eb99031f665d5593111 Mon Sep 17 00:00:00 2001 From: Evgen Byelozorov Date: Wed, 26 Aug 2026 22:17:03 +0200 Subject: [PATCH] fix(datalog): reject forged dl-program handles instead of dereferencing them MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit dl-program wrapped a raw dl_program_t* in a plain -RAY_I64 atom with no tag, and dl_unwrap_program reinterpreted ANY i64 as that pointer. So a one-line expression — (dl-free 1), (dl-stratify 1), (dl-query 1 'x), (dl-eval 1), (dl-add-edb 1 ...) — dereferenced an attacker-chosen address and crashed with SIGSEGV. On a server that evaluates client input over IPC this is a remotely-triggerable crash, and a chosen integer is an arbitrary-pointer free/deref (memory corruption), not merely a DoS. Tag the handle atom with RAY_ATTR_DLPROG (mirroring the RAY_ATTR_GRAPH / RAY_ATTR_HNSW handle scheme) and verify it in dl_unwrap_program, so a plain integer or an arithmetic copy (which does not carry attrs) is rejected with a type error. dl-free clears the tag and zeroes the pointer on free, keeping its idempotent-false contract for a genuine double-free while rejecting forged handles. The bit reuses 0x20 (RAY_ATTR_SORTED / ATTR_QUOTED), which is vector / -RAY_SYM scoped and never read on a -RAY_I64 atom, and no free-path touches 0x20 — so there is no finalizer collision. Also closes the arithmetic-copy double-free (a copy loses the tag, so only the original frees) and the snapshot/ser-de-restored stale-handle crash (the restored atom loses the tag and is rejected). Adds forged/copied-handle coverage to datalog_coverage.rfl. --- src/mem/heap.h | 9 ++++++++ src/ops/datalog.c | 31 ++++++++++++++++++--------- test/rfl/datalog/datalog_coverage.rfl | 18 ++++++++++++++++ 3 files changed, 48 insertions(+), 10 deletions(-) diff --git a/src/mem/heap.h b/src/mem/heap.h index 0e82b4026..17dc98131 100644 --- a/src/mem/heap.h +++ b/src/mem/heap.h @@ -91,6 +91,15 @@ * Checked by HNSW builtins before dereferencing. User must (hnsw-free h). */ #define RAY_ATTR_HNSW 0x04 +/* I64 atom carries an owning dl_program_t* (a Datalog program) in its .i64 + * slot. Checked by the dl-* builtins before dereferencing, so a forged/plain + * integer or an arithmetic copy (which drops attrs) is rejected with a type + * error instead of being reinterpreted as a pointer — see dl_unwrap_program. + * Reuses 0x20 (RAY_ATTR_SORTED on vectors / ATTR_QUOTED on -RAY_SYM); the + * -RAY_I64 type tag disambiguates, and no free-path or generic check reads + * 0x20 on a -RAY_I64 atom. User must (dl-free h). */ +#define RAY_ATTR_DLPROG 0x20 + /* Vector is a linked column. The 8 bytes of the aux union at offset * 8 (i.e. parent->_idx_pad / parent->slice_offset / parent->str_pool * slot, depending on which arm is in use) hold an int64 diff --git a/src/ops/datalog.c b/src/ops/datalog.c index d9db9bade..243e1e973 100644 --- a/src/ops/datalog.c +++ b/src/ops/datalog.c @@ -4409,18 +4409,23 @@ ray_t* ray_query_fn(ray_t** args, int64_t n) { * Programmatic Datalog API builtins * ══════════════════════════════════════════ */ -/* Opaque handle for dl_program_t stored in a ray_t atom. - * We store the pointer in the i64 field. */ +/* Opaque handle for dl_program_t stored in a ray_t atom. The pointer lives + * in the i64 field, tagged with RAY_ATTR_DLPROG so dl_unwrap_program can reject + * a plain integer (or an arithmetic copy, which does not carry the attr) + * instead of dereferencing an arbitrary value as a dl_program_t*. Mirrors the + * RAY_ATTR_GRAPH / RAY_ATTR_HNSW handle scheme. */ static ray_t* dl_wrap_program(dl_program_t* prog) { ray_t* obj = ray_alloc(0); if (!obj || RAY_IS_ERR(obj)) return ray_error("oom", NULL); obj->type = -RAY_I64; obj->i64 = (int64_t)(uintptr_t)prog; + obj->attrs |= RAY_ATTR_DLPROG; return obj; } static dl_program_t* dl_unwrap_program(ray_t* obj) { - if (!obj || obj->type != -RAY_I64) return NULL; + if (!obj || obj->type != -RAY_I64 || !(obj->attrs & RAY_ATTR_DLPROG)) + return NULL; return (dl_program_t*)(uintptr_t)obj->i64; } @@ -4514,16 +4519,22 @@ ray_t* ray_dl_provenance_fn(ray_t* prog_obj, ray_t* pred_obj) { /* (dl-free prog) — free a dl-program handle created by (dl-program). * The handle wraps a raw dl_program_t* with no automatic finalizer, so * without an explicit free the program block and all relation tables it - * owns leak. Idempotent: zeroes the handle so a second call is a no-op - * (returns false) instead of double-freeing. */ + * owns leak. On free we clear RAY_ATTR_DLPROG and zero the pointer, so a + * second call is an idempotent no-op (returns false) rather than a + * double-free. A tagged pointer that isn't an i64 is a type error, and a + * plain/forged integer (RAY_ATTR_DLPROG absent, non-zero value) is rejected + * as a type error instead of being dereferenced as a dl_program_t*. */ ray_t* ray_dl_free_fn(ray_t* x) { if (!x || x->type != -RAY_I64) return ray_error("type", "dl-free: arg must be a dl-program"); - dl_program_t* prog = dl_unwrap_program(x); - if (!prog) return ray_bool(false); /* already freed / null handle */ - dl_program_free(prog); - x->i64 = 0; - return ray_bool(true); + if (x->attrs & RAY_ATTR_DLPROG) { + dl_program_free((dl_program_t*)(uintptr_t)x->i64); + x->attrs &= (uint8_t)~RAY_ATTR_DLPROG; + x->i64 = 0; + return ray_bool(true); + } + if (x->i64 == 0) return ray_bool(false); /* already-freed handle: idempotent */ + return ray_error("type", "dl-free: not a dl-program handle"); } /* Reset global Datalog rule storage (called from ray_lang_destroy) */ diff --git a/test/rfl/datalog/datalog_coverage.rfl b/test/rfl/datalog/datalog_coverage.rfl index 43b90187a..c498f99a3 100644 --- a/test/rfl/datalog/datalog_coverage.rfl +++ b/test/rfl/datalog/datalog_coverage.rfl @@ -544,6 +544,24 @@ (dl-query FP 'edge) !- type (dl-free 'notprog) !- type +;; --- Claim 5b: forged/copied dl-program handles must not be dereferenced --- +;; A plain integer is NOT a dl-program handle. Every dl-* builtin that unwraps +;; one must reject it with a type error rather than reinterpreting the value as +;; a dl_program_t* pointer — before the RAY_ATTR_DLPROG tag check, (dl-free 1) +;; and friends dereferenced address 1 and crashed with SIGSEGV. +(dl-free 1) !- type +(dl-stratify 1) !- type +(dl-eval 1) !- type +(dl-query 1 'edge) !- type +(dl-add-edb 1 'edge (table ['a 'b] (list [1] [2])) 2) !- type +;; An arithmetic copy of a live handle drops the tag (attrs are not carried +;; through arithmetic), so it can't be freed — which would otherwise +;; double-free the original. The original still frees exactly once. +(set CP (dl-program)) +(set CPcopy (+ CP 0)) +(dl-free CPcopy) !- type +(dl-free CP) -- true + ;; --- Claim 4: fixpoint non-convergence must error loudly, not return a partial result --- ;; Reachability over a linear chain needs ~N iterations. The loop caps at 1000.