diff --git a/lang/runtime/el_runtime.c b/lang/runtime/el_runtime.c index e0db23e..73c04e0 100644 --- a/lang/runtime/el_runtime.c +++ b/lang/runtime/el_runtime.c @@ -712,6 +712,20 @@ el_val_t el_map_set(el_val_t mapv, el_val_t keyv, el_val_t value) { * happen to look like aligned heap pointers are exceedingly unlikely to land * on a page whose first 4 bytes match either magic. */ +/* el_tagged — THE gate for "is this slot a heap object carrying this tag". + * + * el_val_t carries both integers and tagged heap pointers, so deciding which + * requires checking the value BEFORE dereferencing it. That check was a + * convention every author had to know rather than a gate they had to pass + * through, and the result is measurable: geom_of and mfld_of call + * looks_like_heap_obj and are correct; el_bin_lookup checked only a 4096 floor + * -- no alignment, no small-int, no negative -- and reads EIGHT BYTES BACKWARD + * from the pointer. sha256_hex(50000) therefore compiled clean and segfaulted. + * + * Exported, so the engram siblings stop re-deriving it. Anything that + * dereferences a slot without passing through here is the defect. */ +int el_tagged(el_val_t v, uint32_t magic); + static int looks_like_heap_obj(el_val_t v) { if (v == 0) return 0; int64_t s = (int64_t)v; @@ -722,6 +736,12 @@ static int looks_like_heap_obj(el_val_t v) { return 1; } +int el_tagged(el_val_t v, uint32_t magic) { + if (!looks_like_heap_obj(v)) return 0; + return *(const uint32_t*)(uintptr_t)v == magic; +} + + void el_retain(el_val_t v) { if (!looks_like_heap_obj(v)) return; ElHeader* h = (ElHeader*)(uintptr_t)v; @@ -18272,8 +18292,11 @@ static int el_bin_lookup(const void* p, size_t* out_len) { /* Avoid reading off the front of a page on tiny pointers (e.g. NULs * passed in as int-cast values). 4096 is a safe lower bound on any * platform we target. */ - if ((uintptr_t)p < 4096) return 0; - const el_bin_hdr_t* hdr = (const el_bin_hdr_t*)((const char*)p - sizeof(el_bin_hdr_t)); + /* Reads BACKWARD, so the HEADER address is what must be validated -- and a + * 4096 floor alone let sha256_hex(50000) through to a SIGSEGV. */ + const char* hp = (const char*)p - sizeof(el_bin_hdr_t); + if (!looks_like_heap_obj((el_val_t)(uintptr_t)hp)) return 0; + const el_bin_hdr_t* hdr = (const el_bin_hdr_t*)hp; if (hdr->magic != EL_MAGIC_BIN) return 0; *out_len = hdr->length; return 1; @@ -18283,7 +18306,12 @@ static int el_bin_lookup(const void* p, size_t* out_len) { static size_t el_input_len(const char* s) { size_t n; if (el_bin_lookup(s, &n)) return n; - return s ? strlen(s) : 0; + /* The FALLBACK is the hazard, not the tagged lookup. A NULL check does not + * establish that a slot is a pointer: el_val_t carries integers too, so + * strlen() on `sha256_hex(50000)` walks address 50000. Guarding the tagged + * path alone left this untouched and the SIGSEGV unchanged -- measured. */ + if (!looks_like_heap_obj((el_val_t)(uintptr_t)s)) return 0; + return strlen(s); } /* ─── SHA-256 (Brad Conte / public domain) ──────────────────────────────── */ diff --git a/lang/runtime/el_runtime.h b/lang/runtime/el_runtime.h index 0bf2d43..2492c68 100644 --- a/lang/runtime/el_runtime.h +++ b/lang/runtime/el_runtime.h @@ -888,6 +888,7 @@ el_val_t engram_age_field_catchup(void); el_val_t engram_chrono_persist_tick(void); el_val_t engram_chrono_tick(void); el_val_t engram_boundary_beat(el_val_t op_name, el_val_t construct); +int el_tagged(el_val_t v, uint32_t magic); /* the gate: validate a slot BEFORE dereferencing it */ el_val_t el_seam_run(el_val_t fn_name, el_val_t phase, el_val_t result); /* runtime construct seam */ el_val_t el_seam_wrap(el_val_t fn_name, el_val_t (*body)(void*), void* env); /* runtime invocation control */ /* API-reshape decorator-seam auto-emit; construct = the decorator that caused the beat */ el_val_t engram_self_anchor_capture(void); diff --git a/lang/tests/integration/tagged_gate.sh b/lang/tests/integration/tagged_gate.sh new file mode 100755 index 0000000..5445dcc --- /dev/null +++ b/lang/tests/integration/tagged_gate.sh @@ -0,0 +1,35 @@ +#!/usr/bin/env bash +# tagged_gate.sh — a slot must be validated before it is dereferenced. +# +# el_val_t carries both integers and tagged heap pointers, so "is this a +# pointer" is undecidable without checking first. That check was a CONVENTION +# every author had to know rather than a GATE they had to pass through: +# geom_of, mfld_of call looks_like_heap_obj correct +# el_bin_lookup checked only a 4096 floor read 8 bytes backward +# el_input_len checked only for NULL strlen'd an integer +# sha256_hex(50000) therefore compiled clean and segfaulted (exit 139). +set -uo pipefail +ELC="${1:?usage: tagged_gate.sh }" +LANG_DIR="${2:-$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)}" +W=$(mktemp -d); trap 'rm -rf "$W"' EXIT; F=0 +chk(){ [ "$2" = "$3" ] && printf ' ok %s\n' "$1" || { printf ' FAIL %s\n expected %s got %s\n' "$1" "$2" "$3"; F=$((F+1)); }; } +cd "$LANG_DIR" +SRCS=$(../scripts/el-runtime-sources.sh runtime) +CF="-std=c11 -O2 -I runtime"; for d in /opt/homebrew/opt/openssl@3 /usr/local/opt/openssl@3; do [ -d "$d" ] && CF="$CF -I $d/include" && LF="-L $d/lib"; done +LF="${LF:-} -lcurl -lssl -lcrypto -lpthread -lm" + +build(){ "$ELC" "$1" > "$W/t.c" 2>/dev/null && cc $CF -o "$W/t" "$W/t.c" $SRCS $LF 2>/dev/null; } + +printf 'fn main() { let h: String = sha256_hex(50000) println("got " + h) }\n' > "$W/a.el" +build "$W/a.el"; "$W/t" >"$W/o" 2>&1; chk "an integer where a string is expected does not crash" "0" "$?" +chk "and yields the empty-string hash, not memory" "1" "$(grep -c e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 "$W/o")" + +printf 'fn main() { let h: String = sha256_hex(-5) println("got " + h) }\n' > "$W/b.el" +build "$W/b.el"; "$W/t" >/dev/null 2>&1; chk "a NEGATIVE integer does not crash" "0" "$?" + +printf 'fn main() { println(sha256_hex("abc")) }\n' > "$W/c.el" +build "$W/c.el"; out=$("$W/t" 2>&1) +chk "a legitimate string still hashes correctly" "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad" "$out" + +chk "the gate is exported, so siblings stop re-deriving it" "1" "$(grep -c 'int *el_tagged(el_val_t' runtime/el_runtime.h)" +echo; echo " 5 assertions, $((5-F)) passed, $F failed"; exit $F