This commit is contained in:
udo
2026-06-13 15:10:42 +00:00
parent afaa4dd7c0
commit 5a56d4f39e
14 changed files with 531 additions and 83 deletions
+214 -5
View File
@@ -35,6 +35,7 @@
#include <optional>
#include <string>
#include <sys/stat.h>
#include <unistd.h>
#include <sys/time.h>
#include <vector>
@@ -69,6 +70,7 @@ struct WasmWorkerConfig
String core_wasm_path = "bin/wasm/core.wasm";
String site_root; // absolute
String cache_root = "/tmp/uce/work";
std::vector<String> write_roots; // absolute prefixes the write membrane allows
int64_t memory_limit = 512ll * 1024 * 1024;
u32 table_headroom = 4096;
u64 epoch_deadline_ticks = 200; // ticker period × ticks = CPU budget
@@ -308,6 +310,15 @@ private:
{
wasmtime::Config config;
config.epoch_interruption(true);
// CRITICAL: the host (linux_fastcgi.cpp) installs its own SIGSEGV/SIGILL/
// SIGBUS handlers per request (install_request_fault_handlers) with plain
// signal(), which clobbers Wasmtime's trap-handling handlers. With
// signals_based_traps on, a guest `unreachable`/OOB surfaces as a host
// signal that the native on_segfault catches and abort()s the worker —
// a 502 instead of a clean wasm trap. Disabling it makes Cranelift emit
// explicit trap checks: guest traps stay pure wasm traps returned as
// Result errors, never a host signal, so the two never collide.
config.signals_based_traps(false);
return(wasmtime::Engine(std::move(config)));
}
@@ -329,6 +340,18 @@ public:
{
}
#ifdef UCE_WASM_HOST_CONNECTORS
// Host-owned resource handle table (§3.1): connections opened by the guest
// live here and are closed when the workspace drops at request end.
std::vector<SQLite*> sqlite_handles;
~WasmWorkspace()
{
for(auto* db : sqlite_handles)
if(db)
delete db; // ~SQLite disconnects
}
#endif
// resolve-kind values shared with the guest core (src/wasm/core.cpp)
// must match WasmResolveKind in src/wasm/core.cpp
enum ResolveKind { RESOLVE_COMPONENT = 0, RESOLVE_RENDER = 1, RESOLVE_EXISTS = 2, RESOLVE_ONCE = 3 };
@@ -794,6 +817,13 @@ private:
if(worker.cfg.verbose)
fprintf(stderr, "[wasm] loaded %s (mem_base=%u table_base=%u)\n",
source_path.c_str(), memory_base, table_base);
// The epoch budget is a guest-CPU watchdog, but the ticker is wall-clock
// and host-side module compilation here (lazy, mid-render, possibly many
// units) burns it without the guest running. Reset the deadline after a
// load so the budget measures guest execution between membrane crossings,
// not our compile time. A genuine runaway loop makes no loads, so it
// still trips the deadline.
ctx().set_epoch_deadline(worker.cfg.epoch_deadline_ticks);
return("");
}
@@ -864,17 +894,33 @@ private:
candidates.push_back(entry_dir + "/" + raw);
candidates.push_back(worker.cfg.site_root + "/" + raw);
}
char site_real[4096];
if(!realpath(worker.cfg.site_root.c_str(), site_real))
return("");
String site_prefix = String(site_real) + "/";
// readable roots = the site tree plus the writable scratch dirs (a page
// can read back what it is allowed to write, e.g. /tmp), canonicalized.
std::vector<String> read_roots;
read_roots.push_back(worker.cfg.site_root);
for(auto& root : worker.cfg.write_roots)
read_roots.push_back(root);
std::vector<String> root_prefixes;
for(auto& root : read_roots)
{
char root_real[4096];
if(root != "" && realpath(root.c_str(), root_real))
root_prefixes.push_back(String(root_real) + "/");
}
for(auto& candidate : candidates)
{
char resolved[4096];
if(!realpath(candidate.c_str(), resolved))
continue;
String path(resolved);
if(path.rfind(site_prefix, 0) != 0)
bool allowed = false;
for(auto& prefix : root_prefixes)
if(path.rfind(prefix, 0) == 0)
{
allowed = true;
break;
}
if(!allowed)
continue;
if(file_exists_host(path))
return(path);
@@ -882,6 +928,42 @@ private:
return("");
}
// write membrane policy: resolve the target (absolute, or relative to the
// current unit / site root) and allow it only if its parent directory
// canonicalizes under one of the configured write roots (site tree + the
// runtime scratch dirs). The file itself need not exist yet.
String resolve_guest_write(const String& raw, const String& current_unit)
{
if(raw == "" || raw.find('\0') != String::npos)
return("");
String target;
if(raw.rfind("/", 0) == 0)
target = raw;
else
{
String current_dir = current_unit != "" ? dir_of(current_unit) : String("");
target = (current_dir != "" ? current_dir : worker.cfg.site_root) + "/" + raw;
}
String parent = dir_of(target);
String base = parent.size() < target.size() ? target.substr(parent.size() + 1) : String("");
if(base == "" || base == "." || base == "..")
return("");
char parent_real[4096];
if(!realpath(parent.c_str(), parent_real))
return("");
String resolved_parent(parent_real);
for(auto& root : worker.cfg.write_roots)
{
char root_real[4096];
if(root == "" || !realpath(root.c_str(), root_real))
continue;
String root_prefix(root_real);
if(resolved_parent == root_prefix || resolved_parent.rfind(root_prefix + "/", 0) == 0)
return(resolved_parent + "/" + base);
}
return("");
}
static String sanitize_symbol_suffix(const String& raw)
{
// mirrors ascii_safe_name in functionlib.cpp
@@ -1062,6 +1144,133 @@ private:
results[0] = Val((int32_t)bytes.size());
return(std::monostate());
}));
if(mod == "env" && name == "uce_host_file_write")
return(add([self](Caller, Span<const Val> args, Span<Val> results) -> Result<std::monostate, Trap> {
String path, current, content;
self->hostcall_read(args[0].i32(), args[1].i32(), path);
self->hostcall_read(args[2].i32(), args[3].i32(), current);
self->hostcall_read(args[4].i32(), args[5].i32(), content);
bool append = args[6].i32() != 0;
String resolved = self->resolve_guest_write(path, current);
bool ok = false;
if(resolved != "")
ok = append ? file_append_contents(resolved, content) : file_put_contents(resolved, content);
else if(self->worker.cfg.verbose)
fprintf(stderr, "[wasm] file_write denied: %s\n", path.c_str());
results[0] = Val(ok ? (int32_t)1 : (int32_t)0);
return(std::monostate());
}));
#ifdef UCE_WASM_HOST_CONNECTORS
if(mod == "env" && name == "uce_host_sqlite")
return(add([self](Caller, Span<const Val> args, Span<Val> results) -> Result<std::monostate, Trap> {
// {op,handle,path,query,params} in → result out. The real native
// connector runs host-side; connections live in the workspace
// handle table (handle = 1-based index).
String encoded;
self->hostcall_read(args[0].i32(), args[1].i32(), encoded);
DValue request, response, decode_err_unused;
String decode_error;
if(ucb_decode(encoded, request, &decode_error))
{
String op = request["op"].to_string();
if(op == "connect")
{
SQLite* db = new SQLite();
db->connect(request["path"].to_string());
u64 handle = 0;
if(db->connection)
{
self->sqlite_handles.push_back(db);
handle = self->sqlite_handles.size();
}
response["handle"] = (f64)handle;
response["error_code"] = (f64)db->error_code;
response["statement_info"] = db->error();
if(handle == 0)
delete db;
}
else
{
u64 handle = request["handle"].to_u64();
SQLite* db = (handle >= 1 && handle <= self->sqlite_handles.size())
? self->sqlite_handles[(size_t)handle - 1] : 0;
if(op == "query" && db)
{
StringMap params;
DValue* p = request.key("params");
if(p)
p->each([&](const DValue& value, String key) { params[key] = value.to_string(); });
response["result"] = db->query(request["query"].to_string(), params);
response["insert_id"] = (f64)db->insert_id;
response["affected"] = (f64)db->affected_rows;
response["error_code"] = (f64)db->error_code;
response["statement_info"] = db->error();
}
else if(op == "disconnect" && db)
{
delete db;
self->sqlite_handles[(size_t)handle - 1] = 0;
}
}
}
String out = ucb_encode(response);
u32 cap = (u32)args[3].i32();
int32_t buf = args[2].i32();
if(buf != 0 && cap >= out.size())
self->hostcall_write(buf, out);
results[0] = Val((int32_t)out.size());
return(std::monostate());
}));
#endif
if(mod == "env" && name == "uce_host_file_unlink")
return(add([self](Caller, Span<const Val> args, Span<Val>) -> Result<std::monostate, Trap> {
String path, current;
self->hostcall_read(args[0].i32(), args[1].i32(), path);
self->hostcall_read(args[2].i32(), args[3].i32(), current);
String resolved = self->resolve_guest_write(path, current);
if(resolved != "")
::unlink(resolved.c_str());
return(std::monostate());
}));
if(mod == "env" && name == "uce_host_regex")
return(add([self](Caller, Span<const Val> args, Span<Val> results) -> Result<std::monostate, Trap> {
// {op,pattern,subject,flags,replacement} in (UCEB1) → result out.
// PCRE2 lives host-side; this runs the real native regex_*.
String encoded;
self->hostcall_read(args[0].i32(), args[1].i32(), encoded);
DValue request;
String decode_error;
DValue response;
if(ucb_decode(encoded, request, &decode_error))
{
String op = request["op"].to_string();
String pattern = request["pattern"].to_string();
String subject = request["subject"].to_string();
String flags = request["flags"].to_string();
if(op == "match")
response["bool"].set_bool(regex_match(pattern, subject, flags));
else if(op == "search")
response["tree"] = regex_search(pattern, subject, flags);
else if(op == "search_all")
response["tree"] = regex_search_all(pattern, subject, flags);
else if(op == "replace")
response["text"] = regex_replace(pattern, request["replacement"].to_string(), subject, flags);
else if(op == "split")
for(auto& part : regex_split(pattern, subject, flags))
{
DValue value;
value = part;
response["list"].push(value);
}
}
String out = ucb_encode(response);
u32 cap = (u32)args[3].i32();
int32_t buf = args[2].i32();
if(buf != 0 && cap >= out.size())
self->hostcall_write(buf, out);
results[0] = Val((int32_t)out.size());
return(std::monostate());
}));
if(mod == "env" && name == "uce_host_component_resolve")
return(add([self](Caller, Span<const Val> args, Span<Val> results) -> Result<std::monostate, Trap> {
String target, current, resolved;