356 lines
11 KiB
C
356 lines
11 KiB
C
/*
|
||
StarForth — Steady-State Virtual Machine Runtime
|
||
|
||
Copyright (c) 2023–2025 Robert A. James
|
||
All rights reserved.
|
||
|
||
This file is part of the StarForth project.
|
||
|
||
Licensed under the StarForth License, Version 1.0 (the "License");
|
||
you may not use this file except in compliance with the License.
|
||
|
||
You may obtain a copy of the License at:
|
||
https://github.com/star.4th@proton.me/StarForth/LICENSE.txt
|
||
|
||
This software is provided "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||
express or implied, including but not limited to the warranties of
|
||
merchantability, fitness for a particular purpose, and noninfringement.
|
||
|
||
See the License for the specific language governing permissions and
|
||
limitations under the License.
|
||
|
||
*/
|
||
|
||
/*
|
||
* acl_words.c — C primitive words for the word-level ACL system
|
||
*
|
||
* All policy logic lives in capsules/ACL.4th. These primitives expose
|
||
* DictEntry ACL fields to FORTH: ACL-MODE@, ACL-MODE!, ACL-PINNED?,
|
||
* ACL-PIN, ACL-TTL@, ACL-TTL!, ACL-ALLOW@, ACL-ALLOW!, ACL-HEAT@,
|
||
* ACL-INHERIT, ACL-INIT-PRIMITIVES, ACL-WORD-ID.
|
||
*
|
||
* All words that take an XT expect a DictEntry* cast to cell_t on the
|
||
* stack — exactly what ' (tick) produces in StarForth.
|
||
*/
|
||
|
||
#include "include/acl_words.h"
|
||
#include "../../include/vm.h"
|
||
#include "../../include/word_registry.h"
|
||
#include "../../include/log.h"
|
||
|
||
/**
|
||
* @brief Pop an execution token (XT) from the data stack and return the
|
||
* corresponding DictEntry pointer.
|
||
*
|
||
* Sets @c vm->error and returns NULL on stack underflow or if the XT is zero.
|
||
* All ACL word implementations call this before accessing DictEntry fields.
|
||
*
|
||
* @param vm Pointer to the VM structure
|
||
* @return Pointer to the DictEntry, or NULL on error
|
||
*/
|
||
static DictEntry *pop_xt(VM *vm)
|
||
{
|
||
if (!vm || vm->dsp < 0) {
|
||
log_message(LOG_ERROR, "ACL word: stack underflow");
|
||
if (vm) vm->error = 1;
|
||
return NULL;
|
||
}
|
||
cell_t xt = vm_pop(vm);
|
||
DictEntry *entry = (DictEntry *)(uintptr_t)xt;
|
||
if (!entry) {
|
||
log_message(LOG_ERROR, "ACL word: NULL XT");
|
||
vm->error = 1;
|
||
return NULL;
|
||
}
|
||
return entry;
|
||
}
|
||
|
||
/**
|
||
* @brief Implements FORTH word @c ACL-MODE@ — fetch ACL enforcement mode
|
||
*
|
||
* Stack effect: ( xt -- mode )
|
||
* Pushes the enforcement mode of the word identified by @c xt:
|
||
* 0 = STRICT (allow/deny is permanent), 1 = TTL (expires after countdown).
|
||
*
|
||
* @param vm Pointer to the VM structure
|
||
*/
|
||
static void forth_acl_mode_fetch(VM *vm)
|
||
{
|
||
DictEntry *e = pop_xt(vm);
|
||
if (!e) return;
|
||
vm_push(vm, (cell_t)e->acl_mode);
|
||
}
|
||
|
||
/**
|
||
* @brief Implements FORTH word @c ACL-MODE! — store ACL enforcement mode
|
||
*
|
||
* Stack effect: ( mode xt -- )
|
||
* Sets the enforcement mode of the word identified by @c xt.
|
||
* No-op if the word is pinned — pinned entries are immutable.
|
||
*
|
||
* @param vm Pointer to the VM structure
|
||
*/
|
||
static void forth_acl_mode_store(VM *vm)
|
||
{
|
||
if (!vm || vm->dsp < 1) {
|
||
log_message(LOG_ERROR, "ACL-MODE!: stack underflow");
|
||
if (vm) vm->error = 1;
|
||
return;
|
||
}
|
||
DictEntry *e = pop_xt(vm);
|
||
if (!e) return;
|
||
cell_t mode = vm_pop(vm);
|
||
if (e->acl_pinned) return; /* one-way ratchet: pinned is immutable */
|
||
e->acl_mode = (uint8_t)(mode & 0xFF);
|
||
}
|
||
|
||
/**
|
||
* @brief Implements FORTH word @c ACL-PINNED? — query pin status
|
||
*
|
||
* Stack effect: ( xt -- flag )
|
||
* Pushes -1 (true) if the word is pinned, 0 (false) otherwise.
|
||
* A pinned word's ACL fields cannot be modified by any FORTH operation.
|
||
*
|
||
* @param vm Pointer to the VM structure
|
||
*/
|
||
static void forth_acl_pinned_query(VM *vm)
|
||
{
|
||
DictEntry *e = pop_xt(vm);
|
||
if (!e) return;
|
||
vm_push(vm, e->acl_pinned ? -1 : 0);
|
||
}
|
||
|
||
/**
|
||
* @brief Implements FORTH word @c ACL-PIN — pin a word's ACL entry
|
||
*
|
||
* Stack effect: ( xt -- )
|
||
* Sets @c acl_pinned = 1 on the word identified by @c xt.
|
||
* This is a one-way ratchet: once set it can never be cleared, even by
|
||
* ACL-INHERIT. Use with @c ' WORD ACL-PIN in FORTH, never from C policy code.
|
||
*
|
||
* @param vm Pointer to the VM structure
|
||
*/
|
||
static void forth_acl_pin(VM *vm)
|
||
{
|
||
DictEntry *e = pop_xt(vm);
|
||
if (!e) return;
|
||
e->acl_pinned = 1;
|
||
}
|
||
|
||
/**
|
||
* @brief Implements FORTH word @c ACL-TTL@ — fetch TTL countdown value
|
||
*
|
||
* Stack effect: ( xt -- n )
|
||
* Pushes the current TTL (time-to-live) countdown of the word's ACL entry.
|
||
* When TTL reaches zero in TTL mode the interpreter calls @c acl_recheck().
|
||
*
|
||
* @param vm Pointer to the VM structure
|
||
*/
|
||
static void forth_acl_ttl_fetch(VM *vm)
|
||
{
|
||
DictEntry *e = pop_xt(vm);
|
||
if (!e) return;
|
||
vm_push(vm, (cell_t)e->acl_ttl);
|
||
}
|
||
|
||
/**
|
||
* @brief Implements FORTH word @c ACL-TTL! — store TTL countdown value
|
||
*
|
||
* Stack effect: ( n xt -- )
|
||
* Sets the TTL countdown on the word's ACL entry. Negative values are
|
||
* clamped to 0; values above UINT32_MAX are clamped to UINT32_MAX.
|
||
* No-op if the word is pinned.
|
||
*
|
||
* @param vm Pointer to the VM structure
|
||
*/
|
||
static void forth_acl_ttl_store(VM *vm)
|
||
{
|
||
if (!vm || vm->dsp < 1) {
|
||
log_message(LOG_ERROR, "ACL-TTL!: stack underflow");
|
||
if (vm) vm->error = 1;
|
||
return;
|
||
}
|
||
DictEntry *e = pop_xt(vm);
|
||
if (!e) return;
|
||
cell_t n = vm_pop(vm);
|
||
if (e->acl_pinned) return;
|
||
e->acl_ttl = (uint32_t)(n < 0 ? 0 : (uint64_t)n > UINT32_MAX ? UINT32_MAX : (uint32_t)n);
|
||
}
|
||
|
||
/**
|
||
* @brief Implements FORTH word @c ACL-ALLOW@ — fetch cached allow/deny flag
|
||
*
|
||
* Stack effect: ( xt -- flag )
|
||
* Pushes the cached ACL decision for the word: -1 = allowed, 0 = denied.
|
||
* This cached value is checked on every execution in TTL mode; the cache is
|
||
* refreshed by @c acl_recheck() when TTL expires.
|
||
*
|
||
* @param vm Pointer to the VM structure
|
||
*/
|
||
static void forth_acl_allow_fetch(VM *vm)
|
||
{
|
||
DictEntry *e = pop_xt(vm);
|
||
if (!e) return;
|
||
vm_push(vm, e->acl_allow ? -1 : 0);
|
||
}
|
||
|
||
/**
|
||
* @brief Implements FORTH word @c ACL-ALLOW! — store cached allow/deny flag
|
||
*
|
||
* Stack effect: ( flag xt -- )
|
||
* Sets the cached ACL decision: any non-zero @c flag = allowed, 0 = denied.
|
||
* No-op if the word is pinned.
|
||
*
|
||
* @param vm Pointer to the VM structure
|
||
*/
|
||
static void forth_acl_allow_store(VM *vm)
|
||
{
|
||
if (!vm || vm->dsp < 1) {
|
||
log_message(LOG_ERROR, "ACL-ALLOW!: stack underflow");
|
||
if (vm) vm->error = 1;
|
||
return;
|
||
}
|
||
DictEntry *e = pop_xt(vm);
|
||
if (!e) return;
|
||
cell_t flag = vm_pop(vm);
|
||
if (e->acl_pinned) return;
|
||
e->acl_allow = (flag != 0) ? 1 : 0;
|
||
}
|
||
|
||
/**
|
||
* @brief Implements FORTH word @c ACL-HEAT@ — fetch execution heat
|
||
*
|
||
* Stack effect: ( xt -- heat )
|
||
* Pushes the @c execution_heat counter for the word identified by @c xt.
|
||
* Used by @c ACL-TTL-COMPUTE in @c ACL.4th to calibrate the TTL value based
|
||
* on how frequently the word is executed.
|
||
*
|
||
* @param vm Pointer to the VM structure
|
||
*/
|
||
static void forth_acl_heat_fetch(VM *vm)
|
||
{
|
||
DictEntry *e = pop_xt(vm);
|
||
if (!e) return;
|
||
vm_push(vm, (cell_t)e->execution_heat);
|
||
}
|
||
|
||
/**
|
||
* @brief Implements FORTH word @c ACL-WORD-ID — fetch stable word identifier
|
||
*
|
||
* Stack effect: ( xt -- n )
|
||
* Pushes the stable @c word_id for the entry identified by @c xt. Unlike
|
||
* dictionary positions, @c word_id is assigned at registration and never
|
||
* changes, making it safe to use as a persistent table index in @c ACL.4th.
|
||
*
|
||
* @param vm Pointer to the VM structure
|
||
*/
|
||
static void forth_acl_word_id(VM *vm)
|
||
{
|
||
DictEntry *e = pop_xt(vm);
|
||
if (!e) return;
|
||
vm_push(vm, (cell_t)e->word_id);
|
||
}
|
||
|
||
/**
|
||
* @brief Implements FORTH word @c ACL-INHERIT — inherit ACL policy from parent
|
||
*
|
||
* Stack effect: ( src dst -- )
|
||
* Copies @c acl_mode from @c src to @c dst, then clears @c dst->acl_pinned,
|
||
* resets @c acl_ttl to 0, and sets @c acl_allow to 1 (optimistic default).
|
||
* This is a C primitive because FORTH code cannot clear @c acl_pinned — pin
|
||
* is a one-way ratchet that only C can reset during inheritance.
|
||
*
|
||
* @param vm Pointer to the VM structure
|
||
*/
|
||
static void forth_acl_inherit(VM *vm)
|
||
{
|
||
if (!vm || vm->dsp < 1) {
|
||
log_message(LOG_ERROR, "ACL-INHERIT: stack underflow");
|
||
if (vm) vm->error = 1;
|
||
return;
|
||
}
|
||
DictEntry *dst = pop_xt(vm);
|
||
if (!dst) return;
|
||
DictEntry *src = pop_xt(vm);
|
||
if (!src) return;
|
||
acl_inherit_entry(src, dst);
|
||
}
|
||
|
||
/**
|
||
* @brief Implements FORTH word @c ACL-INIT-PRIMITIVES — reset ACL fields on all
|
||
* unpinned dictionary entries
|
||
*
|
||
* Stack effect: ( -- )
|
||
* Walks the entire dictionary and resets ACL fields on every entry that is not
|
||
* pinned: sets @c acl_ttl = 0 (force recheck on first execution), @c acl_allow
|
||
* = 1 (optimistic default), @c acl_mode = ACL_MODE_TTL. Called by @c ACL-BOOT
|
||
* in @c ACL.4th immediately after the ACL capsule finishes loading.
|
||
*
|
||
* @param vm Pointer to the VM structure
|
||
*/
|
||
static void forth_acl_init_primitives(VM *vm)
|
||
{
|
||
if (!vm) return;
|
||
for (DictEntry *e = vm->latest; e; e = e->link) {
|
||
if (!e->acl_pinned) {
|
||
/* Re-assert permissive defaults; ACL-RECHECK will calibrate on first exec */
|
||
e->acl_ttl = 0; /* force recheck on first execution */
|
||
e->acl_allow = 1;
|
||
e->acl_mode = ACL_MODE_TTL;
|
||
}
|
||
}
|
||
}
|
||
|
||
/**
|
||
* @brief Direct C entry point for ACL policy inheritance on the capsule birth path.
|
||
*
|
||
* Copies @c acl_mode from @c src to @c dst, clears @c acl_pinned, resets
|
||
* @c acl_ttl to 0, and sets @c acl_allow to 1. Called from capsule birth code
|
||
* (not from FORTH) — no stack manipulation is performed.
|
||
*
|
||
* @param src Source DictEntry whose @c acl_mode is inherited
|
||
* @param dst Destination DictEntry whose ACL fields are updated
|
||
*/
|
||
void acl_inherit_entry(DictEntry *src, DictEntry *dst)
|
||
{
|
||
if (!src || !dst) return;
|
||
dst->acl_mode = src->acl_mode;
|
||
dst->acl_pinned = 0; /* pin is contextual, not viral */
|
||
dst->acl_ttl = 0; /* child has no history; force recheck */
|
||
dst->acl_allow = 1; /* optimistic default */
|
||
}
|
||
|
||
/**
|
||
* @brief Register all ACL C-primitive words with the VM dictionary.
|
||
*
|
||
* Registers: field accessors (@c ACL-MODE@ / @c ACL-MODE! / @c ACL-PINNED? /
|
||
* @c ACL-TTL@ / @c ACL-TTL! / @c ACL-ALLOW@ / @c ACL-ALLOW! / @c ACL-HEAT@ /
|
||
* @c ACL-WORD-ID), and mutation words (@c ACL-PIN / @c ACL-INHERIT /
|
||
* @c ACL-INIT-PRIMITIVES). Called from @c vm_bootstrap.c during VM
|
||
* initialisation.
|
||
*
|
||
* @param vm Pointer to the VM structure
|
||
*/
|
||
void register_acl_words(VM *vm)
|
||
{
|
||
/* Field accessors — read */
|
||
register_word(vm, "ACL-MODE@", forth_acl_mode_fetch);
|
||
register_word(vm, "ACL-PINNED?", forth_acl_pinned_query);
|
||
register_word(vm, "ACL-TTL@", forth_acl_ttl_fetch);
|
||
register_word(vm, "ACL-ALLOW@", forth_acl_allow_fetch);
|
||
register_word(vm, "ACL-HEAT@", forth_acl_heat_fetch);
|
||
register_word(vm, "ACL-WORD-ID", forth_acl_word_id);
|
||
|
||
/* Field accessors — write (all no-op if pinned) */
|
||
register_word(vm, "ACL-MODE!", forth_acl_mode_store);
|
||
register_word(vm, "ACL-TTL!", forth_acl_ttl_store);
|
||
register_word(vm, "ACL-ALLOW!", forth_acl_allow_store);
|
||
|
||
/* Pin ratchet — one-way set */
|
||
register_word(vm, "ACL-PIN", forth_acl_pin);
|
||
|
||
/* Higher-level operations */
|
||
register_word(vm, "ACL-INHERIT", forth_acl_inherit);
|
||
register_word(vm, "ACL-INIT-PRIMITIVES", forth_acl_init_primitives);
|
||
}
|