a slot must be validated before it is dereferenced
ISHIKAWA: el_val_t carries 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, and looks_like_heap_obj was static -- so every sibling translation unit re-derived it. MEASURED, across the five existing tags geom_of looks_like_heap_obj full guard correct mfld_of looks_like_heap_obj full guard correct el_bin_lookup (uintptr_t)p < 4096 floor only reads 8 bytes BACKWARD el_input_len s ? ... : 0 NULL only strlen's an integer sha256_hex(50000) -> exit 139, SIGSEGV, compiled clean PREDICTIONS AND RESULTS P1 looks_like_heap_obj is static, not exported TRUE P2 each tagged type re-derives the check TRUE P3 at least one is missing guard components TRUE (two are) P6 sha256_hex(<int>) reads out of bounds TRUE P8 routing el_bin_lookup through the gate fixes it FALSE P9 the legitimate hash is unchanged TRUE P11 fixpoint and suites hold TRUE P8 IS THE USEFUL FAILURE. Guarding the tagged lookup changed nothing -- looks_like_heap_obj(49992) correctly returns 0, el_bin_lookup bails, and then el_input_len falls through to strlen() on address 50000. The FALLBACK was the hazard, not the tagged path. A NULL check does not establish that a slot is a pointer. I would have shipped the wrong fix and called it verified. A MEASUREMENT DEFECT, fourth today: my first run of the crash reported exit=0, because $? read head's exit through a pipe rather than the program's. I nearly recorded a segfault as a clean run. Same shape as grepping only parser.el and searching by variable name instead of by operation. AND I PROVED THE HAZARD FROM THE INSIDE. Sixty seconds after diagnosing `let s: String = 42` as an arbitrary-read primitive, I wrote the identical defect into el_await -- dereferencing ->magic off an unvalidated slot -- and only then found the runtime had already made it twice. el_tagged() is now exported in el_runtime.h. Anything that dereferences a slot without passing through it is the defect. 105/105 native, 42/42 integration across eight harnesses, fixpoint ok.
This commit is contained in:
@@ -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) ──────────────────────────────── */
|
||||
|
||||
Reference in New Issue
Block a user