diff --git a/API_TODO.md b/API_TODO.md new file mode 100644 index 0000000..0cb9f0a --- /dev/null +++ b/API_TODO.md @@ -0,0 +1,148 @@ +# API_TODO — trans-membrane OS surface expansion + +Status: briefing / design spec for the next round of `uce_host_*` membrane work. +Implementation delegated to the pi agent + sub-agents. + +## Why + +UCE units run as wasm in a per-request workspace; the **only** way they touch the +OS is via `uce_host_*` hostcalls (see `docs/wasm-runtime-architecture.md`). That +constrained, auditable surface is a feature — it's the security boundary +sysadmins can reason about. The current file API (`file_get_contents` / +`file_put_contents` / `file_append` / `file_exists` / `file_unlink` / `ls` / +`file_mkdir` / `file_mtime` / `path_*` / `cwd_*`) is simple and good but is +**whole-file only** and lacks several OS capabilities real apps need. This doc +widens *what* units can do **within the existing path roots** — not *where*. + +## Cross-cutting design principles (apply to EVERY new call) + +1. **Requests must complete fast — never block indefinitely.** Every potentially + blocking call takes an explicit timeout/deadline and **fails fast** on expiry + with a clear result, rather than spinning or blocking forever. In particular: + - **File locks expire.** Auto-locking (writes take exclusive, reads wait for + writers) must use a **bounded** wait (deadline, e.g. config + `UCE_FILE_LOCK_TIMEOUT_MS`, sane default ~2000ms). On timeout the op returns + a clear "lock timeout" error — it does NOT hang the request. This applies to + `file_put_contents`/`file_append`/`file_get_contents` and the new streaming + handles below. + - `http_request`, blocking `shell_exec`, `job_await`, etc. all take timeouts. +2. **Fire-and-forget + check back later.** Long work goes to the background and + the request returns immediately with a **job id**; a *later* request (any + worker) checks the outcome. A single host-side **job registry** backs this + (see "Async jobs"). This is the model for background shell, async http, and + any long op — not per-request blocking. +3. **Simple AND versatile.** Prefer one small, composable primitive over many + special cases. Structured inputs/outputs are `DValue` (UCEB across the + membrane). Streaming uses opaque host **handles** (u64). Errors are returned + values (no exceptions in the guest). +4. **One capability per named hostcall** (blocklist granularity — see Future). + Don't bundle unrelated capabilities into one mega-hostcall. +5. **Always policy-gated.** Every path goes through `resolve_guest_file` / + `resolve_guest_write` (roots); handles/stat/rename/etc. gated the same way. +6. **No exceptions/RTTI** in unit-facing code; return values + status DValues. + +## API groups to implement + +### 1. Streaming / handle-based file I/O (P0 — the keystone) +Whole-file String I/O can't handle large or partial files. Add a handle API. +Locking stays automatic and **lifetime-scoped** (open-for-write = exclusive, +open-for-read = shared, released on `file_close`), with the bounded-wait rule +from principle 1 — this is NOT the old manual lock API, the user never calls a +lock function. +``` +u64 file_open(String path, String mode) // "r" "w" "a" "r+"; 0 = error (incl. lock timeout) +String file_read(u64 h, u64 len) // sequential +String file_pread(u64 h, u64 offset, u64 len) // positional +u64 file_write(u64 h, String data) // sequential; returns bytes written +u64 file_pwrite(u64 h, u64 offset, String data) +s64 file_seek(u64 h, s64 offset, int whence) // + file_tell(h) +void file_close(u64 h) // releases lock + fd +``` +Keep the existing whole-file `file_get_contents`/`put_contents`/`append` (now +auto-locking) as the easy path. + +### 2. Metadata (P0/P1) +``` +DValue file_stat(String path) // { exists, size, mtime, ctime, mode, is_dir, is_file, is_symlink } +DValue dir_list(String path) // [ { name, type, size, mtime } ... ]; optional glob/recursive flags +``` +(`file_mtime` stays; `dir_list` is the richer `ls` so callers don't N+1 `file_stat`.) + +### 3. Structural ops (P1) +``` +bool file_rename(String from, String to) // atomic replace; enables write-temp-then-rename +bool file_copy(String from, String to) // host-side, preserves mode +bool file_truncate(String path, u64 size) // (or on a handle) +bool dir_remove(String path, bool recursive) +``` + +### 4. Niceties (P2) +``` +String file_temp(String prefix) // race-free temp file under a write root (mkstemp) +bool file_chmod(String path, u32 mode) +bool file_symlink(String target, String linkpath); String file_readlink(String path) +// durability: file_put_contents/file_write gain an optional fsync flag/variant +``` + +### 5. Outbound HTTP (with TLS) (high value) +`socket_*` is raw TCP only — no secure outbound. Add a real client: +``` +DValue http_request(DValue req) // req: { method, url, headers{}, body, timeout_ms, follow_redirects } + // -> { status, headers{}, body, error } (TLS/https supported) +u64 http_request_async(DValue req) // -> job_id (see Async jobs) +``` + +### 6. Crypto (extended) (high value) +`hash.h` has sha1 + rng only. +``` +String sha256(String data); String sha256_hex(String data) +String hmac_sha256(String key, String data) // + _hex +String base64_encode(String data); String base64_decode(String b64) +String random_bytes(u64 n) // CSPRNG +bool crypto_equal(String a, String b) // constant-time compare +``` + +### 7. Shell (extended + background) (high value) +Current `shell_exec` is blocking + output-only. +``` +DValue shell_exec(DValue spec) // spec: { cmd, stdin, env{}, timeout_ms } -> { exit_code, stdout, stderr, timed_out } +u64 shell_spawn(DValue spec) // background -> job_id; non-blocking; request returns immediately +``` +(Keep a simple `shell_exec(String)` convenience overload returning stdout.) + +### 8. Async jobs (the unifying "check back later" primitive) +Host-side job registry, survives the spawning request, reachable from any later +request/worker by id. +``` +DValue job_status(u64 job_id) // { state: pending|running|done|failed, ... } +DValue job_result(u64 job_id) // blocks up to a small default; { done, ... , result } +DValue job_await(u64 job_id, u64 timeout_ms) // bounded wait; returns even if not done (state=running) +bool job_cancel(u64 job_id) +``` +`shell_spawn` and `http_request_async` produce job ids consumed here. Jobs have a +TTL and are reaped; results are retrievable until collected or TTL expires. + +## Explicitly OUT of scope (declined) +- DNS resolution API, UDP sockets, `setenv` / environment mutation. + +## Future (design influences now, NOT implemented here) +- **Configurable trans-membrane blocklist.** A server config will let sysadmins + disable specific hostcalls by name (e.g. turn off `shell_exec`, `http_request`) + to harden a deployment. This is WHY principle 4 (one capability per named + hostcall) matters — keep the granularity clean so each is independently + gateable. A blocked call should return a clear "disabled by policy" error. +- **Native app extensions.** Longer term: let app developers ship their own + native compiled modules (`.so` living in the site tree) that units can call + across the membrane — a per-app plugin mechanism. Out of scope now, but the + hostcall/membrane shape should not preclude a future generic + "call host extension" dispatch. + +## Implementation notes +- Each call = a discrete `uce_host_*` hostcall: add to `src/wasm/core_hostcalls.syms`, + register in `src/wasm/worker.cpp`, declare/implement the wasm-core wrapper in + `src/lib/sys.cpp` (+ header) mirroring existing hostcalls (sized-buffer for + bytes, sized-DValue/UCEB for structured returns). +- Reuse native impls where they exist; policy-gate every path. +- Each group lands suite-gated (`run_cli_tests --include-wasm-kill`, 0 failed) on + the build host; add tests + doc pages per new function (function pages, and + method/`2_` pages where a class gains methods). diff --git a/docs/setup.md b/docs/setup.md index b16a6b9..4e349a4 100644 --- a/docs/setup.md +++ b/docs/setup.md @@ -35,7 +35,7 @@ apt update apt install -y clang build-essential libpcre2-dev mariadb-client libmariadb-dev curl rsync ca-certificates ``` -UCE also requires two non-vendored dependencies. WASI SDK is load-bearing at runtime because UCE compiles units on demand during requests and during proactive startup scans. +UCE also requires two non-vendored dependencies. WASI SDK is load-bearing at runtime because UCE compiles units on demand during requests and during proactive startup scans. The `curl` binary is also a pinned runtime package dependency: `http_request()` and `http_request_async()` execute it directly with an explicit argument vector for TLS-capable outbound HTTP. - **Wasmtime C API / C++ headers** at `/opt/wasmtime` by default. `scripts/build_linux.sh` expects: - `/opt/wasmtime/include/wasmtime.hh` diff --git a/scripts/api_coverage_manifest.py b/scripts/api_coverage_manifest.py index 187746d..bc16a2a 100755 --- a/scripts/api_coverage_manifest.py +++ b/scripts/api_coverage_manifest.py @@ -15,13 +15,13 @@ DOC_DIR = ROOT / "site" / "doc" / "pages" # name, needs_doc, status. status: public | internal | integration PUBLIC_APIS = [ - ("shell_exec", True, "public"), ("shell_escape", True, "public"), + ("http_request", True, "public"), ("http_request_async", True, "public"), + ("shell_exec", True, "public"), ("shell_escape", True, "public"), ("shell_spawn", True, "public"), + ("job_status", True, "public"), ("job_result", True, "public"), ("job_await", True, "public"), ("job_cancel", True, "public"), ("basename", True, "public"), ("dirname", True, "public"), ("path_join", True, "public"), ("path_real", True, "public"), ("path_is_within", True, "public"), - ("file_open_locked", True, "public"), ("file_close_locked", True, "public"), - ("file_release_process_locks", True, "public"), ("file_get_contents_locked_fd", True, "public"), - ("file_put_contents_locked_fd", True, "public"), ("file_get_contents", True, "public"), - ("file_put_contents", True, "public"), ("file_append_contents", False, "public"), + ("file_get_contents", True, "public"), + ("file_put_contents", True, "public"), ("file_append", True, "public"), ("cwd_get", True, "public"), ("cwd_set", True, "public"), ("process_start_directory", True, "public"), ("file_mtime", True, "public"), ("file_unlink", True, "public"), ("expand_path", True, "public"), ("ls", True, "public"), ("to_u64", True, "public"), ("to_s64", True, "public"), @@ -35,7 +35,9 @@ PUBLIC_APIS = [ ("float_val", True, "public"), ("nibble", True, "public"), ("json_consume_space", False, "public"), ("array_merge", True, "public"), ("safe_name", True, "public"), ("ascii_safe_name", True, "public"), ("to_json", False, "public"), ("remove", False, "public"), ("clear", False, "public"), - ("gen_sha1", True, "public"), ("gen_noise32", True, "public"), ("gen_noise64", True, "public"), + ("gen_sha1", True, "public"), ("sha256", True, "public"), ("sha256_hex", True, "public"), + ("hmac_sha256", True, "public"), ("hmac_sha256_hex", True, "public"), ("random_bytes", True, "public"), + ("crypto_equal", True, "public"), ("gen_noise32", True, "public"), ("gen_noise64", True, "public"), ("gen_noise01", True, "public"), ("gen_int", True, "public"), ("gen_float", True, "public"), ("draw_int", True, "public"), ("draw_float", True, "public"), ("encode_query", True, "public"), ("request_script_url", True, "public"), diff --git a/scripts/install_wasi_sdk.sh b/scripts/install_wasi_sdk.sh index 98d57f8..24eaa3a 100755 --- a/scripts/install_wasi_sdk.sh +++ b/scripts/install_wasi_sdk.sh @@ -59,7 +59,12 @@ verify_tree() { return 1 fi done + if ! command -v curl >/dev/null 2>&1; then + echo "Missing runtime dependency: curl (required by UCE http_request/http_request_async)" >&2 + return 1 + fi "$root/bin/clang++" --version | head -n 1 + curl --version | head -n 1 } if [[ $check_only -eq 1 ]]; then diff --git a/site/doc/areas/noise.txt b/site/doc/areas/noise.txt index 1e40100..2d231d1 100644 --- a/site/doc/areas/noise.txt +++ b/site/doc/areas/noise.txt @@ -8,3 +8,9 @@ gen_noise64 gen_float gen_int gen_sha1 +sha256 +sha256_hex +hmac_sha256 +hmac_sha256_hex +random_bytes +crypto_equal diff --git a/site/doc/areas/socket.txt b/site/doc/areas/socket.txt index 4084260..1b73e16 100644 --- a/site/doc/areas/socket.txt +++ b/site/doc/areas/socket.txt @@ -1,5 +1,7 @@ Socket Functions +http_request +http_request_async socket_close socket_connect socket_read diff --git a/site/doc/areas/sys.txt b/site/doc/areas/sys.txt index b67bbc1..be33f4f 100644 --- a/site/doc/areas/sys.txt +++ b/site/doc/areas/sys.txt @@ -4,7 +4,6 @@ basename dirname expand_path file_append -file_append_contents file_exists file_get_contents file_mtime @@ -16,6 +15,11 @@ path_join cwd_set shell_escape shell_exec +shell_spawn +job_status +job_result +job_await +job_cancel file_unlink zip_create zip_list @@ -25,3 +29,22 @@ gz_compress gz_uncompress server_start_http server_stop +file_open +file_read +file_pread +file_write +file_pwrite +file_seek +file_tell +file_close +file_stat +dir_list +file_rename +file_copy +file_truncate +dir_remove +file_temp +file_chmod +file_symlink +file_readlink +file_fsync diff --git a/site/doc/pages/3_Blocked functions.txt b/site/doc/pages/3_Blocked functions.txt new file mode 100644 index 0000000..d8ca4e8 --- /dev/null +++ b/site/doc/pages/3_Blocked functions.txt @@ -0,0 +1,60 @@ +:title +Blocked functions (hostcall blocklist) + +:content +UCE units reach the operating system only through a fixed set of `uce_host_*` +membrane hostcalls (see the runtime architecture). A server operator can +**disable individual hostcalls** so a deployment exposes only the capabilities it +wants — for example turning off `shell_exec` or `http_request` on a hardened +host. A unit that calls a disabled function fails at request time with the +configurable error page, stating exactly which function was blocked and why. + +## Configuration + +Set `UCE_HOSTCALL_BLOCKLIST` in `/etc/uce/settings.cfg` to a comma-separated list +of hostcall names. Names may be given bare (`shell_exec`) or fully qualified +(`uce_host_shell_exec`); whitespace is ignored. Empty (the default) blocks +nothing. + +``` +UCE_HOSTCALL_BLOCKLIST=shell_exec, shell_spawn, http_request, http_request_async, mysql +``` + +Changes take effect on **restart** (`systemctl restart uce`). There is no hot +reload — the list is parsed once per worker process into a fast lookup, so an +empty list has zero runtime cost and a non-empty list costs only a single check +per hostcall at workspace birth (never per call). + +## Behaviour when a blocked function is called + +The blocked hostcall resolves to a trap stub instead of its real implementation. +When a unit invokes it, the request fails into the runtime error page with: + +- error type `policy_blocked` (so a custom error page template can special-case it), +- a title `function disabled by server policy`, +- a message naming the exact function, e.g. *"this unit called uce_host_shell_exec, + which is disabled on this server by configuration (UCE_HOSTCALL_BLOCKLIST)"*. + +The worker is unharmed (it is a clean guest trap, like any other), and only the +offending request fails. A unit cannot catch this — blocking is enforcement, not +a soft signal. + +## What can and cannot be blocked + +Any `uce_host_*` capability hostcall can be listed — file I/O, `shell_exec` / +`shell_spawn`, `http_request`, `mysql`, sockets, memcache, crypto, the job +registry, etc. (the full set is the membrane list in the runtime architecture +doc and `src/wasm/core_hostcalls.syms`). + +A small core set the runtime itself needs is **exempt** and ignored even if +listed, so a deployment cannot be bricked by an over-broad blocklist: +`component_resolve` (used for `component()` / unit rendering). + +## Notes + +- No recompilation is required (neither the wasm core nor the native binary) — + this is pure runtime configuration; blocked hostcalls still exist as imports, + they simply resolve to a trap. +- Pure-compute library functions that are NOT hostcalls (string ops, `DValue` + methods, hashing helpers like `gen_noise`, etc.) are not OS capabilities and + cannot be blocked this way — only `uce_host_*` membrane calls are gateable. diff --git a/site/doc/pages/coming_from_react.txt b/site/doc/pages/3_Coming from React.txt similarity index 100% rename from site/doc/pages/coming_from_react.txt rename to site/doc/pages/3_Coming from React.txt diff --git a/site/doc/pages/crypto_equal.txt b/site/doc/pages/crypto_equal.txt new file mode 100644 index 0000000..064bcae --- /dev/null +++ b/site/doc/pages/crypto_equal.txt @@ -0,0 +1,7 @@ +# crypto_equal + +```cpp +bool crypto_equal(String a, String b) +``` + +Constant-time byte comparison for secrets such as MACs and tokens. diff --git a/site/doc/pages/dir_list.txt b/site/doc/pages/dir_list.txt new file mode 100644 index 0000000..10e0713 --- /dev/null +++ b/site/doc/pages/dir_list.txt @@ -0,0 +1,3 @@ +DValue dir_list(String path) + +Returns a list of `{ name, type, size, mtime }` entries for a policy-gated directory. Names are sorted and exclude `.`/`..`. diff --git a/site/doc/pages/dir_remove.txt b/site/doc/pages/dir_remove.txt new file mode 100644 index 0000000..f0e6c2e --- /dev/null +++ b/site/doc/pages/dir_remove.txt @@ -0,0 +1,3 @@ +dir_remove + +Structural filesystem operation across the wasm host membrane. All path arguments are policy-gated through the same guest file/write roots as other file APIs and return false on denial or OS error. diff --git a/site/doc/pages/file_append.txt b/site/doc/pages/file_append.txt index f28569a..3c5946d 100644 --- a/site/doc/pages/file_append.txt +++ b/site/doc/pages/file_append.txt @@ -11,6 +11,7 @@ file_name : file name of file that should be written to :content Opens or creates a file and appends data to it. +The append transparently takes an exclusive file lock for the duration of the append operation. Concurrent writers are serialized, and `file_get_contents()` waits for in-progress writes to finish; callers do not manage locks manually. ## Related Concepts - PHP: `file_put_contents($file, $data, FILE_APPEND)` diff --git a/site/doc/pages/file_append_contents.txt b/site/doc/pages/file_append_contents.txt deleted file mode 100644 index 262d7a9..0000000 --- a/site/doc/pages/file_append_contents.txt +++ /dev/null @@ -1,26 +0,0 @@ -:sig -bool file_append_contents(String file_name, String content) - -:params -file_name : path to open or create -content : bytes to append -return value : `true` when the append succeeds - -:see ->sys -file_append -file_put_contents -file_get_contents - -:content -Appends one string to a file. - -`file_append()` is the variadic convenience wrapper for ordinary page code. Use `file_append_contents()` when you already have one string buffer. - -Example: - -```uce -bool ok = file_append_contents("/tmp/uce-log.txt", "line\n"); -``` - -The file is created if it does not exist. diff --git a/site/doc/pages/file_chmod.txt b/site/doc/pages/file_chmod.txt new file mode 100644 index 0000000..f5678a1 --- /dev/null +++ b/site/doc/pages/file_chmod.txt @@ -0,0 +1,3 @@ +file_chmod + +Filesystem nicety added to the wasm host membrane. Path arguments are policy-gated; operations return an empty value or false on denial/error. file_fsync() takes an open file handle and flushes it with fsync(). diff --git a/site/doc/pages/file_close.txt b/site/doc/pages/file_close.txt new file mode 100644 index 0000000..5d0f097 --- /dev/null +++ b/site/doc/pages/file_close.txt @@ -0,0 +1,5 @@ +file_close + +Streaming/handle-based file I/O across the wasm host membrane. Handles are opaque u64 values returned by file_open(); 0 means open failed (including path policy denial or bounded lock timeout). Locks are automatic and lifetime-scoped: read opens take a shared lock, write/append/read-write opens take an exclusive lock, and file_close() releases it. Lock wait is bounded by UCE_FILE_LOCK_TIMEOUT_MS (default 2000ms). + +See also: file_get_contents, file_put_contents, file_append. diff --git a/site/doc/pages/file_close_locked.txt b/site/doc/pages/file_close_locked.txt deleted file mode 100644 index 61c5626..0000000 --- a/site/doc/pages/file_close_locked.txt +++ /dev/null @@ -1,12 +0,0 @@ -:sig -void file_close_locked(int fd) - -:params -fd : handle returned by `file_open_locked()` - -:see ->sys ->file_open_locked - -:content -Releases the flock and closes a locked file handle. Wasm handles are opaque and request-local. diff --git a/site/doc/pages/file_copy.txt b/site/doc/pages/file_copy.txt new file mode 100644 index 0000000..d05b3e7 --- /dev/null +++ b/site/doc/pages/file_copy.txt @@ -0,0 +1,3 @@ +file_copy + +Structural filesystem operation across the wasm host membrane. All path arguments are policy-gated through the same guest file/write roots as other file APIs and return false on denial or OS error. diff --git a/site/doc/pages/file_fsync.txt b/site/doc/pages/file_fsync.txt new file mode 100644 index 0000000..ecb8804 --- /dev/null +++ b/site/doc/pages/file_fsync.txt @@ -0,0 +1,3 @@ +file_fsync + +Filesystem nicety added to the wasm host membrane. Path arguments are policy-gated; operations return an empty value or false on denial/error. file_fsync() takes an open file handle and flushes it with fsync(). diff --git a/site/doc/pages/file_get_contents.txt b/site/doc/pages/file_get_contents.txt index c35694b..f7acd56 100644 --- a/site/doc/pages/file_get_contents.txt +++ b/site/doc/pages/file_get_contents.txt @@ -11,8 +11,9 @@ return value : String containing the file's contents :content Reads the file identified by `file_name` and returns it as a `String`. -If the file cannot be read, this function returns an empty string. +The read transparently takes a shared file lock for the duration of the call. If another worker is writing the same file with `file_put_contents()` or `file_append()`, this read waits for that exclusive write lock to finish, so callers do not manage locks manually. +If the file cannot be read, this function returns an empty string. ## Related Concepts - PHP: `file_get_contents()` diff --git a/site/doc/pages/file_get_contents_locked_fd.txt b/site/doc/pages/file_get_contents_locked_fd.txt deleted file mode 100644 index 42a3a42..0000000 --- a/site/doc/pages/file_get_contents_locked_fd.txt +++ /dev/null @@ -1,14 +0,0 @@ -:sig -String file_get_contents_locked_fd(int fd) - -:params -fd : handle returned by `file_open_locked()` -return value : complete file contents, or an empty string on error/empty file - -:see ->sys ->file_open_locked ->file_put_contents_locked_fd - -:content -Reads the full contents of a locked file handle from the start of the file. diff --git a/site/doc/pages/file_open.txt b/site/doc/pages/file_open.txt new file mode 100644 index 0000000..73e6f02 --- /dev/null +++ b/site/doc/pages/file_open.txt @@ -0,0 +1,5 @@ +file_open + +Streaming/handle-based file I/O across the wasm host membrane. Handles are opaque u64 values returned by file_open(); 0 means open failed (including path policy denial or bounded lock timeout). Locks are automatic and lifetime-scoped: read opens take a shared lock, write/append/read-write opens take an exclusive lock, and file_close() releases it. Lock wait is bounded by UCE_FILE_LOCK_TIMEOUT_MS (default 2000ms). + +See also: file_get_contents, file_put_contents, file_append. diff --git a/site/doc/pages/file_open_locked.txt b/site/doc/pages/file_open_locked.txt deleted file mode 100644 index b1d6740..0000000 --- a/site/doc/pages/file_open_locked.txt +++ /dev/null @@ -1,18 +0,0 @@ -:sig -int file_open_locked(String file_name, int open_flags, int lock_type = LOCK_SH, int create_mode = 0644, f64 wait_timeout_seconds = 3.0, String purpose = "") - -:params -file_name : path to open -open_flags : host open(2) flags -lock_type : `LOCK_SH` or `LOCK_EX` -create_mode : mode used when creating -return value : opaque locked file handle, or -1 - -:see ->sys ->file_close_locked ->file_get_contents_locked_fd ->file_put_contents_locked_fd - -:content -Opens and locks a file on the host. In wasm units the returned integer is an opaque worker-owned handle that is valid only for the current request. diff --git a/site/doc/pages/file_pread.txt b/site/doc/pages/file_pread.txt new file mode 100644 index 0000000..8eded7f --- /dev/null +++ b/site/doc/pages/file_pread.txt @@ -0,0 +1,5 @@ +file_pread + +Streaming/handle-based file I/O across the wasm host membrane. Handles are opaque u64 values returned by file_open(); 0 means open failed (including path policy denial or bounded lock timeout). Locks are automatic and lifetime-scoped: read opens take a shared lock, write/append/read-write opens take an exclusive lock, and file_close() releases it. Lock wait is bounded by UCE_FILE_LOCK_TIMEOUT_MS (default 2000ms). + +See also: file_get_contents, file_put_contents, file_append. diff --git a/site/doc/pages/file_put_contents.txt b/site/doc/pages/file_put_contents.txt index ebc2240..7be63b7 100644 --- a/site/doc/pages/file_put_contents.txt +++ b/site/doc/pages/file_put_contents.txt @@ -12,6 +12,7 @@ return value : true if write was successful :content Writes `content` into the file identified by `file_name`, overwriting any pre-existing content. +The write transparently takes an exclusive file lock for the duration of the truncate-and-write operation. Concurrent writers are serialized, and `file_get_contents()` waits for in-progress writes to finish; callers do not manage locks manually. ## Related Concepts - PHP: `file_put_contents()` diff --git a/site/doc/pages/file_put_contents_locked_fd.txt b/site/doc/pages/file_put_contents_locked_fd.txt deleted file mode 100644 index 56f9cfa..0000000 --- a/site/doc/pages/file_put_contents_locked_fd.txt +++ /dev/null @@ -1,15 +0,0 @@ -:sig -bool file_put_contents_locked_fd(int fd, String content) - -:params -fd : handle returned by `file_open_locked()` -content : bytes to write -return value : true on complete write - -:see ->sys ->file_open_locked ->file_get_contents_locked_fd - -:content -Truncates and rewrites the file behind a locked file handle. diff --git a/site/doc/pages/file_pwrite.txt b/site/doc/pages/file_pwrite.txt new file mode 100644 index 0000000..d5ae18b --- /dev/null +++ b/site/doc/pages/file_pwrite.txt @@ -0,0 +1,5 @@ +file_pwrite + +Streaming/handle-based file I/O across the wasm host membrane. Handles are opaque u64 values returned by file_open(); 0 means open failed (including path policy denial or bounded lock timeout). Locks are automatic and lifetime-scoped: read opens take a shared lock, write/append/read-write opens take an exclusive lock, and file_close() releases it. Lock wait is bounded by UCE_FILE_LOCK_TIMEOUT_MS (default 2000ms). + +See also: file_get_contents, file_put_contents, file_append. diff --git a/site/doc/pages/file_read.txt b/site/doc/pages/file_read.txt new file mode 100644 index 0000000..3aa926b --- /dev/null +++ b/site/doc/pages/file_read.txt @@ -0,0 +1,5 @@ +file_read + +Streaming/handle-based file I/O across the wasm host membrane. Handles are opaque u64 values returned by file_open(); 0 means open failed (including path policy denial or bounded lock timeout). Locks are automatic and lifetime-scoped: read opens take a shared lock, write/append/read-write opens take an exclusive lock, and file_close() releases it. Lock wait is bounded by UCE_FILE_LOCK_TIMEOUT_MS (default 2000ms). + +See also: file_get_contents, file_put_contents, file_append. diff --git a/site/doc/pages/file_readlink.txt b/site/doc/pages/file_readlink.txt new file mode 100644 index 0000000..abba584 --- /dev/null +++ b/site/doc/pages/file_readlink.txt @@ -0,0 +1,3 @@ +file_readlink + +Filesystem nicety added to the wasm host membrane. Path arguments are policy-gated; operations return an empty value or false on denial/error. file_fsync() takes an open file handle and flushes it with fsync(). diff --git a/site/doc/pages/file_release_process_locks.txt b/site/doc/pages/file_release_process_locks.txt deleted file mode 100644 index fa11b44..0000000 --- a/site/doc/pages/file_release_process_locks.txt +++ /dev/null @@ -1,12 +0,0 @@ -:sig -void file_release_process_locks(String reason = "") - -:params -reason : diagnostic reason for releasing locks - -:see ->sys ->file_open_locked - -:content -Releases locked file handles owned by the current process/workspace. Wasm uses this to close all request-local locked file handles. diff --git a/site/doc/pages/file_rename.txt b/site/doc/pages/file_rename.txt new file mode 100644 index 0000000..72d9923 --- /dev/null +++ b/site/doc/pages/file_rename.txt @@ -0,0 +1,3 @@ +file_rename + +Structural filesystem operation across the wasm host membrane. All path arguments are policy-gated through the same guest file/write roots as other file APIs and return false on denial or OS error. diff --git a/site/doc/pages/file_seek.txt b/site/doc/pages/file_seek.txt new file mode 100644 index 0000000..444fae9 --- /dev/null +++ b/site/doc/pages/file_seek.txt @@ -0,0 +1,5 @@ +file_seek + +Streaming/handle-based file I/O across the wasm host membrane. Handles are opaque u64 values returned by file_open(); 0 means open failed (including path policy denial or bounded lock timeout). Locks are automatic and lifetime-scoped: read opens take a shared lock, write/append/read-write opens take an exclusive lock, and file_close() releases it. Lock wait is bounded by UCE_FILE_LOCK_TIMEOUT_MS (default 2000ms). + +See also: file_get_contents, file_put_contents, file_append. diff --git a/site/doc/pages/file_stat.txt b/site/doc/pages/file_stat.txt new file mode 100644 index 0000000..f251ecc --- /dev/null +++ b/site/doc/pages/file_stat.txt @@ -0,0 +1,3 @@ +DValue file_stat(String path) + +Returns `{ exists, size, mtime, ctime, mode, is_dir, is_file, is_symlink }` for a policy-gated path. Missing or denied paths return `exists=false`. diff --git a/site/doc/pages/file_symlink.txt b/site/doc/pages/file_symlink.txt new file mode 100644 index 0000000..d007f7a --- /dev/null +++ b/site/doc/pages/file_symlink.txt @@ -0,0 +1,3 @@ +file_symlink + +Filesystem nicety added to the wasm host membrane. Path arguments are policy-gated; operations return an empty value or false on denial/error. file_fsync() takes an open file handle and flushes it with fsync(). diff --git a/site/doc/pages/file_tell.txt b/site/doc/pages/file_tell.txt new file mode 100644 index 0000000..2406cbd --- /dev/null +++ b/site/doc/pages/file_tell.txt @@ -0,0 +1,5 @@ +file_tell + +Streaming/handle-based file I/O across the wasm host membrane. Handles are opaque u64 values returned by file_open(); 0 means open failed (including path policy denial or bounded lock timeout). Locks are automatic and lifetime-scoped: read opens take a shared lock, write/append/read-write opens take an exclusive lock, and file_close() releases it. Lock wait is bounded by UCE_FILE_LOCK_TIMEOUT_MS (default 2000ms). + +See also: file_get_contents, file_put_contents, file_append. diff --git a/site/doc/pages/file_temp.txt b/site/doc/pages/file_temp.txt new file mode 100644 index 0000000..e84a00a --- /dev/null +++ b/site/doc/pages/file_temp.txt @@ -0,0 +1,3 @@ +file_temp + +Filesystem nicety added to the wasm host membrane. Path arguments are policy-gated; operations return an empty value or false on denial/error. file_fsync() takes an open file handle and flushes it with fsync(). diff --git a/site/doc/pages/file_truncate.txt b/site/doc/pages/file_truncate.txt new file mode 100644 index 0000000..c3e21e9 --- /dev/null +++ b/site/doc/pages/file_truncate.txt @@ -0,0 +1,3 @@ +file_truncate + +Structural filesystem operation across the wasm host membrane. All path arguments are policy-gated through the same guest file/write roots as other file APIs and return false on denial or OS error. diff --git a/site/doc/pages/file_write.txt b/site/doc/pages/file_write.txt new file mode 100644 index 0000000..cf88557 --- /dev/null +++ b/site/doc/pages/file_write.txt @@ -0,0 +1,5 @@ +file_write + +Streaming/handle-based file I/O across the wasm host membrane. Handles are opaque u64 values returned by file_open(); 0 means open failed (including path policy denial or bounded lock timeout). Locks are automatic and lifetime-scoped: read opens take a shared lock, write/append/read-write opens take an exclusive lock, and file_close() releases it. Lock wait is bounded by UCE_FILE_LOCK_TIMEOUT_MS (default 2000ms). + +See also: file_get_contents, file_put_contents, file_append. diff --git a/site/doc/pages/hmac_sha256.txt b/site/doc/pages/hmac_sha256.txt new file mode 100644 index 0000000..5794869 --- /dev/null +++ b/site/doc/pages/hmac_sha256.txt @@ -0,0 +1,7 @@ +# hmac_sha256 + +```cpp +String hmac_sha256(String key, String data) +``` + +Returns the raw 32-byte HMAC-SHA-256 digest for `data` keyed by `key`. diff --git a/site/doc/pages/hmac_sha256_hex.txt b/site/doc/pages/hmac_sha256_hex.txt new file mode 100644 index 0000000..1f7d5d0 --- /dev/null +++ b/site/doc/pages/hmac_sha256_hex.txt @@ -0,0 +1,7 @@ +# hmac_sha256_hex + +```cpp +String hmac_sha256_hex(String key, String data) +``` + +Returns the lowercase hexadecimal HMAC-SHA-256 digest. diff --git a/site/doc/pages/http_request.txt b/site/doc/pages/http_request.txt new file mode 100644 index 0000000..ade2eed --- /dev/null +++ b/site/doc/pages/http_request.txt @@ -0,0 +1,9 @@ +# http_request + +```cpp +DValue http_request(DValue req) +``` + +Performs a bounded outbound HTTP(S) request using the runtime `curl` binary. Request fields: `method`, `url`, `headers`, `body`, `timeout_ms`, `follow_redirects`. + +Returns `{ status, headers, body, error }`. `headers` is a name/value map. A missing `curl` binary returns a clear `error` string. diff --git a/site/doc/pages/http_request_async.txt b/site/doc/pages/http_request_async.txt new file mode 100644 index 0000000..b2be57b --- /dev/null +++ b/site/doc/pages/http_request_async.txt @@ -0,0 +1,7 @@ +# http_request_async + +```cpp +u64 http_request_async(DValue req) +``` + +Starts the same bounded curl-backed request as `http_request()` in the file-backed async job registry and returns a job id. Use `job_await()` or `job_result()` to retrieve the HTTP result. diff --git a/site/doc/pages/job_await.txt b/site/doc/pages/job_await.txt new file mode 100644 index 0000000..0ea706a --- /dev/null +++ b/site/doc/pages/job_await.txt @@ -0,0 +1,7 @@ +# job_await + +```cpp +DValue job_await(u64 job_id, u64 timeout_ms) +``` + +Waits up to `timeout_ms` for a job to finish, then returns status/result data. The wait is always bounded and returns with `state=running` if the job is still active. diff --git a/site/doc/pages/job_cancel.txt b/site/doc/pages/job_cancel.txt new file mode 100644 index 0000000..c234b0a --- /dev/null +++ b/site/doc/pages/job_cancel.txt @@ -0,0 +1,7 @@ +# job_cancel + +```cpp +bool job_cancel(u64 job_id) +``` + +Attempts to terminate the background job process group and marks the registry entry as `cancelled`. diff --git a/site/doc/pages/job_result.txt b/site/doc/pages/job_result.txt new file mode 100644 index 0000000..fb600a0 --- /dev/null +++ b/site/doc/pages/job_result.txt @@ -0,0 +1,7 @@ +# job_result + +```cpp +DValue job_result(u64 job_id) +``` + +Checks a job result with a small bounded wait. The returned value includes the current status fields and, when complete, a structured `result` value. diff --git a/site/doc/pages/job_status.txt b/site/doc/pages/job_status.txt new file mode 100644 index 0000000..bd5869e --- /dev/null +++ b/site/doc/pages/job_status.txt @@ -0,0 +1,7 @@ +# job_status + +```cpp +DValue job_status(u64 job_id) +``` + +Returns the file-backed async job state, e.g. `{ state, done, kind, pid, job_id }`. States include `pending`, `running`, `done`, `failed`, `cancelled`, and `missing`. diff --git a/site/doc/pages/random_bytes.txt b/site/doc/pages/random_bytes.txt new file mode 100644 index 0000000..d190537 --- /dev/null +++ b/site/doc/pages/random_bytes.txt @@ -0,0 +1,7 @@ +# random_bytes + +```cpp +String random_bytes(u64 n) +``` + +Returns up to `n` bytes from the host CSPRNG. Requests are capped to a bounded size. diff --git a/site/doc/pages/sha256.txt b/site/doc/pages/sha256.txt new file mode 100644 index 0000000..b39fdb5 --- /dev/null +++ b/site/doc/pages/sha256.txt @@ -0,0 +1,7 @@ +# sha256 + +```cpp +String sha256(String data) +``` + +Returns the raw 32-byte SHA-256 digest for `data`. Use `sha256_hex()` for printable lowercase hex. diff --git a/site/doc/pages/sha256_hex.txt b/site/doc/pages/sha256_hex.txt new file mode 100644 index 0000000..0a48c84 --- /dev/null +++ b/site/doc/pages/sha256_hex.txt @@ -0,0 +1,7 @@ +# sha256_hex + +```cpp +String sha256_hex(String data) +``` + +Returns the lowercase hexadecimal SHA-256 digest for `data`. diff --git a/site/doc/pages/shell_spawn.txt b/site/doc/pages/shell_spawn.txt new file mode 100644 index 0000000..b0efcab --- /dev/null +++ b/site/doc/pages/shell_spawn.txt @@ -0,0 +1,9 @@ +# shell_spawn + +```cpp +u64 shell_spawn(DValue spec) +``` + +Starts a bounded background shell job and returns a job id. `spec` fields: `cmd`, optional `stdin`, optional `env` map, optional `timeout_ms`. + +Use `job_status()`, `job_await()`, `job_result()`, or `job_cancel()` with the returned id. diff --git a/site/tests/io.uce b/site/tests/io.uce index d253699..1bfd8ba 100644 --- a/site/tests/io.uce +++ b/site/tests/io.uce @@ -31,9 +31,9 @@ RENDER(Request& context) check("path_join()", file_name == "/tmp/uce-site-tests-io.txt", file_name); check("basename() / dirname() / expand_path()", basename(file_name) == "uce-site-tests-io.txt" && dirname(file_name) == "/tmp" && expand_path("child", "/tmp/base") == "/tmp/base/child" && contains(expand_path("../sibling", "/tmp/base"), "sibling"), basename(file_name) + " / " + dirname(file_name) + " / " + expand_path("../sibling", "/tmp/base")); check("file_put_contents()", write_ok, file_name); - check("file_append() / file_append_contents()", file_append_contents(file_name, "|gamma") && contains(file_get_contents(file_name), "|gamma"), file_get_contents(file_name)); + check("file_append() automatic exclusive lock", file_append(file_name, "|gamma") && contains(file_get_contents(file_name), "|gamma"), file_get_contents(file_name)); file_put_contents(file_name, "alpha|beta"); - check("file_get_contents()", file_text == "alpha|beta", file_text); + check("file_get_contents() automatic shared lock", file_text == "alpha|beta", file_text); check("file_mtime()", file_mtime(file_name) > 0, std::to_string((u64)file_mtime(file_name))); String real_tmp = path_real("/tmp"); @@ -42,17 +42,88 @@ RENDER(Request& context) String shell_escaped = shell_escape("a'b; echo injected"); String shell_out = shell_exec("printf %s " + shell_escaped); - check("shell_escape() / shell_exec()", shell_out == "a'b; echo injected" && contains(shell_escaped, "'\\''"), shell_escaped + " => " + shell_out); + DValue shell_spec; + shell_spec["cmd"] = "printf out; printf err >&2"; + shell_spec["timeout_ms"] = (f64)500; + DValue shell_dv = shell_exec(shell_spec); + check("shell_escape() / shell_exec()", shell_out == "a'b; echo injected" && shell_dv["stdout"].to_string() == "out" && shell_dv["stderr"].to_string() == "err" && shell_dv["exit_code"].to_u64() == 0 && contains(shell_escaped, "'\\''"), shell_escaped + " => " + shell_out + " / " + json_encode(shell_dv)); - String lock_file = "/tmp/uce-site-tests-lock.txt"; - int lock_fd = file_open_locked(lock_file, 66, LOCK_EX, 0644, 3.0, "site tests"); - bool lock_write = file_put_contents_locked_fd(lock_fd, "locked-content"); - String lock_read = file_get_contents_locked_fd(lock_fd); - file_close_locked(lock_fd); - check("file_open_locked() / locked fd read-write / close", lock_fd > 0 && lock_write && lock_read == "locked-content" && file_get_contents(lock_file) == "locked-content", "fd=" + std::to_string(lock_fd) + " read=" + lock_read); - int release_fd = file_open_locked(lock_file, 66, LOCK_EX, 0644, 3.0, "release test"); - file_release_process_locks("site tests release"); - check("file_release_process_locks()", release_fd > 0, "fd=" + std::to_string(release_fd)); + DValue spawn_spec; + spawn_spec["cmd"] = "printf spawned"; + spawn_spec["timeout_ms"] = (f64)500; + u64 job_id = shell_spawn(spawn_spec); + DValue job_waited = job_await(job_id, 3000); + DValue job_checked = job_status(job_id); + DValue job_res = job_result(job_id); + DValue cancel_spec; + cancel_spec["cmd"] = "sleep 2"; + cancel_spec["timeout_ms"] = (f64)5000; + u64 cancel_job = shell_spawn(cancel_spec); + bool cancel_ok = job_cancel(cancel_job); + check("shell_spawn() / async job registry", job_id > 0 && job_waited["done"].to_bool() && job_checked["state"].to_string() == "done" && job_res["result"]["stdout"].to_string() == "spawned" && cancel_ok, "job=" + std::to_string(job_id) + " wait=" + json_encode(job_waited) + " cancel=" + std::to_string(cancel_job)); + + DValue http_req; + http_req["method"] = "GET"; + http_req["url"] = "http://127.0.0.1/tests/security_headers.uce"; + http_req["headers"]["Host"] = "uce.openfu.com"; + http_req["timeout_ms"] = (f64)500; + DValue http_res = http_request(http_req); + u64 http_job = http_request_async(http_req); + DValue http_async = job_await(http_job, 1500); + check("http_request() / http_request_async()", http_res["status"].to_u64() == 500 && contains(http_res["body"].to_string(), "security header sanitizer test") && http_job > 0 && http_async["done"].to_bool() && http_async["result"]["status"].to_u64() == 500, json_encode(http_res) + " async=" + json_encode(http_async)); + + String auto_lock_file = "/tmp/uce-site-tests-auto-lock.txt"; + bool auto_lock_ok = file_put_contents(auto_lock_file, "one") && file_append(auto_lock_file, "|two") && file_get_contents(auto_lock_file) == "one|two"; + check("automatic file locking API shape", auto_lock_ok, file_get_contents(auto_lock_file)); + + String stream_file = "/tmp/uce-site-tests-stream.txt"; + u64 wh = file_open(stream_file, "w"); + u64 stream_write_ok = wh ? file_write(wh, "abcdef") : 0; + s64 stream_pos = file_tell(wh); + file_close(wh); + u64 rh = file_open(stream_file, "r"); + String stream_read = file_read(rh, 3); + String stream_pread = file_pread(rh, 2, 3); + s64 stream_seek = file_seek(rh, 1, 0); + String stream_after_seek = file_read(rh, 2); + file_close(rh); + u64 rwh = file_open(stream_file, "r+"); + u64 stream_pwrite_ok = file_pwrite(rwh, 3, "XYZ"); + file_close(rwh); + u64 ah = file_open(stream_file, "a"); + u64 stream_append_ok = file_write(ah, "!"); + file_close(ah); + check("streaming file handles", wh > 0 && rh > 0 && rwh > 0 && ah > 0 && stream_write_ok == 6 && stream_pos == 6 && stream_read == "abc" && stream_pread == "cde" && stream_seek == 1 && stream_after_seek == "bc" && stream_pwrite_ok == 3 && stream_append_ok == 1 && file_get_contents(stream_file) == "abcXYZ!", stream_read + " / " + stream_pread + " / " + file_get_contents(stream_file)); + + DValue st = file_stat(stream_file); + DValue dl = dir_list("/tmp"); + bool found_stream = false; + dl.each([&](const DValue& item, String key) { if(item.key("name") && item.key("name")->to_string() == "uce-site-tests-stream.txt") found_stream = true; }); + check("file_stat() / dir_list()", st["exists"].to_bool() && st["is_file"].to_bool() && st["size"].to_u64() == 7 && found_stream, json_encode(st)); + + String ops_src = "/tmp/uce-site-tests-ops-src.txt"; + String ops_copy = "/tmp/uce-site-tests-ops-copy.txt"; + String ops_renamed = "/tmp/uce-site-tests-ops-renamed.txt"; + file_put_contents(ops_src, "abcdef"); + bool copy_ok = file_copy(ops_src, ops_copy); + bool trunc_ok = file_truncate(ops_copy, 3); + bool rename_ok = file_rename(ops_copy, ops_renamed); + String dir_ops = "/tmp/uce-site-tests-rmdir"; + mkdir(dir_ops); + file_put_contents(path_join(dir_ops, "child.txt"), "child"); + bool dir_remove_ok = dir_remove(dir_ops, true); + check("file_rename() / file_copy() / file_truncate() / dir_remove()", copy_ok && trunc_ok && rename_ok && file_get_contents(ops_renamed) == "abc" && dir_remove_ok && !file_exists(dir_ops), file_get_contents(ops_renamed)); + + String tmp_created = file_temp("/tmp/uce-site-tests-temp-"); + bool chmod_ok = tmp_created != "" && file_chmod(tmp_created, 0600); + u64 fsync_h = file_open(tmp_created, "a"); + bool fsync_ok = fsync_h > 0 && file_write(fsync_h, "durable") == 7 && file_fsync(fsync_h); + file_close(fsync_h); + String link_path = "/tmp/uce-site-tests-link"; + file_unlink(link_path); + bool symlink_ok = file_symlink(tmp_created, link_path); + String readlink_value = file_readlink(link_path); + check("file_temp() / file_chmod() / file_symlink() / file_readlink() / file_fsync()", tmp_created != "" && file_exists(tmp_created) && chmod_ok && fsync_ok && symlink_ok && readlink_value == path_real(tmp_created), tmp_created + " -> " + readlink_value); String start_dir = process_start_directory(); String old_cwd = cwd_get(); @@ -79,6 +150,11 @@ RENDER(Request& context) String relative = time_format_relative(time() - 120, "recent %deltaS", 1, "medium %deltaM", 3600, "old %deltaH"); check("time_format_local() / time_format_relative() / time_parse()", parsed_epoch == 5 && utc_year == "1970" && local_year != "" && contains(relative, "medium"), "parsed=" + std::to_string(parsed_epoch) + " utc=" + utc_year + " local=" + local_year + " rel=" + relative); + String crypto_random = random_bytes(16); + String crypto_b64 = base64_encode("hello"); + String crypto_b64_dec = base64_decode(crypto_b64); + check("sha256() / hmac_sha256() / base64 / random_bytes() / crypto_equal()", sha256_hex("abc") == "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad" && hmac_sha256_hex("key", "The quick brown fox jumps over the lazy dog") == "f7bc83f430538424b13298e6aa6fb143ef4d59a14946175997479dbc2d1a3cd8" && crypto_b64 == "aGVsbG8=" && crypto_b64_dec == "hello" && crypto_random.length() == 16 && crypto_equal("same", "same") && !crypto_equal("same", "diff"), crypto_b64 + " / " + sha256_hex("abc")); + StringList escaped_keys = memcache_escape_keys({"a b", "line\nkey"}); check("memcache_escape_key() / memcache_escape_keys()", memcache_escape_key("a b") == "a_b" && escaped_keys.size() == 2 && !contains(escaped_keys[1], "\n"), join(escaped_keys, ",")); check("signal_name() / runtime_safe_key() / backtrace helpers", signal_name(SIGSEGV) == "SIGSEGV" && runtime_safe_key(" task one ") == gen_sha1("task one") && runtime_safe_key(" ") == "" && capture_backtrace_string() == "" && backtrace_frames_string(0, 0) == "", signal_name(SIGSEGV) + " / " + runtime_safe_key(" task one ")); diff --git a/src/lib/compiler.cpp b/src/lib/compiler.cpp index da8b64e..09a5f6a 100644 --- a/src/lib/compiler.cpp +++ b/src/lib/compiler.cpp @@ -5,12 +5,13 @@ #include #include #include +#include #include #include namespace { -const u64 UCE_UNIT_ABI_VERSION = 7; +const u64 UCE_UNIT_ABI_VERSION = 8; struct SharedUnitFilesystemState { @@ -210,15 +211,28 @@ int compiler_open_lock_file(String file_name, String purpose) auto lock_dir = dirname(file_name); if(lock_dir != "") mkdir(lock_dir); - int fdlock = file_open_locked(file_name, O_RDWR | O_CREAT, LOCK_EX, 0666); + int fdlock = open(file_name.c_str(), O_RDWR | O_CREAT, 0666); if(fdlock == -1) + { printf("(!) Could not open lock file %s\n", file_name.c_str()); + return(fdlock); + } + fcntl(fdlock, F_SETFD, FD_CLOEXEC); + if(flock(fdlock, LOCK_EX) != 0) + { + close(fdlock); + printf("(!) Could not lock file %s\n", file_name.c_str()); + return(-1); + } return(fdlock); } void compiler_close_lock_file(int fdlock) { - file_close_locked(fdlock); + if(fdlock == -1) + return; + flock(fdlock, LOCK_UN); + close(fdlock); } String compiler_normalize_unit_path(Request* context, String file_name) diff --git a/src/lib/hash.cpp b/src/lib/hash.cpp index 71c8505..fc4208d 100644 --- a/src/lib/hash.cpp +++ b/src/lib/hash.cpp @@ -344,3 +344,80 @@ f64 draw_float(f64 from, f64 to, f64 decimal_precision) return(gen_float(from, to, context->random_index++, context->random_seed, decimal_precision)); } + +namespace { +struct SHA256_CTX_UCE { u8 data[64]; u32 datalen; unsigned long long bitlen; u32 state[8]; }; +#define UCE_SHA256_ROTR(a,b) (((a) >> (b)) | ((a) << (32-(b)))) +#define UCE_SHA256_CH(x,y,z) (((x) & (y)) ^ (~(x) & (z))) +#define UCE_SHA256_MAJ(x,y,z) (((x) & (y)) ^ ((x) & (z)) ^ ((y) & (z))) +#define UCE_SHA256_EP0(x) (UCE_SHA256_ROTR(x,2) ^ UCE_SHA256_ROTR(x,13) ^ UCE_SHA256_ROTR(x,22)) +#define UCE_SHA256_EP1(x) (UCE_SHA256_ROTR(x,6) ^ UCE_SHA256_ROTR(x,11) ^ UCE_SHA256_ROTR(x,25)) +#define UCE_SHA256_SIG0(x) (UCE_SHA256_ROTR(x,7) ^ UCE_SHA256_ROTR(x,18) ^ ((x) >> 3)) +#define UCE_SHA256_SIG1(x) (UCE_SHA256_ROTR(x,17) ^ UCE_SHA256_ROTR(x,19) ^ ((x) >> 10)) +static const u32 uce_sha256_k[64] = { + 0x428a2f98,0x71374491,0xb5c0fbcf,0xe9b5dba5,0x3956c25b,0x59f111f1,0x923f82a4,0xab1c5ed5, + 0xd807aa98,0x12835b01,0x243185be,0x550c7dc3,0x72be5d74,0x80deb1fe,0x9bdc06a7,0xc19bf174, + 0xe49b69c1,0xefbe4786,0x0fc19dc6,0x240ca1cc,0x2de92c6f,0x4a7484aa,0x5cb0a9dc,0x76f988da, + 0x983e5152,0xa831c66d,0xb00327c8,0xbf597fc7,0xc6e00bf3,0xd5a79147,0x06ca6351,0x14292967, + 0x27b70a85,0x2e1b2138,0x4d2c6dfc,0x53380d13,0x650a7354,0x766a0abb,0x81c2c92e,0x92722c85, + 0xa2bfe8a1,0xa81a664b,0xc24b8b70,0xc76c51a3,0xd192e819,0xd6990624,0xf40e3585,0x106aa070, + 0x19a4c116,0x1e376c08,0x2748774c,0x34b0bcb5,0x391c0cb3,0x4ed8aa4a,0x5b9cca4f,0x682e6ff3, + 0x748f82ee,0x78a5636f,0x84c87814,0x8cc70208,0x90befffa,0xa4506ceb,0xbef9a3f7,0xc67178f2 }; +static void uce_sha256_transform(SHA256_CTX_UCE* ctx, const u8 data[]) +{ + u32 m[64]; + for(u32 i=0,j=0; i<16; ++i,j+=4) m[i]=((u32)data[j]<<24)|((u32)data[j+1]<<16)|((u32)data[j+2]<<8)|((u32)data[j+3]); + for(u32 i=16; i<64; ++i) m[i]=UCE_SHA256_SIG1(m[i-2])+m[i-7]+UCE_SHA256_SIG0(m[i-15])+m[i-16]; + u32 a=ctx->state[0],b=ctx->state[1],c=ctx->state[2],d=ctx->state[3],e=ctx->state[4],f=ctx->state[5],g=ctx->state[6],h=ctx->state[7]; + for(u32 i=0; i<64; ++i) { u32 t1=h+UCE_SHA256_EP1(e)+UCE_SHA256_CH(e,f,g)+uce_sha256_k[i]+m[i]; u32 t2=UCE_SHA256_EP0(a)+UCE_SHA256_MAJ(a,b,c); h=g; g=f; f=e; e=d+t1; d=c; c=b; b=a; a=t1+t2; } + ctx->state[0]+=a; ctx->state[1]+=b; ctx->state[2]+=c; ctx->state[3]+=d; ctx->state[4]+=e; ctx->state[5]+=f; ctx->state[6]+=g; ctx->state[7]+=h; +} +static void uce_sha256_init(SHA256_CTX_UCE* ctx) +{ + ctx->datalen=0; ctx->bitlen=0; ctx->state[0]=0x6a09e667; ctx->state[1]=0xbb67ae85; ctx->state[2]=0x3c6ef372; ctx->state[3]=0xa54ff53a; ctx->state[4]=0x510e527f; ctx->state[5]=0x9b05688c; ctx->state[6]=0x1f83d9ab; ctx->state[7]=0x5be0cd19; +} +static void uce_sha256_update(SHA256_CTX_UCE* ctx, const u8 data[], size_t len) +{ + for(size_t i=0; idata[ctx->datalen++]=data[i]; if(ctx->datalen==64) { uce_sha256_transform(ctx,ctx->data); ctx->bitlen += 512; ctx->datalen=0; } } +} +static void uce_sha256_final(SHA256_CTX_UCE* ctx, u8 hash[]) +{ + u32 i=ctx->datalen; + ctx->data[i++]=0x80; + if(i>56) { while(i<64) ctx->data[i++]=0; uce_sha256_transform(ctx,ctx->data); i=0; } + while(i<56) ctx->data[i++]=0; + ctx->bitlen += (unsigned long long)ctx->datalen * 8ull; + for(int j=7; j>=0; --j) ctx->data[63-j]=(u8)(ctx->bitlen >> (j*8)); + uce_sha256_transform(ctx,ctx->data); + for(i=0; i<4; ++i) for(u32 j=0; j<8; ++j) hash[i + j*4] = (u8)((ctx->state[j] >> (24 - i*8)) & 0xff); +} +} + +String sha256_native(String data) +{ + u8 digest[32]; SHA256_CTX_UCE ctx; uce_sha256_init(&ctx); uce_sha256_update(&ctx, (const u8*)data.data(), data.size()); uce_sha256_final(&ctx, digest); + return(String((const char*)digest, 32)); +} +String sha256_hex_native(String data) +{ + String digest = sha256_native(data), out; for(unsigned char c : digest) out += to_hex(c, 2); return(to_lower(out)); +} +String hmac_sha256_native(String key, String data) +{ + if(key.size() > 64) key = sha256_native(key); + key.resize(64, '\0'); + String o(64, '\0'), i(64, '\0'); + for(size_t n=0; n<64; n++) { o[n] = key[n] ^ 0x5c; i[n] = key[n] ^ 0x36; } + return(sha256_native(o + sha256_native(i + data))); +} +String hmac_sha256_hex_native(String key, String data) +{ + String digest = hmac_sha256_native(key, data), out; for(unsigned char c : digest) out += to_hex(c, 2); return(to_lower(out)); +} +bool crypto_equal_native(String a, String b) +{ + u8 diff = (u8)(a.size() ^ b.size()); + size_t n = a.size() > b.size() ? a.size() : b.size(); + for(size_t i=0; i 1024 * 1024) n = 1024 * 1024; + String out(n, 0); size_t got = n ? uce_host_random(&out[0], n) : 0; out.resize(got <= n ? got : 0); return(out); +} +bool crypto_equal(String a, String b) { return(uce_host_crypto_equal(a.data(), a.size(), b.data(), b.size()) != 0); } + + +DValue http_request(DValue req) +{ + String encoded = ucb_encode(req); + size_t required = uce_host_http_request(encoded.data(), encoded.size(), 0, 0); + String out(required, 0); size_t got = required ? uce_host_http_request(encoded.data(), encoded.size(), &out[0], required) : 0; out.resize(got <= required ? got : 0); return(wasm_decode_dvalue_result(out)); +} +u64 http_request_async(DValue req) +{ + String encoded = ucb_encode(req); + return((u64)uce_host_http_request_async(encoded.data(), encoded.size())); +} + +DValue shell_exec(DValue spec) +{ + String encoded = ucb_encode(spec); + size_t required = uce_host_shell_exec_dv(encoded.data(), encoded.size(), 0, 0); + String out(required, 0); + size_t got = required ? uce_host_shell_exec_dv(encoded.data(), encoded.size(), &out[0], required) : 0; + out.resize(got <= required ? got : 0); + return(wasm_decode_dvalue_result(out)); +} +u64 shell_spawn(DValue spec) +{ + String encoded = ucb_encode(spec); + return((u64)uce_host_shell_spawn(encoded.data(), encoded.size())); +} +DValue job_status(u64 job_id) +{ + size_t required = uce_host_job_status(job_id, 0, 0); + String out(required, 0); + size_t got = required ? uce_host_job_status(job_id, &out[0], required) : 0; + out.resize(got <= required ? got : 0); + return(wasm_decode_dvalue_result(out)); +} +DValue job_result(u64 job_id) +{ + size_t required = uce_host_job_result(job_id, 0, 0); + String out(required, 0); + size_t got = required ? uce_host_job_result(job_id, &out[0], required) : 0; + out.resize(got <= required ? got : 0); + return(wasm_decode_dvalue_result(out)); +} +DValue job_await(u64 job_id, u64 timeout_ms) +{ + size_t required = uce_host_job_await(job_id, timeout_ms, 0, 0); + String out(required, 0); + size_t got = required ? uce_host_job_await(job_id, timeout_ms, &out[0], required) : 0; + out.resize(got <= required ? got : 0); + return(wasm_decode_dvalue_result(out)); +} +bool job_cancel(u64 job_id) { return(uce_host_job_cancel(job_id) != 0); } + String cwd_get() { size_t required = uce_host_cwd_get(0, 0); @@ -522,6 +716,15 @@ StringMap default_config() #include #include "sys.h" #include "hash.h" +#include "uri.h" + +String sha256(String data) { return(sha256_native(data)); } +String sha256_hex(String data) { return(sha256_hex_native(data)); } +String hmac_sha256(String key, String data) { return(hmac_sha256_native(key, data)); } +String hmac_sha256_hex(String key, String data) { return(hmac_sha256_hex_native(key, data)); } +String base64_decode(String raw) { bool ok=false; return(::base64_decode(raw, ok)); } +String random_bytes(u64 n) { if(n > 1024*1024) n = 1024*1024; String out(n, 0); int fd=open("/dev/urandom", O_RDONLY); if(fd<0) return(""); size_t off=0; while(off= deadline) + { + fprintf(stderr, "(!) file lock timeout after %llums: %s\n", (unsigned long long)timeout, file_name.c_str()); + close(fd); + return(-1); + } + usleep(10000); + } +} + +void close_locked_file(int fd) +{ + if(fd == -1) + return; + flock(fd, LOCK_UN); + close(fd); +} } @@ -737,36 +992,9 @@ bool file_exists(String path) return(std::filesystem::exists(fp)); } -int file_open_locked(String file_name, int open_flags, int lock_type, int create_mode, f64 wait_timeout_seconds, String purpose) -{ - (void)wait_timeout_seconds; - (void)purpose; - int fd = open(file_name.c_str(), open_flags, create_mode); - if(fd == -1) - return(-1); - fcntl(fd, F_SETFD, FD_CLOEXEC); - if(flock(fd, lock_type) != 0) - { - close(fd); - return(-1); - } - return(fd); -} +namespace { -void file_close_locked(int fd) -{ - if(fd == -1) - return; - flock(fd, LOCK_UN); - close(fd); -} - -void file_release_process_locks(String reason) -{ - (void)reason; -} - -String file_get_contents_locked_fd(int fd) +String file_read_all(int fd) { if(fd == -1) return(""); @@ -780,7 +1008,6 @@ String file_get_contents_locked_fd(int fd) return(content); } -namespace { bool file_write_all(int fd, const char* data, size_t remaining) { @@ -803,41 +1030,30 @@ bool file_write_all(int fd, const char* data, size_t remaining) } -bool file_put_contents_locked_fd(int fd, String content) -{ - if(fd == -1) - return(false); - lseek(fd, 0, SEEK_SET); - if(ftruncate(fd, 0) != 0) - return(false); - if(!file_write_all(fd, content.data(), content.length())) - return(false); - return(true); -} - String file_get_contents(String file_name) { - s32 fd = file_open_locked(file_name, O_RDONLY, LOCK_SH, 0644, FILE_LOCK_WAIT_TIMEOUT_SECONDS, "file_get_contents:" + file_name); + s32 fd = open_locked_file(file_name, O_RDONLY, LOCK_SH); if(fd == -1) { printf("(!) Could not read %s\n", file_name.c_str()); return(""); } - String content = file_get_contents_locked_fd(fd); - file_close_locked(fd); + String content = file_read_all(fd); + close_locked_file(fd); return(content); } bool file_put_contents(String file_name, String content) { - s32 fd = file_open_locked(file_name, O_RDWR | O_CREAT, LOCK_EX, 0644, FILE_LOCK_WAIT_TIMEOUT_SECONDS, "file_put_contents:" + file_name); + s32 fd = open_locked_file(file_name, O_RDWR | O_CREAT, LOCK_EX, 0644); if(fd == -1) { printf("(!) Could not write %s\n", file_name.c_str()); return(false); } - bool ok = file_put_contents_locked_fd(fd, content); - file_close_locked(fd); + lseek(fd, 0, SEEK_SET); + bool ok = ftruncate(fd, 0) == 0 && file_write_all(fd, content.data(), content.length()); + close_locked_file(fd); if(!ok) { printf("(!) Could not fully write %s\n", file_name.c_str()); @@ -846,9 +1062,9 @@ bool file_put_contents(String file_name, String content) return(true); } -bool file_append_contents(String file_name, String content) +bool file_append(String file_name, String content) { - s32 fd = file_open_locked(file_name, O_RDWR | O_CREAT, LOCK_EX, 0644, FILE_LOCK_WAIT_TIMEOUT_SECONDS, "file_append:" + file_name); + s32 fd = open_locked_file(file_name, O_RDWR | O_CREAT, LOCK_EX, 0644); if(fd == -1) { printf("(!) Could not append %s\n", file_name.c_str()); @@ -856,7 +1072,7 @@ bool file_append_contents(String file_name, String content) } lseek(fd, 0, SEEK_END); bool ok = file_write_all(fd, content.data(), content.length()); - file_close_locked(fd); + close_locked_file(fd); if(!ok) { printf("(!) Could not fully append %s\n", file_name.c_str()); @@ -1154,7 +1370,6 @@ pid_t spawn_subprocess(std::function exec_after_spawn) p = fork(); if(p == 0) { - file_release_process_locks("fork child startup"); my_pid = getpid(); //printf("(C) child procress started, PID:%i\n", my_pid); prctl(PR_SET_PDEATHSIG, SIGHUP); @@ -1264,7 +1479,7 @@ pid_t task_pid(String key) { String status_file_name = task_file_prefix(key); String lock_file_name = status_file_name + ".lock"; - int lock_fd = file_open_locked(lock_file_name, O_RDWR | O_CREAT, LOCK_EX, 0644, FILE_LOCK_WAIT_TIMEOUT_SECONDS, "task-pid:" + key); + int lock_fd = open_locked_file(lock_file_name, O_RDWR | O_CREAT, LOCK_EX, 0644); if(lock_fd == -1) { fprintf(stderr, "task_pid(): could not lock task key '%s'\n", key.c_str()); @@ -1276,12 +1491,12 @@ pid_t task_pid(String key) TaskStatus status = task_status_parse(status_file); if(task_status_is_alive(status)) { - file_close_locked(lock_fd); + close_locked_file(lock_fd); return(status.pid); } file_unlink(status_file_name); } - file_close_locked(lock_fd); + close_locked_file(lock_fd); return(0); } @@ -1289,7 +1504,7 @@ pid_t task(String key, std::function exec_after_spawn, u64 timeout) { String status_file_name = task_file_prefix(key); String lock_file_name = status_file_name + ".lock"; - int lock_fd = file_open_locked(lock_file_name, O_RDWR | O_CREAT, LOCK_EX, 0644, FILE_LOCK_WAIT_TIMEOUT_SECONDS, "task:" + key); + int lock_fd = open_locked_file(lock_file_name, O_RDWR | O_CREAT, LOCK_EX, 0644); if(lock_fd == -1) { fprintf(stderr, "task(): could not lock task key '%s'\n", key.c_str()); @@ -1303,7 +1518,7 @@ pid_t task(String key, std::function exec_after_spawn, u64 timeout) if(task_status_is_alive(status)) { printf("(P) worker process '%s' already running: PID %i\n", key.c_str(), status.pid); - file_close_locked(lock_fd); + close_locked_file(lock_fd); return(status.pid); } file_unlink(status_file_name); @@ -1312,13 +1527,12 @@ pid_t task(String key, std::function exec_after_spawn, u64 timeout) if(p < 0) { fprintf(stderr, "task(): fork failed for key '%s': %s\n", key.c_str(), strerror(errno)); - file_close_locked(lock_fd); + close_locked_file(lock_fd); return(0); } if(p == 0) { - file_release_process_locks("task child startup"); - file_close_locked(lock_fd); + close_locked_file(lock_fd); my_pid = getpid(); signal(SIGALRM, SIG_DFL); if(timeout > 0) @@ -1331,11 +1545,11 @@ pid_t task(String key, std::function exec_after_spawn, u64 timeout) } task_close_inherited_fds(); exec_after_spawn(); - int exit_lock_fd = file_open_locked(lock_file_name, O_RDWR | O_CREAT, LOCK_EX, 0644, FILE_LOCK_WAIT_TIMEOUT_SECONDS, "task-exit:" + key); + int exit_lock_fd = open_locked_file(lock_file_name, O_RDWR | O_CREAT, LOCK_EX, 0644); if(exit_lock_fd != -1) { file_unlink(status_file_name); - file_close_locked(exit_lock_fd); + close_locked_file(exit_lock_fd); } else { @@ -1349,10 +1563,10 @@ pid_t task(String key, std::function exec_after_spawn, u64 timeout) { fprintf(stderr, "task(): could not write status file for key '%s'; terminating child PID %i\n", key.c_str(), p); kill(p, SIGTERM); - file_close_locked(lock_fd); + close_locked_file(lock_fd); return(0); } - file_close_locked(lock_fd); + close_locked_file(lock_fd); printf("(P) worker process '%s' spawned: PID %i\n", key.c_str(), p); return(p); } @@ -1425,6 +1639,8 @@ StringMap make_server_settings() // command batches here at workspace teardown. cfg["WS_BROKER_SOCKET_PATH"] = "/run/uce/ws-broker.sock"; cfg["WS_BROKER_OUTBOUND_TIMEOUT_SECONDS"] = "30"; + // Comma-separated uce_host_* names a sysadmin disables; empty = nothing blocked. + cfg["UCE_HOSTCALL_BLOCKLIST"] = ""; cfg["TMP_UPLOAD_PATH"] = "/tmp/uce/uploads"; cfg["SESSION_PATH"] = "/tmp/uce/sessions"; cfg["COMPILER_SYS_PATH"] = "."; diff --git a/src/lib/sys.h b/src/lib/sys.h index 73e7d25..01b81cb 100644 --- a/src/lib/sys.h +++ b/src/lib/sys.h @@ -25,7 +25,24 @@ int usleep(unsigned int usec); #include #include +struct DValue; + String shell_exec(String cmd); +DValue http_request(DValue req); +u64 http_request_async(DValue req); +DValue shell_exec(DValue spec); +u64 shell_spawn(DValue spec); +DValue job_status(u64 job_id); +DValue job_result(u64 job_id); +DValue job_await(u64 job_id, u64 timeout_ms); +bool job_cancel(u64 job_id); +String sha256(String data); +String sha256_hex(String data); +String hmac_sha256(String key, String data); +String hmac_sha256_hex(String key, String data); +String base64_decode(String raw); +String random_bytes(u64 n); +bool crypto_equal(String a, String b); String shell_escape(String raw); String basename(String fn); String dirname(String fn); @@ -34,20 +51,34 @@ String path_real(String path); bool path_is_within(String path, String root); bool mkdir(String path); bool file_exists(String path); -int file_open_locked(String file_name, int open_flags, int lock_type = LOCK_SH, int create_mode = 0644, f64 wait_timeout_seconds = 3.0, String purpose = ""); -void file_close_locked(int fd); -void file_release_process_locks(String reason = ""); -String file_get_contents_locked_fd(int fd); -bool file_put_contents_locked_fd(int fd, String content); String file_get_contents(String file_name); bool file_put_contents(String file_name, String content); -bool file_append_contents(String file_name, String content); +bool file_append(String file_name, String content); +u64 file_open(String path, String mode); +String file_read(u64 h, u64 len); +String file_pread(u64 h, u64 offset, u64 len); +u64 file_write(u64 h, String data); +u64 file_pwrite(u64 h, u64 offset, String data); +s64 file_seek(u64 h, s64 offset, int whence); +s64 file_tell(u64 h); +void file_close(u64 h); +DValue file_stat(String path); +DValue dir_list(String path); +bool file_rename(String from, String to); +bool file_copy(String from, String to); +bool file_truncate(String path, u64 size); +bool dir_remove(String path, bool recursive = false); +String file_temp(String prefix); +bool file_chmod(String path, u32 mode); +bool file_symlink(String target, String linkpath); +String file_readlink(String path); +bool file_fsync(u64 h); template inline bool file_append(String file_name, Ts... args) { std::ostringstream out; ((out << args), ...); - return(file_append_contents(file_name, out.str())); + return(file_append(file_name, out.str())); } String cwd_get(); void cwd_set(String path); @@ -61,7 +92,6 @@ u64 time(); String time_format_local(String format = "", u64 timestamp = 0); // Runtime timing/profiling snapshot for the active wasm request/workspace. -struct DValue; DValue request_perf(); String time_format_utc(String format = "", u64 timestamp = 0); diff --git a/src/linux_fastcgi.cpp b/src/linux_fastcgi.cpp index dbf8135..eec8ffb 100644 --- a/src/linux_fastcgi.cpp +++ b/src/linux_fastcgi.cpp @@ -130,7 +130,7 @@ void render_request_failure(Request& request, String title, String details, Stri if(!request.resources.is_cli) { DValue error_info; - error_info["type"] = "runtime_error"; + error_info["type"] = (title == "function disabled by server policy") ? "policy_blocked" : "runtime_error"; error_info["title"] = title; error_info["details"] = details; error_info["source"] = request.params["SCRIPT_FILENAME"]; @@ -533,9 +533,23 @@ int handle_complete(FastCGIRequest& request) { request_fault_active = 1; if(wasm_error != "") { - failure_title = "wasm runtime error during request"; - failure_details = ""; - failure_trace = wasm_error; + size_t blocked_at = wasm_error.find("UCE_POLICY_BLOCKED:"); + if(blocked_at != String::npos) + { + String fn = wasm_error.substr(blocked_at + 19); + size_t fn_end = fn.find_first_of(" \t\r\n\""); + if(fn_end != String::npos) + fn = fn.substr(0, fn_end); + failure_title = "function disabled by server policy"; + failure_details = "this unit called " + fn + ", which is disabled on this server by configuration (UCE_HOSTCALL_BLOCKLIST)"; + failure_trace = ""; + } + else + { + failure_title = "wasm runtime error during request"; + failure_details = ""; + failure_trace = wasm_error; + } } }; @@ -1307,7 +1321,6 @@ void ensure_proactive_compiler() } if(p == 0) { - file_release_process_locks("proactive compiler fork"); prctl(PR_SET_PDEATHSIG, SIGHUP); run_proactive_compiler(); exit(0); @@ -1329,7 +1342,6 @@ void listen_for_connections() server.on_cli_complete = &handle_cli_complete; for(;;) { - file_release_process_locks("worker loop cleanup"); server.process(-1); } } diff --git a/src/wasm/backend.cpp b/src/wasm/backend.cpp index f5a1c49..57df54c 100644 --- a/src/wasm/backend.cpp +++ b/src/wasm/backend.cpp @@ -53,6 +53,17 @@ static String wasm_backend_ensure_started(Request* context) wc.memory_limit = (int64_t)to_u64(cfg["WASM_MEMORY_LIMIT_BYTES"], 512ull * 1024 * 1024); wc.epoch_deadline_ticks = to_u64(cfg["WASM_EPOCH_DEADLINE_TICKS"], 200); wc.verbose = to_bool(cfg["WASM_BACKEND_VERBOSE"], false); + // UCE_HOSTCALL_BLOCKLIST: comma-separated uce_host_* names (with or without + // the "uce_host_" prefix) the sysadmin disables; each blocked call traps into + // the error page (see make_host_import). Parsed once here, per worker process. + for(String entry : split(cfg["UCE_HOSTCALL_BLOCKLIST"], ",")) + { + entry = trim(entry); + if(entry.rfind("uce_host_", 0) == 0) + entry = entry.substr(9); + if(entry != "") + wc.hostcall_blocklist.insert(entry); + } g_wasm_worker = new WasmWorker(wc); g_wasm_init_error = g_wasm_worker->init(); @@ -131,6 +142,12 @@ String wasm_backend_serve(Request& request, const String& entry_unit, const Stri // Raw request body: cli_input() parses a JSON CLI payload from context.in, // and serve_http handlers read it as req->in; carried into the workspace. ctx["in"] = request.in; + // Configurable error pages read context.call["error"] (the error_info DValue + // set natively in render_wasm_error_page). apply_context binds context.call to + // the decoded ctx, so marshal the native request.call children across the + // membrane — otherwise the error page renders with no error data. + for(auto& entry : request.call._map) + ctx[entry.first] = entry.second; // WebSocket event context: the workspace owns no connections, so the frame's // connection identity goes in and the handler's ws_send/ws_close dispatch // commands come back out (below) for the native broker to apply. diff --git a/src/wasm/core_hostcalls.syms b/src/wasm/core_hostcalls.syms index 5047b36..7d87911 100644 --- a/src/wasm/core_hostcalls.syms +++ b/src/wasm/core_hostcalls.syms @@ -3,6 +3,13 @@ uce_host_time_precise uce_host_env uce_host_log uce_host_random +uce_host_sha256 +uce_host_sha256_hex +uce_host_hmac_sha256 +uce_host_hmac_sha256_hex +uce_host_base64_encode +uce_host_base64_decode +uce_host_crypto_equal uce_host_task_spawn uce_host_task_pid uce_host_task_kill @@ -20,6 +27,14 @@ uce_host_units uce_host_component_resolve uce_host_request_perf uce_host_shell_exec +uce_host_http_request +uce_host_http_request_async +uce_host_shell_exec_dv +uce_host_shell_spawn +uce_host_job_status +uce_host_job_result +uce_host_job_await +uce_host_job_cancel uce_host_file_exists uce_host_file_read uce_host_file_write @@ -27,11 +42,25 @@ uce_host_file_unlink uce_host_file_list uce_host_file_mkdir uce_host_file_mtime -uce_host_file_open_locked -uce_host_file_close_locked -uce_host_file_release_process_locks -uce_host_file_read_locked_fd -uce_host_file_write_locked_fd +uce_host_file_open +uce_host_file_handle_read +uce_host_file_handle_pread +uce_host_file_handle_write +uce_host_file_handle_pwrite +uce_host_file_handle_seek +uce_host_file_handle_tell +uce_host_file_handle_close +uce_host_file_stat +uce_host_dir_list +uce_host_file_rename +uce_host_file_copy +uce_host_file_truncate +uce_host_dir_remove +uce_host_file_temp +uce_host_file_chmod +uce_host_file_symlink +uce_host_file_readlink +uce_host_file_fsync uce_host_path_real uce_host_path_is_within uce_host_cwd_get diff --git a/src/wasm/worker.cpp b/src/wasm/worker.cpp index efa915c..7a0d6c8 100644 --- a/src/wasm/worker.cpp +++ b/src/wasm/worker.cpp @@ -31,7 +31,9 @@ #include #include #include +#include #include +#include #include #include #include @@ -49,6 +51,9 @@ #include #include #include +#include +#include +#include struct WasmDylinkInfo { @@ -86,6 +91,10 @@ struct WasmWorkerConfig u32 table_headroom = 4096; u64 epoch_deadline_ticks = 200; // ticker period × ticks = CPU budget bool verbose = false; + // uce_host_* names (bare, without the "uce_host_" prefix) the sysadmin has + // disabled via UCE_HOSTCALL_BLOCKLIST. A blocked hostcall resolves to a trap + // stub at workspace birth (see make_host_import); empty = feature off. + std::set hostcall_blocklist; }; struct WasmResponse @@ -100,6 +109,345 @@ struct WasmResponse u64 component_resolve_total_us = 0; }; +static u64 wasm_file_lock_timeout_ms() +{ + const char* raw = getenv("UCE_FILE_LOCK_TIMEOUT_MS"); + if(!raw || !*raw) + return(2000); + char* end = 0; + unsigned long long parsed = strtoull(raw, &end, 10); + return(end == raw ? 2000 : (u64)parsed); +} + +static u64 wasm_monotonic_ms() +{ + struct timespec ts; + clock_gettime(CLOCK_MONOTONIC, &ts); + return((u64)ts.tv_sec * 1000ull + (u64)ts.tv_nsec / 1000000ull); +} + +static int wasm_open_locked_file(const String& file_name, int flags, int lock_type, bool truncate_after_lock) +{ + int fd = open(file_name.c_str(), flags, 0644); + if(fd < 0) + return(-1); + fcntl(fd, F_SETFD, FD_CLOEXEC); + u64 timeout = wasm_file_lock_timeout_ms(); + u64 deadline = wasm_monotonic_ms() + timeout; + while(true) + { + if(flock(fd, lock_type | LOCK_NB) == 0) + { + if(truncate_after_lock && ftruncate(fd, 0) != 0) + { + flock(fd, LOCK_UN); + close(fd); + return(-1); + } + return(fd); + } + if(errno != EWOULDBLOCK && errno != EAGAIN && errno != EINTR) + { + close(fd); + return(-1); + } + if(timeout == 0 || wasm_monotonic_ms() >= deadline) + { + fprintf(stderr, "[wasm] file lock timeout after %llums: %s\n", (unsigned long long)timeout, file_name.c_str()); + close(fd); + return(-1); + } + usleep(10000); + } +} + +static bool wasm_fd_write_all(int fd, const char* data, size_t remaining, u64* written_out) +{ + u64 total = 0; + while(remaining > 0) + { + ssize_t n = write(fd, data, remaining); + if(n < 0) + { + if(errno == EINTR) + continue; + return(false); + } + if(n == 0) + return(false); + data += n; + remaining -= (size_t)n; + total += (u64)n; + } + if(written_out) + *written_out = total; + return(true); +} + + + +// ---- file-backed async job registry + bounded process execution ---------- + +static String uce_job_root() +{ + const char* env = getenv("UCE_JOB_ROOT"); + String root = (env && *env) ? String(env) : String("/run/uce/jobs"); + std::error_code ec; + std::filesystem::create_directories(root, ec); + if(ec) + { + root = "/tmp/uce/jobs"; + std::filesystem::create_directories(root, ec); + } + return(root); +} + +static String uce_job_path(u64 id) { return(uce_job_root() + "/" + std::to_string(id)); } +static String uce_read_text(const String& path) { std::ifstream in(path, std::ios::binary); if(!in) return(""); std::ostringstream ss; ss << in.rdbuf(); return(ss.str()); } +static void uce_write_text(const String& path, const String& data) { std::ofstream out(path, std::ios::binary|std::ios::trunc); out.write(data.data(), (std::streamsize)data.size()); } + +static u64 uce_job_new(const String& kind) +{ + String root = uce_job_root(); + std::error_code ec; + std::filesystem::create_directories(root, ec); + u64 seed = ((u64)time(0) << 32) ^ ((u64)getpid() << 16) ^ (u64)rand(); + for(int i = 0; i < 100; i++) + { + u64 id = seed ^ (wasm_monotonic_ms() + (u64)i * 0x9e3779b97f4a7c15ull); + String dir = root + "/" + std::to_string(id); + if(mkdir(dir.c_str(), 0700) == 0) + { + uce_write_text(dir + "/kind", kind); + uce_write_text(dir + "/created", std::to_string((u64)time(0))); + uce_write_text(dir + "/state", "pending"); + return(id); + } + } + return(0); +} + +static void uce_job_reap() +{ + String root = uce_job_root(); + u64 now = (u64)time(0); + u64 ttl = 3600; + if(const char* raw = getenv("UCE_JOB_TTL_SECONDS")) { char* e=0; unsigned long long v=strtoull(raw,&e,10); if(e!=raw && v>0) ttl=(u64)v; } + std::error_code ec; + for(auto& e : std::filesystem::directory_iterator(root, ec)) + { + if(!e.is_directory()) continue; + u64 created = strtoull(uce_read_text(e.path().string()+"/created").c_str(), 0, 10); + if(created > 0 && now > created + ttl) + std::filesystem::remove_all(e.path(), ec); + } +} + +static DValue uce_process_exec(String cmd, String input, StringMap env, u64 timeout_ms) +{ + DValue r; + r["exit_code"] = (f64)-1; + r["stdout"] = ""; + r["stderr"] = ""; + r["timed_out"].set_bool(false); + if(timeout_ms == 0) timeout_ms = 5000; + int inpipe[2], outpipe[2], errpipe[2]; + if(pipe(inpipe) || pipe(outpipe) || pipe(errpipe)) { r["stderr"]="pipe failed"; return(r); } + pid_t pid = fork(); + if(pid == 0) + { + dup2(inpipe[0], 0); dup2(outpipe[1], 1); dup2(errpipe[1], 2); + close(inpipe[0]); close(inpipe[1]); close(outpipe[0]); close(outpipe[1]); close(errpipe[0]); close(errpipe[1]); + for(auto& kv : env) setenv(kv.first.c_str(), kv.second.c_str(), 1); + execl("/bin/sh", "sh", "-c", cmd.c_str(), (char*)0); + _exit(127); + } + close(inpipe[0]); close(outpipe[1]); close(errpipe[1]); + fcntl(outpipe[0], F_SETFL, fcntl(outpipe[0], F_GETFL, 0) | O_NONBLOCK); + fcntl(errpipe[0], F_SETFL, fcntl(errpipe[0], F_GETFL, 0) | O_NONBLOCK); + size_t input_off = 0; bool in_open = true, out_open = true, err_open = true; int status = 0; bool exited = false; + u64 deadline = wasm_monotonic_ms() + timeout_ms; + while(out_open || err_open || !exited) + { + if(!exited) + { + pid_t w = waitpid(pid, &status, WNOHANG); + if(w == pid) exited = true; + } + if(in_open) + { + if(input_off < input.size()) { ssize_t n=write(inpipe[1], input.data()+input_off, input.size()-input_off); if(n>0) input_off += (size_t)n; else if(n<0 && errno!=EINTR) { close(inpipe[1]); in_open=false; } } + else { close(inpipe[1]); in_open=false; } + } + char buf[4096]; + ssize_t n; + while((n=read(outpipe[0], buf, sizeof(buf))) > 0) r["stdout"] = r["stdout"].to_string() + String(buf, n); + if(n == 0 && out_open) { close(outpipe[0]); out_open=false; } + while((n=read(errpipe[0], buf, sizeof(buf))) > 0) r["stderr"] = r["stderr"].to_string() + String(buf, n); + if(n == 0 && err_open) { close(errpipe[0]); err_open=false; } + if(!exited && wasm_monotonic_ms() >= deadline) + { + r["timed_out"].set_bool(true); + kill(pid, SIGKILL); + waitpid(pid, &status, 0); + exited = true; + } + if((out_open || err_open || !exited)) usleep(10000); + } + if(WIFEXITED(status)) { r["exit_code"] = (f64)WEXITSTATUS(status); r["timed_out"].set_bool(false); } + else if(WIFSIGNALED(status)) r["exit_code"] = (f64)(128 + WTERMSIG(status)); + return(r); +} + +static DValue uce_shell_exec_spec(const DValue& spec) +{ + return(uce_process_exec(spec.key("cmd") ? spec.key("cmd")->to_string() : String(""), spec.key("stdin") ? spec.key("stdin")->to_string() : String(""), spec.key("env") ? spec.key("env")->to_stringmap() : StringMap(), spec.key("timeout_ms") ? spec.key("timeout_ms")->to_u64(5000) : 5000)); +} + +static void uce_job_finish(u64 id, DValue result, String final_state="done") +{ + String dir = uce_job_path(id); + uce_write_text(dir + "/result.tmp", ucb_encode(result)); + rename((dir + "/result.tmp").c_str(), (dir + "/result").c_str()); + uce_write_text(dir + "/state", final_state); +} + +static u64 uce_shell_spawn_spec(const DValue& spec) +{ + uce_job_reap(); + u64 id = uce_job_new("shell"); + if(!id) return(0); + pid_t pid = fork(); + if(pid == 0) + { + setsid(); + uce_write_text(uce_job_path(id) + "/worker_pid", std::to_string((long long)getpid())); + uce_write_text(uce_job_path(id) + "/state", "running"); + DValue result = uce_shell_exec_spec(spec); + uce_job_finish(id, result, "done"); + _exit(0); + } + if(pid < 0) { DValue r; r["error"]="fork failed"; uce_job_finish(id,r,"failed"); return(id); } + uce_write_text(uce_job_path(id) + "/worker_pid", std::to_string((long long)pid)); + uce_write_text(uce_job_path(id) + "/state", "running"); + return(id); +} + + +static DValue uce_exec_argv_capture(std::vector argv, String input, u64 timeout_ms) +{ + DValue r; r["exit_code"]=(f64)-1; r["stdout"]=""; r["stderr"]=""; r["timed_out"].set_bool(false); + if(argv.empty()) { r["stderr"]="empty argv"; return(r); } + if(timeout_ms == 0) timeout_ms = 5000; + int inpipe[2], outpipe[2], errpipe[2]; + if(pipe(inpipe)||pipe(outpipe)||pipe(errpipe)) { r["stderr"]="pipe failed"; return(r); } + pid_t pid=fork(); + if(pid==0) + { + dup2(inpipe[0],0); dup2(outpipe[1],1); dup2(errpipe[1],2); + close(inpipe[0]); close(inpipe[1]); close(outpipe[0]); close(outpipe[1]); close(errpipe[0]); close(errpipe[1]); + std::vector args; for(auto& a: argv) args.push_back((char*)a.c_str()); args.push_back(0); + execvp(args[0], args.data()); _exit(127); + } + close(inpipe[0]); close(outpipe[1]); close(errpipe[1]); + fcntl(outpipe[0], F_SETFL, fcntl(outpipe[0], F_GETFL, 0)|O_NONBLOCK); fcntl(errpipe[0], F_SETFL, fcntl(errpipe[0], F_GETFL, 0)|O_NONBLOCK); + size_t input_off=0; bool in_open=true,out_open=true,err_open=true,exited=false; int status=0; u64 deadline=wasm_monotonic_ms()+timeout_ms; + while(out_open || err_open || !exited) + { + if(!exited) { pid_t w=waitpid(pid,&status,WNOHANG); if(w==pid) exited=true; } + if(in_open) { if(input_off0) input_off+=(size_t)n; else if(n<0 && errno!=EINTR) { close(inpipe[1]); in_open=false; } } else { close(inpipe[1]); in_open=false; } } + char buf[4096]; ssize_t n; while((n=read(outpipe[0],buf,sizeof(buf)))>0) r["stdout"] = r["stdout"].to_string()+String(buf,n); if(n==0&&out_open){close(outpipe[0]);out_open=false;} + while((n=read(errpipe[0],buf,sizeof(buf)))>0) r["stderr"] = r["stderr"].to_string()+String(buf,n); if(n==0&&err_open){close(errpipe[0]);err_open=false;} + if(!exited && wasm_monotonic_ms() >= deadline) { r["timed_out"].set_bool(true); kill(pid,SIGKILL); waitpid(pid,&status,0); exited=true; } + if(out_open || err_open || !exited) usleep(10000); + } + if(WIFEXITED(status)) { r["exit_code"]=(f64)WEXITSTATUS(status); r["timed_out"].set_bool(false); } else if(WIFSIGNALED(status)) r["exit_code"]=(f64)(128+WTERMSIG(status)); + return(r); +} + +static bool uce_header_name_safe(String name) +{ + if(name=="") return(false); + for(unsigned char c: name) if(!(isalnum(c)||c=='-'||c=='_')) return(false); + return(true); +} + +static DValue uce_http_request_value(const DValue& req) +{ + DValue r; r["status"]=(f64)0; r["headers"].set_array(); r["body"]=""; r["error"]=""; + String method = req.key("method") ? to_upper(req.key("method")->to_string()) : String("GET"); + String url = req.key("url") ? req.key("url")->to_string() : String(""); + if(url=="" || url.find('\0')!=String::npos) { r["error"]="missing url"; return(r); } + if(method=="") method="GET"; + std::vector argv = {"curl", "-sS", "--http1.0", "-X", method, "-D", "-", "-w", "\nUCE_HTTP_STATUS:%{http_code}", "--max-time", std::to_string(std::max(1, (req.key("timeout_ms") ? req.key("timeout_ms")->to_u64(5000) : 5000) / 1000))}; + if(req.key("follow_redirects") && req.key("follow_redirects")->to_bool()) argv.push_back("-L"); + if(req.key("headers")) req.key("headers")->each([&](const DValue& v, String k){ if(uce_header_name_safe(k)) { argv.push_back("-H"); argv.push_back(k + ": " + replace(replace(v.to_string(), "\r", " "), "\n", " ")); } }); + String body = req.key("body") ? req.key("body")->to_string() : String(""); + if(req.key("body")) { argv.push_back("--data-binary"); argv.push_back("@-"); } + argv.push_back(url); + if(access("/usr/bin/curl", X_OK)!=0 && access("/bin/curl", X_OK)!=0) { r["error"]="curl binary not found in runtime PATH"; return(r); } + DValue pr = uce_exec_argv_capture(argv, body, req.key("timeout_ms") ? req.key("timeout_ms")->to_u64(5000) : 5000); + String out = pr["stdout"].to_string(); + String marker="\nUCE_HTTP_STATUS:"; size_t mp=out.rfind(marker); + if(mp!=String::npos) { r["status"]=(f64)strtoull(out.c_str()+mp+marker.size(),0,10); out=out.substr(0,mp); } + else r["error"]="curl did not report status"; + String sep="\r\n\r\n"; size_t hp=out.rfind(sep); size_t sep_len=4; if(hp==String::npos) { sep="\n\n"; hp=out.rfind(sep); sep_len=2; } + String hdrs = hp==String::npos ? String("") : out.substr(0,hp); r["body"] = hp==String::npos ? out : out.substr(hp+sep_len); + DValue headers; + for(String line: split(replace(hdrs,"\r",""), "\n")) { size_t c=line.find(':'); if(c!=String::npos) headers[trim(line.substr(0,c))] = trim(line.substr(c+1)); } + r["headers"] = headers; + if(pr["exit_code"].to_s64() != 0 && r["error"].to_string()=="") r["error"] = trim(pr["stderr"].to_string()); + return(r); +} + +static u64 uce_http_spawn_spec(const DValue& req) +{ + uce_job_reap(); u64 id=uce_job_new("http"); if(!id) return(0); + pid_t pid=fork(); + if(pid==0) { setsid(); uce_write_text(uce_job_path(id)+"/worker_pid", std::to_string((long long)getpid())); uce_write_text(uce_job_path(id)+"/state", "running"); DValue result=uce_http_request_value(req); uce_job_finish(id,result,result["error"].to_string()==""?"done":"failed"); _exit(0); } + if(pid<0) { DValue r; r["error"]="fork failed"; uce_job_finish(id,r,"failed"); return(id); } + uce_write_text(uce_job_path(id)+"/worker_pid", std::to_string((long long)pid)); uce_write_text(uce_job_path(id)+"/state", "running"); return(id); +} + +static DValue uce_job_status_value(u64 id) +{ + DValue r; String dir=uce_job_path(id); r["job_id"]=(f64)id; + if(id==0 || !std::filesystem::is_directory(dir)) { r["state"]="missing"; return(r); } + String state=trim(uce_read_text(dir+"/state")); if(state=="") state="pending"; r["state"]=state; + r["kind"]=trim(uce_read_text(dir+"/kind")); r["pid"]=(f64)strtoull(uce_read_text(dir+"/worker_pid").c_str(),0,10); + r["done"].set_bool(state=="done"||state=="failed"||state=="cancelled"); + return(r); +} + +static DValue uce_job_result_value(u64 id, u64 timeout_ms) +{ + u64 deadline = wasm_monotonic_ms() + timeout_ms; + while(timeout_ms > 0 && wasm_monotonic_ms() < deadline) + { + DValue st = uce_job_status_value(id); + if(st["done"].to_bool()) break; + usleep(10000); + } + DValue r = uce_job_status_value(id); + String encoded = uce_read_text(uce_job_path(id)+"/result"); + if(encoded != "") { DValue decoded; String err; if(ucb_decode(encoded, decoded, &err)) r["result"] = decoded; } + return(r); +} + +static bool uce_job_cancel_value(u64 id) +{ + DValue st = uce_job_status_value(id); + if(st["state"].to_string()=="missing") return(false); + String state = st["state"].to_string(); + if(state == "done" || state == "failed" || state == "cancelled") + return(false); + pid_t pid = (pid_t)st["pid"].to_u64(0); + if(pid > 0) kill(-pid, SIGKILL); + DValue result; result["cancelled"].set_bool(true); + uce_job_finish(id, result, "cancelled"); + return(true); +} + // ---- module byte parsing (hardened; carried from the phase 3 spike) ------- // included into both w3_driver.cpp and the native server TU (via backend.cpp); @@ -411,6 +759,13 @@ public: u64 component_resolve_count = 0; u64 component_resolve_total_us = 0; + struct FileHandle + { + int fd = -1; + bool writable = false; + }; + std::vector file_handles; + struct RequestPerfSnapshot { u64 worker_pid = 0; @@ -435,9 +790,6 @@ public: request_perf.active = true; } -// Host-owned opaque fd handles opened by wasm file_open_locked(). - std::vector locked_file_handles; - #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. @@ -446,12 +798,15 @@ public: #endif ~WasmWorkspace() { - for(int fd : locked_file_handles) - if(fd >= 0) + for(auto& h : file_handles) + { + if(h.fd >= 0) { - flock(fd, LOCK_UN); - ::close(fd); + flock(h.fd, LOCK_UN); + close(h.fd); + h.fd = -1; } + } #ifdef UCE_WASM_HOST_CONNECTORS for(auto* db : sqlite_handles) if(db) @@ -1124,7 +1479,7 @@ private: { char root_real[4096]; if(root != "" && realpath(root.c_str(), root_real)) - root_prefixes.push_back(String(root_real) + "/"); + root_prefixes.push_back(String(root_real)); } for(auto& candidate : candidates) { @@ -1134,7 +1489,7 @@ private: String path(resolved); bool allowed = false; for(auto& prefix : root_prefixes) - if(path.rfind(prefix, 0) == 0) + if(path == prefix || path.rfind(prefix + "/", 0) == 0) { allowed = true; break; @@ -1304,6 +1659,25 @@ private: return(host_funcs.back()); }; + // Hostcall blocklist (UCE_HOSTCALL_BLOCKLIST): a sysadmin-disabled hostcall + // resolves to a trap stub instead of its real implementation, so a unit + // invoking it fails at runtime into the configurable error page. The + // decision is made once per import at workspace birth — no per-call cost, + // and zero cost when nothing is blocked. A small core set stays exempt so + // the runtime itself cannot be bricked. + if(mod == "env" && !worker.cfg.hostcall_blocklist.empty() && name.rfind("uce_host_", 0) == 0) + { + static const std::set non_blockable = { "component_resolve" }; + String bare = name.substr(9); + if(worker.cfg.hostcall_blocklist.count(bare) && !non_blockable.count(bare)) + { + std::string blocked(name); + return(add([blocked](Caller, Span, Span) -> Result { + return(Trap("UCE_POLICY_BLOCKED:" + blocked)); + })); + } + } + if(mod == "env" && name == "uce_host_time") return(add([](Caller, Span, Span results) -> Result { results[0] = Val((int64_t)::time(0)); @@ -1367,6 +1741,20 @@ private: results[0] = Val((int32_t)bytes.size()); return(std::monostate()); })); + if(mod == "env" && name == "uce_host_sha256") + return(add([self](Caller, Span args, Span results) -> Result { String in; self->hostcall_read(args[0].i32(), args[1].i32(), in); String out=sha256_native(in); u32 cap=(u32)args[3].i32(); int32_t buf=args[2].i32(); if(buf&&cap>=out.size()) self->hostcall_write(buf,out); results[0]=Val((int32_t)out.size()); return(std::monostate()); })); + if(mod == "env" && name == "uce_host_sha256_hex") + return(add([self](Caller, Span args, Span results) -> Result { String in; self->hostcall_read(args[0].i32(), args[1].i32(), in); String out=sha256_hex_native(in); u32 cap=(u32)args[3].i32(); int32_t buf=args[2].i32(); if(buf&&cap>=out.size()) self->hostcall_write(buf,out); results[0]=Val((int32_t)out.size()); return(std::monostate()); })); + if(mod == "env" && name == "uce_host_hmac_sha256") + return(add([self](Caller, Span args, Span results) -> Result { String key,in; self->hostcall_read(args[0].i32(), args[1].i32(), key); self->hostcall_read(args[2].i32(), args[3].i32(), in); String out=hmac_sha256_native(key,in); u32 cap=(u32)args[5].i32(); int32_t buf=args[4].i32(); if(buf&&cap>=out.size()) self->hostcall_write(buf,out); results[0]=Val((int32_t)out.size()); return(std::monostate()); })); + if(mod == "env" && name == "uce_host_hmac_sha256_hex") + return(add([self](Caller, Span args, Span results) -> Result { String key,in; self->hostcall_read(args[0].i32(), args[1].i32(), key); self->hostcall_read(args[2].i32(), args[3].i32(), in); String out=hmac_sha256_hex_native(key,in); u32 cap=(u32)args[5].i32(); int32_t buf=args[4].i32(); if(buf&&cap>=out.size()) self->hostcall_write(buf,out); results[0]=Val((int32_t)out.size()); return(std::monostate()); })); + if(mod == "env" && name == "uce_host_base64_encode") + return(add([self](Caller, Span args, Span results) -> Result { String in; self->hostcall_read(args[0].i32(), args[1].i32(), in); String out=base64_encode(in); u32 cap=(u32)args[3].i32(); int32_t buf=args[2].i32(); if(buf&&cap>=out.size()) self->hostcall_write(buf,out); results[0]=Val((int32_t)out.size()); return(std::monostate()); })); + if(mod == "env" && name == "uce_host_base64_decode") + return(add([self](Caller, Span args, Span results) -> Result { String in; self->hostcall_read(args[0].i32(), args[1].i32(), in); bool ok=false; String out=base64_decode(in, ok); if(!ok) out=""; u32 cap=(u32)args[3].i32(); int32_t buf=args[2].i32(); if(buf&&cap>=out.size()) self->hostcall_write(buf,out); results[0]=Val((int32_t)out.size()); return(std::monostate()); })); + if(mod == "env" && name == "uce_host_crypto_equal") + return(add([self](Caller, Span args, Span results) -> Result { String a,b; self->hostcall_read(args[0].i32(), args[1].i32(), a); self->hostcall_read(args[2].i32(), args[3].i32(), b); results[0]=Val((int32_t)(crypto_equal_native(a,b)?1:0)); return(std::monostate()); })); if(mod == "env" && name == "uce_host_log") return(add([self](Caller, Span args, Span) -> Result { String text; @@ -1394,6 +1782,36 @@ private: results[0] = Val((int32_t)out.size()); return(std::monostate()); })); + if(mod == "env" && name == "uce_host_http_request") + return(add([self](Caller caller, Span args, Span results) -> Result { + String encoded; self->hostcall_read(args[0].i32(), args[1].i32(), encoded); u32 cap=(u32)args[3].i32(); int32_t buf=args[2].i32(); String out; String stage_key="http:"+encoded; + if(!self->hostcall_staged(stage_key,out)) { DValue req,response; String err; if(ucb_decode(encoded,req,&err)) response=uce_http_request_value(req); else response["error"]="http_request decode failed: "+err; out=ucb_encode(response); if(buf==0) self->hostcall_stage(stage_key,out); } + if(buf&&cap>=out.size()) self->hostcall_write(buf,out); caller.context().set_epoch_deadline(self->worker.cfg.epoch_deadline_ticks); results[0]=Val((int32_t)out.size()); return(std::monostate()); + })); + if(mod == "env" && name == "uce_host_http_request_async") + return(add([self](Caller, Span args, Span results) -> Result { String encoded; self->hostcall_read(args[0].i32(), args[1].i32(), encoded); DValue req; String err; u64 id=0; if(ucb_decode(encoded,req,&err)) id=uce_http_spawn_spec(req); results[0]=Val((int64_t)id); return(std::monostate()); })); + if(mod == "env" && name == "uce_host_shell_exec_dv") + return(add([self](Caller caller, Span args, Span results) -> Result { + String encoded; self->hostcall_read(args[0].i32(), args[1].i32(), encoded); + u32 cap=(u32)args[3].i32(); int32_t buf=args[2].i32(); + String out; String stage_key="shell_dv:"+encoded; + if(!self->hostcall_staged(stage_key,out)) { DValue spec, response; String err; if(ucb_decode(encoded,spec,&err)) response=uce_shell_exec_spec(spec); else response["error"]="shell_exec spec decode failed: "+err; out=ucb_encode(response); if(buf==0) self->hostcall_stage(stage_key,out); } + if(buf&&cap>=out.size()) self->hostcall_write(buf,out); + caller.context().set_epoch_deadline(self->worker.cfg.epoch_deadline_ticks); + results[0]=Val((int32_t)out.size()); return(std::monostate()); + })); + if(mod == "env" && name == "uce_host_shell_spawn") + return(add([self](Caller, Span args, Span results) -> Result { + String encoded; self->hostcall_read(args[0].i32(), args[1].i32(), encoded); DValue spec; String err; u64 id=0; if(ucb_decode(encoded,spec,&err)) id=uce_shell_spawn_spec(spec); results[0]=Val((int64_t)id); return(std::monostate()); + })); + if(mod == "env" && name == "uce_host_job_status") + return(add([self](Caller, Span args, Span results) -> Result { String out=ucb_encode(uce_job_status_value((u64)args[0].i64())); u32 cap=(u32)args[2].i32(); int32_t buf=args[1].i32(); if(buf&&cap>=out.size()) self->hostcall_write(buf,out); results[0]=Val((int32_t)out.size()); return(std::monostate()); })); + if(mod == "env" && name == "uce_host_job_result") + return(add([self](Caller, Span args, Span results) -> Result { String out=ucb_encode(uce_job_result_value((u64)args[0].i64(), 100)); u32 cap=(u32)args[2].i32(); int32_t buf=args[1].i32(); if(buf&&cap>=out.size()) self->hostcall_write(buf,out); results[0]=Val((int32_t)out.size()); return(std::monostate()); })); + if(mod == "env" && name == "uce_host_job_await") + return(add([self](Caller caller, Span args, Span results) -> Result { u64 timeout=std::min((u64)args[1].i64(), 30000); String out=ucb_encode(uce_job_result_value((u64)args[0].i64(), timeout)); u32 cap=(u32)args[3].i32(); int32_t buf=args[2].i32(); if(buf&&cap>=out.size()) self->hostcall_write(buf,out); caller.context().set_epoch_deadline(self->worker.cfg.epoch_deadline_ticks); results[0]=Val((int32_t)out.size()); return(std::monostate()); })); + if(mod == "env" && name == "uce_host_job_cancel") + return(add([](Caller, Span args, Span results) -> Result { results[0]=Val((int32_t)(uce_job_cancel_value((u64)args[0].i64())?1:0)); return(std::monostate()); })); if(mod == "env" && name == "uce_host_path_real") return(add([self](Caller, Span args, Span results) -> Result { String path; @@ -1484,109 +1902,25 @@ private: results[0] = Val((int64_t)mtime); return(std::monostate()); })); - if(mod == "env" && name == "uce_host_file_open_locked") - return(add([self](Caller, Span args, Span results) -> Result { - String path, purpose, current; - self->hostcall_read(args[0].i32(), args[1].i32(), path); - self->hostcall_read(args[6].i32(), args[7].i32(), purpose); - self->hostcall_read(args[8].i32(), args[9].i32(), current); - int open_flags = args[2].i32(); - bool may_write = (open_flags & (O_WRONLY | O_RDWR | O_CREAT | O_TRUNC | O_APPEND)) != 0; - String resolved = may_write ? self->resolve_guest_write(path, current) : self->resolve_guest_file(path, current); - int fd = resolved != "" ? ::file_open_locked(resolved, open_flags, args[3].i32(), args[4].i32(), args[5].f64(), purpose) : -1; - int handle = -1; - if(fd >= 0) - { - for(size_t i = 0; i < self->locked_file_handles.size(); i++) - if(self->locked_file_handles[i] < 0) - { - self->locked_file_handles[i] = fd; - handle = (int)i + 1; - break; - } - if(handle < 0) - { - self->locked_file_handles.push_back(fd); - handle = (int)self->locked_file_handles.size(); - } - } - results[0] = Val((int32_t)handle); - return(std::monostate()); - })); - if(mod == "env" && name == "uce_host_file_close_locked") - return(add([self](Caller, Span args, Span) -> Result { - int handle = args[0].i32(); - if(handle >= 1 && (size_t)handle <= self->locked_file_handles.size()) - { - int& fd = self->locked_file_handles[(size_t)handle - 1]; - if(fd >= 0) - { - ::file_close_locked(fd); - fd = -1; - } - } - return(std::monostate()); - })); - if(mod == "env" && name == "uce_host_file_release_process_locks") - return(add([self](Caller, Span, Span) -> Result { - for(int& fd : self->locked_file_handles) - if(fd >= 0) - { - ::file_close_locked(fd); - fd = -1; - } - return(std::monostate()); - })); - if(mod == "env" && name == "uce_host_file_read_locked_fd") - return(add([self](Caller, Span args, Span results) -> Result { - int handle = args[0].i32(); - String content; - if(handle >= 1 && (size_t)handle <= self->locked_file_handles.size()) - { - int fd = self->locked_file_handles[(size_t)handle - 1]; - if(fd >= 0) - content = ::file_get_contents_locked_fd(fd); - } - u32 cap = (u32)args[2].i32(); - int32_t buf = args[1].i32(); - if(buf != 0 && cap >= content.size()) - self->hostcall_write(buf, content); - results[0] = Val((int32_t)content.size()); - return(std::monostate()); - })); - if(mod == "env" && name == "uce_host_file_write_locked_fd") - return(add([self](Caller, Span args, Span results) -> Result { - int handle = args[0].i32(); - String content; - self->hostcall_read(args[1].i32(), args[2].i32(), content); - bool ok = false; - if(handle >= 1 && (size_t)handle <= self->locked_file_handles.size()) - { - int fd = self->locked_file_handles[(size_t)handle - 1]; - if(fd >= 0) - ok = ::file_put_contents_locked_fd(fd, content); - } - results[0] = Val(ok ? (int32_t)1 : (int32_t)0); - return(std::monostate()); - })); if(mod == "env" && name == "uce_host_file_read") return(add([self](Caller, Span args, Span results) -> Result { 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_file(path, current); - std::vector bytes; - if(resolved == "" || !wasm_read_file(resolved, bytes)) + String stage_key = "file_read:" + path + "\0" + current; + String content; + if(!self->hostcall_staged(stage_key, content)) { - results[0] = Val((int32_t)0); - return(std::monostate()); + String resolved = self->resolve_guest_file(path, current); + content = resolved == "" ? String("") : ::file_get_contents(resolved); + self->hostcall_stage(stage_key, content); } u32 cap = (u32)args[5].i32(); int32_t buf = args[4].i32(); // length-query convention: no copy unless the buffer fits - if(buf != 0 && cap >= bytes.size()) - self->hostcall_write(buf, String((const char*)bytes.data(), bytes.size())); - results[0] = Val((int32_t)bytes.size()); + if(buf != 0 && cap >= content.size()) + self->hostcall_write(buf, content); + results[0] = Val((int32_t)content.size()); return(std::monostate()); })); if(mod == "env" && name == "uce_host_file_list") @@ -1631,12 +1965,214 @@ private: 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); + ok = append ? file_append(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()); })); + if(mod == "env" && name == "uce_host_file_open") + return(add([self](Caller, Span args, Span results) -> Result { + String path, current, mode; + 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(), mode); + String resolved; + int flags = O_RDONLY; + int lock_type = LOCK_SH; + bool writable = false; + bool truncate_after_lock = false; + if(mode == "r") + resolved = self->resolve_guest_file(path, current); + else if(mode == "w") + { + resolved = self->resolve_guest_write(path, current); + flags = O_RDWR | O_CREAT; + lock_type = LOCK_EX; + writable = true; + truncate_after_lock = true; + } + else if(mode == "a") + { + resolved = self->resolve_guest_write(path, current); + flags = O_RDWR | O_CREAT | O_APPEND; + lock_type = LOCK_EX; + writable = true; + } + else if(mode == "r+") + { + resolved = self->resolve_guest_write(path, current); + flags = O_RDWR; + lock_type = LOCK_EX; + writable = true; + } + uint64_t handle = 0; + if(resolved != "") + { + int fd = wasm_open_locked_file(resolved, flags, lock_type, truncate_after_lock); + if(fd >= 0) + { + if(mode == "a") + lseek(fd, 0, SEEK_END); + self->file_handles.push_back({fd, writable}); + handle = self->file_handles.size(); + } + } + results[0] = Val((int64_t)handle); + return(std::monostate()); + })); + if(mod == "env" && name == "uce_host_file_handle_read") + return(add([self](Caller, Span args, Span results) -> Result { + u64 handle = (u64)args[0].i64(); + u64 len = (u64)args[1].i64(); + u32 cap = (u32)args[3].i32(); + int32_t buf = args[2].i32(); + String out; + String stage_key = "file_handle_read:" + std::to_string(handle) + ":" + std::to_string(len); + if(!self->hostcall_staged(stage_key, out)) + { + if(handle >= 1 && handle <= self->file_handles.size()) + { + int fd = self->file_handles[(size_t)handle - 1].fd; + if(fd >= 0 && len > 0) + { + out.resize((size_t)std::min(len, 16ull * 1024ull * 1024ull)); + ssize_t n = read(fd, &out[0], out.size()); + out.resize(n > 0 ? (size_t)n : 0); + } + } + if(buf == 0) self->hostcall_stage(stage_key, out); + } + 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_file_handle_pread") + return(add([self](Caller, Span args, Span results) -> Result { + u64 handle = (u64)args[0].i64(); + u64 offset = (u64)args[1].i64(); + u64 len = (u64)args[2].i64(); + u32 cap = (u32)args[4].i32(); + int32_t buf = args[3].i32(); + String out; + String stage_key = "file_handle_pread:" + std::to_string(handle) + ":" + std::to_string(offset) + ":" + std::to_string(len); + if(!self->hostcall_staged(stage_key, out)) + { + if(handle >= 1 && handle <= self->file_handles.size()) + { + int fd = self->file_handles[(size_t)handle - 1].fd; + if(fd >= 0 && len > 0) + { + out.resize((size_t)std::min(len, 16ull * 1024ull * 1024ull)); + ssize_t n = pread(fd, &out[0], out.size(), (off_t)offset); + out.resize(n > 0 ? (size_t)n : 0); + } + } + if(buf == 0) self->hostcall_stage(stage_key, out); + } + 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_file_handle_write") + return(add([self](Caller, Span args, Span results) -> Result { + String data; self->hostcall_read(args[1].i32(), args[2].i32(), data); + u64 handle = (u64)args[0].i64(); u64 written = 0; + if(handle >= 1 && handle <= self->file_handles.size()) + { + auto& h = self->file_handles[(size_t)handle - 1]; + if(h.fd >= 0 && h.writable) wasm_fd_write_all(h.fd, data.data(), data.size(), &written); + } + results[0] = Val((int64_t)written); + return(std::monostate()); + })); + if(mod == "env" && name == "uce_host_file_handle_pwrite") + return(add([self](Caller, Span args, Span results) -> Result { + String data; self->hostcall_read(args[2].i32(), args[3].i32(), data); + u64 handle = (u64)args[0].i64(); u64 offset = (u64)args[1].i64(); u64 written = 0; + if(handle >= 1 && handle <= self->file_handles.size()) + { + auto& h = self->file_handles[(size_t)handle - 1]; + if(h.fd >= 0 && h.writable) + { + while(written < data.size()) + { + ssize_t n = pwrite(h.fd, data.data() + written, data.size() - written, (off_t)(offset + written)); + if(n < 0 && errno == EINTR) continue; + if(n <= 0) break; + written += (u64)n; + } + } + } + results[0] = Val((int64_t)written); + return(std::monostate()); + })); + if(mod == "env" && name == "uce_host_file_handle_seek") + return(add([self](Caller, Span args, Span results) -> Result { + u64 handle = (u64)args[0].i64(); s64 pos = -1; + if(handle >= 1 && handle <= self->file_handles.size()) + { + int fd = self->file_handles[(size_t)handle - 1].fd; + if(fd >= 0) pos = (s64)lseek(fd, (off_t)args[1].i64(), args[2].i32()); + } + results[0] = Val((int64_t)pos); + return(std::monostate()); + })); + if(mod == "env" && name == "uce_host_file_handle_tell") + return(add([self](Caller, Span args, Span results) -> Result { + u64 handle = (u64)args[0].i64(); s64 pos = -1; + if(handle >= 1 && handle <= self->file_handles.size()) + { + int fd = self->file_handles[(size_t)handle - 1].fd; + if(fd >= 0) pos = (s64)lseek(fd, 0, SEEK_CUR); + } + results[0] = Val((int64_t)pos); + return(std::monostate()); + })); + if(mod == "env" && name == "uce_host_file_handle_close") + return(add([self](Caller, Span args, Span) -> Result { + u64 handle = (u64)args[0].i64(); + if(handle >= 1 && handle <= self->file_handles.size()) + { + auto& h = self->file_handles[(size_t)handle - 1]; + if(h.fd >= 0) { flock(h.fd, LOCK_UN); close(h.fd); h.fd = -1; } + } + return(std::monostate()); + })); + if(mod == "env" && name == "uce_host_file_stat") + return(add([self](Caller, Span args, Span results) -> Result { + 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_file(path, current, true); DValue r; struct stat st; + r["exists"].set_bool(resolved != "" && lstat(resolved.c_str(), &st) == 0); + if(r["exists"].to_bool()) { r["size"]=(f64)st.st_size; r["mtime"]=(f64)st.st_mtime; r["ctime"]=(f64)st.st_ctime; r["mode"]=(f64)(st.st_mode & 07777); r["is_dir"].set_bool(S_ISDIR(st.st_mode)); r["is_file"].set_bool(S_ISREG(st.st_mode)); r["is_symlink"].set_bool(S_ISLNK(st.st_mode)); } + else { r["size"]=(f64)0; r["mtime"]=(f64)0; r["ctime"]=(f64)0; r["mode"]=(f64)0; r["is_dir"].set_bool(false); r["is_file"].set_bool(false); r["is_symlink"].set_bool(false); } + String out = ucb_encode(r); u32 cap=(u32)args[5].i32(); int32_t buf=args[4].i32(); if(buf && cap>=out.size()) self->hostcall_write(buf,out); results[0]=Val((int32_t)out.size()); return(std::monostate()); + })); + if(mod == "env" && name == "uce_host_dir_list") + return(add([self](Caller, Span args, Span results) -> Result { + 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_file(path, current, true); DValue list; list.set_array(); + if(resolved != "") { std::vector names; if(DIR* d=opendir(resolved.c_str())) { while(struct dirent* e=readdir(d)) { String n=e->d_name; if(n!="."&&n!="..") names.push_back(n); } closedir(d); } std::sort(names.begin(), names.end()); for(auto& n:names) { String p=resolved+"/"+n; struct stat st; DValue item; item["name"]=n; if(lstat(p.c_str(), &st)==0) { item["size"]=(f64)st.st_size; item["mtime"]=(f64)st.st_mtime; item["type"]=S_ISDIR(st.st_mode)?"dir":S_ISLNK(st.st_mode)?"symlink":S_ISREG(st.st_mode)?"file":"other"; } list.push(item); } } + String out=ucb_encode(list); u32 cap=(u32)args[5].i32(); int32_t buf=args[4].i32(); if(buf&&cap>=out.size()) self->hostcall_write(buf,out); results[0]=Val((int32_t)out.size()); return(std::monostate()); + })); + if(mod == "env" && name == "uce_host_file_rename") + return(add([self](Caller, Span args, Span results) -> Result { String from,to,current; self->hostcall_read(args[0].i32(),args[1].i32(),from); self->hostcall_read(args[2].i32(),args[3].i32(),to); self->hostcall_read(args[4].i32(),args[5].i32(),current); String rf=self->resolve_guest_write(from,current), rt=self->resolve_guest_write(to,current); results[0]=Val((int32_t)(rf!=""&&rt!=""&&rename(rf.c_str(),rt.c_str())==0)); return(std::monostate()); })); + if(mod == "env" && name == "uce_host_file_copy") + return(add([self](Caller, Span args, Span results) -> Result { String from,to,current; self->hostcall_read(args[0].i32(),args[1].i32(),from); self->hostcall_read(args[2].i32(),args[3].i32(),to); self->hostcall_read(args[4].i32(),args[5].i32(),current); String rf=self->resolve_guest_file(from,current), rt=self->resolve_guest_write(to,current); bool ok=false; if(rf!=""&&rt!="") { std::ifstream in(rf, std::ios::binary); std::ofstream out(rt, std::ios::binary|std::ios::trunc); out< args, Span results) -> Result { String path,current; self->hostcall_read(args[0].i32(),args[1].i32(),path); self->hostcall_read(args[2].i32(),args[3].i32(),current); String r=self->resolve_guest_write(path,current); results[0]=Val((int32_t)(r!=""&&truncate(r.c_str(),(off_t)args[4].i64())==0)); return(std::monostate()); })); + if(mod == "env" && name == "uce_host_dir_remove") + return(add([self](Caller, Span args, Span results) -> Result { String path,current; self->hostcall_read(args[0].i32(),args[1].i32(),path); self->hostcall_read(args[2].i32(),args[3].i32(),current); String r=self->resolve_guest_write(path,current); bool rec=args[4].i32()!=0; bool ok=false; if(r!="") { if(rec) ok=std::filesystem::remove_all(r)>0; else ok=::rmdir(r.c_str())==0; } results[0]=Val((int32_t)ok); return(std::monostate()); })); + if(mod == "env" && name == "uce_host_file_temp") + return(add([self](Caller, Span args, Span results) -> Result { String prefix,current; self->hostcall_read(args[0].i32(),args[1].i32(),prefix); self->hostcall_read(args[2].i32(),args[3].i32(),current); u32 cap=(u32)args[5].i32(); int32_t buf=args[4].i32(); String out; String stage_key="file_temp:"+prefix+"\0"+current; if(!self->hostcall_staged(stage_key,out)) { if(prefix=="") prefix="/tmp/uce-temp"; String templ=self->resolve_guest_write(prefix+"XXXXXX",current); if(templ!="") { std::vector t(templ.begin(), templ.end()); t.push_back(0); int fd=mkstemp(t.data()); if(fd>=0) { close(fd); out=t.data(); } } if(buf==0) self->hostcall_stage(stage_key,out); } if(buf&&cap>=out.size()) self->hostcall_write(buf,out); results[0]=Val((int32_t)out.size()); return(std::monostate()); })); + if(mod == "env" && name == "uce_host_file_chmod") + return(add([self](Caller, Span args, Span results) -> Result { String path,current; self->hostcall_read(args[0].i32(),args[1].i32(),path); self->hostcall_read(args[2].i32(),args[3].i32(),current); String r=self->resolve_guest_write(path,current); results[0]=Val((int32_t)(r!=""&&chmod(r.c_str(),(mode_t)args[4].i32())==0)); return(std::monostate()); })); + if(mod == "env" && name == "uce_host_file_symlink") + return(add([self](Caller, Span args, Span results) -> Result { String target,linkpath,current; self->hostcall_read(args[0].i32(),args[1].i32(),target); self->hostcall_read(args[2].i32(),args[3].i32(),linkpath); self->hostcall_read(args[4].i32(),args[5].i32(),current); String rt=self->resolve_guest_file(target,current), rl=self->resolve_guest_write(linkpath,current); results[0]=Val((int32_t)(rt!=""&&rl!=""&&symlink(rt.c_str(),rl.c_str())==0)); return(std::monostate()); })); + if(mod == "env" && name == "uce_host_file_readlink") + return(add([self](Caller, Span args, Span results) -> Result { String path,current; self->hostcall_read(args[0].i32(),args[1].i32(),path); self->hostcall_read(args[2].i32(),args[3].i32(),current); String r=self->resolve_guest_write(path,current); String out; if(r!="") { char b[4096]; ssize_t n=readlink(r.c_str(),b,sizeof(b)); if(n>0) out.assign(b,n); } u32 cap=(u32)args[5].i32(); int32_t buf=args[4].i32(); if(buf&&cap>=out.size()) self->hostcall_write(buf,out); results[0]=Val((int32_t)out.size()); return(std::monostate()); })); + if(mod == "env" && name == "uce_host_file_fsync") + return(add([self](Caller, Span args, Span results) -> Result { u64 handle=(u64)args[0].i64(); bool ok=false; if(handle>=1&&handle<=self->file_handles.size()) { int fd=self->file_handles[(size_t)handle-1].fd; ok=fd>=0&&fsync(fd)==0; } results[0]=Val((int32_t)ok); return(std::monostate()); })); if(mod == "env" && name == "uce_host_zip") return(add([self](Caller, Span args, Span results) -> Result { String encoded;