Add FENCE word (SDK v1.9.0 scoping); fix severe pre-existing FORGET use-after-free
FENCE ( -- ) exposes the dict_fence_latest/dict_fence_here state FORGET already honored internally, letting callers (e.g. a future SDK capsule) raise the boundary after loading their own content -- no new VM fields, no policy logic beyond exposing existing state. Writing a direct test for it surfaced a real, severe, pre-existing bug in FORGET's relink logic, unrelated to FENCE itself and reproducible with the original boot-time fence alone: - Forgetting the single newest word incorrectly destroyed every other word back to the fence too, not just the target. - Forgetting an older word (correctly cascading to remove newer words too, per FORTH-79 semantics) crashed with SIGSEGV. Root cause: the relink code's target_prev pointer was, by construction, always inside the range the preceding loop had just freed whenever target wasn't vm->latest -- so writing through it was a use-after-free every time that branch executed. Fixed by removing the target_prev tracking and both branches entirely; vm->latest unconditionally becomes target_next (target's own captured, still-valid link) after the free loop, correct in every case. Added a FENCE test suite to dictionary_manipulation_words_test.c (Module 14) including the exact regression case (forgetting the newest word must not disturb an older one). Verified zero warnings and identical POST/dict_hash results across all three kernel architectures. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
6d8b48f100
commit
4e7dcdf889
@@ -76,6 +76,7 @@ static void defining_word_literal(VM * vm);
|
||||
static void defining_word_does(VM * vm);
|
||||
static void defining_word_immediate(VM * vm);
|
||||
static void dictionary_word_forget(VM * vm);
|
||||
static void dictionary_word_fence(VM * vm);
|
||||
|
||||
/* ───────────────────────────── Runtimes ───────────────────────────── */
|
||||
|
||||
@@ -579,18 +580,16 @@ static void dictionary_word_forget(VM *vm) {
|
||||
int forget_failed = 0;
|
||||
sf_mutex_lock(&vm->dict_lock);
|
||||
|
||||
/* Find target and its previous entry */
|
||||
DictEntry *prev = NULL, *e = vm->latest;
|
||||
DictEntry *target = NULL, *target_prev = NULL;
|
||||
/* Find target */
|
||||
DictEntry *e = vm->latest;
|
||||
DictEntry *target = NULL;
|
||||
while (e) {
|
||||
if (!(e->flags & WORD_HIDDEN) &&
|
||||
e->name_len == (uint8_t) nlen &&
|
||||
memcmp(e->name, namebuf, (size_t) nlen) == 0) {
|
||||
target = e;
|
||||
target_prev = prev;
|
||||
break;
|
||||
}
|
||||
prev = e;
|
||||
e = e->link;
|
||||
}
|
||||
if (!target) {
|
||||
@@ -649,14 +648,17 @@ static void dictionary_word_forget(VM *vm) {
|
||||
e = next;
|
||||
}
|
||||
|
||||
/* Relink: if target_prev exists, point it past the freed chain */
|
||||
if (target_prev) {
|
||||
target_prev->link = target_next; /* Skip freed entries */
|
||||
vm->latest = target_prev; /* Latest is now the entry before target */
|
||||
} else {
|
||||
/* No prev means target was latest - reset to fence */
|
||||
vm->latest = vm->dict_fence_latest;
|
||||
}
|
||||
/* Relink: FORGET removes target and everything newer (closer to
|
||||
* vm->latest) -- the free loop above just freed every entry from
|
||||
* vm->latest through target inclusive, which by definition includes
|
||||
* any entry between them. There is nothing left to "skip past" --
|
||||
* target_next (target's own surviving link) is always the correct
|
||||
* new head, whether or not target was vm->latest itself. (Previously
|
||||
* this branched on whether an entry existed between vm->latest and
|
||||
* target, and relinked through it -- but that entry was always inside
|
||||
* the just-freed range, making it a use-after-free on every FORGET of
|
||||
* a non-latest word.) */
|
||||
vm->latest = target_next;
|
||||
|
||||
vm->here = (size_t) new_here;
|
||||
|
||||
@@ -669,6 +671,21 @@ forget_exit:
|
||||
return;
|
||||
}
|
||||
|
||||
/* FENCE ( -- ) raise the FORGET boundary to the current dictionary top.
|
||||
* dict_fence_latest/dict_fence_here already exist and are already honored
|
||||
* by FORGET (set once at bootstrap, protecting the base wordset); this is
|
||||
* the first word exposing that state so callers can advance it further --
|
||||
* e.g. an SDK capsule calling FENCE after loading protects its own
|
||||
* definitions the same way the base wordset is already protected. */
|
||||
static void dictionary_word_fence(VM *vm) {
|
||||
if (!vm) return;
|
||||
sf_mutex_lock(&vm->dict_lock);
|
||||
vm->dict_fence_latest = vm->latest;
|
||||
vm->dict_fence_here = vm->here;
|
||||
sf_mutex_unlock(&vm->dict_lock);
|
||||
log_message(LOG_DEBUG, "FENCE: raised to HERE=%ld", (long) vm->dict_fence_here);
|
||||
}
|
||||
|
||||
/* DOES> — IMMEDIATE: finalize the defining word’s create-part and compile DOES>-body */
|
||||
static void defining_word_does(VM *vm) {
|
||||
if (!vm) return;
|
||||
@@ -755,6 +772,7 @@ void register_defining_words(VM *vm) {
|
||||
|
||||
/* Dictionary management */
|
||||
register_word(vm, "FORGET", dictionary_word_forget);
|
||||
register_word(vm, "FENCE", dictionary_word_fence);
|
||||
|
||||
/* Compile helpers — immediate */
|
||||
register_word(vm, "COMPILE", defining_word_compile);
|
||||
|
||||
Reference in New Issue
Block a user