From ccbe945d626657d2d75b08015ea8e9393787b03d Mon Sep 17 00:00:00 2001 From: udo Date: Sun, 19 Jul 2026 05:20:08 +0000 Subject: [PATCH] Handle foreign-owned compile artifacts --- docs/setup.md | 8 ++ docs/wasm-runtime-architecture.md | 9 +- scripts/compile_wasm_unit | 4 +- scripts/test_wasm_compile_timeout.sh | 50 +++++++++- src/lib/compiler.cpp | 135 +++++++++++++++++++++++++-- src/lib/compiler.h | 2 +- src/wasm/worker.cpp | 8 +- 7 files changed, 201 insertions(+), 15 deletions(-) diff --git a/docs/setup.md b/docs/setup.md index f769d3a..c0fc12d 100644 --- a/docs/setup.md +++ b/docs/setup.md @@ -290,6 +290,14 @@ workers continue to use `/etc/uce/settings.cfg`. The parallel precompile regression uses private roots so a running proactive compiler cannot consume or publish its controlled race fixtures. +Prefer the managed restart when the service runs as an unprivileged user: it +precompiles as that same user. Request-time publication still accepts readable +artifacts produced by a trusted administrator. When Linux protected-hardlink +policy rejects the normal rollback snapshot, UCE copies the prior artifact +under the unit lock; generation markers are replaced atomically instead of +requiring write access to the existing inode. Existing readable lock files can +be locked without write access, including the shared-PCH lock. + Equivalent manual systemd service for a source checkout (`` = checkout root): ```ini diff --git a/docs/wasm-runtime-architecture.md b/docs/wasm-runtime-architecture.md index 630b395..6fea480 100644 --- a/docs/wasm-runtime-architecture.md +++ b/docs/wasm-runtime-architecture.md @@ -578,7 +578,14 @@ header free-functions are `inline`. The wasm backend exposes only declarations held unit and registry locks, silent nonzero and missing-output compiler results, compiler descendants, staged-output timeout, prior-generation hashes, same-worker recovery, configured-error-page boundedness, residue, - and deadline-independent offline precompile. + foreign-owned offline-precompile artifacts, and deadline-independent offline + precompile. Normal rollback snapshots are hard links. If Linux ownership or + link policy rejects that fast path, the compiler copies the prior artifact + under the same unit lock and keeps the same all-or-nothing publication. + Source-generation markers likewise publish through a same-directory rename, + so a runtime user can replace a readable marker created by an administrator. + Existing foreign-owned unit, registry, generation, and PCH lock files are + opened read-only for `flock`; new locks are still created read-write. `scripts/test_cold_component_deadline.sh` separately compiles a deliberately cold component that exceeds the development epoch window and proves the parent request still renders it. The focused shell gates create temporary diff --git a/scripts/compile_wasm_unit b/scripts/compile_wasm_unit index 867449b..47f7376 100755 --- a/scripts/compile_wasm_unit +++ b/scripts/compile_wasm_unit @@ -56,7 +56,9 @@ build_pch_if_needed() { return 0 fi mkdir -p "$PCH_DIR" - exec 9>"$PCH_FN.lock" + if ! { exec 9>"$PCH_FN.lock"; } 2>/dev/null; then + exec 9<"$PCH_FN.lock" + fi flock 9 if [ -s "$PCH_FN" ] && [ -z "$(find src/lib -maxdepth 1 -name '*.h' -type f -newer "$PCH_FN" -print -quit)" ]; then flock -u 9 diff --git a/scripts/test_wasm_compile_timeout.sh b/scripts/test_wasm_compile_timeout.sh index 8ffca21..42fa066 100755 --- a/scripts/test_wasm_compile_timeout.sh +++ b/scripts/test_wasm_compile_timeout.sh @@ -55,6 +55,12 @@ COMPILE_FAILURE_RETRY_SECONDS=60 CFG mount --bind "$settings" /etc/uce/settings.cfg +mkdir -p "$root/compiler/src/wasm" "$root/compiler/scripts" +cp -a src/lib "$root/compiler/src/lib" +cp -a src/wasm/abi.h "$root/compiler/src/wasm/abi.h" +cp -a scripts/compile_wasm_unit scripts/build_unit_source_map.py scripts/check_unit_wasm.py "$root/compiler/scripts/" +chmod -R a+rX "$root/compiler" + cat >"$root/compile" <'$root/compiler-pid' (sleep 3; touch '$root/late-descendant') & @@ -74,7 +87,7 @@ fi if [[ -r '$root/silent-fail' ]] && grep -Fxq "\$source_file" '$root/silent-fail'; then exit 23; fi if [[ -r '$root/empty-success' ]] && grep -Fxq "\$source_file" '$root/empty-success'; then exit 0; fi if [[ -r '$root/delay' ]] && grep -Fxq "\$source_file" '$root/delay'; then sleep 1; fi -exec '$(pwd)/scripts/compile_wasm_unit' "\$@" +exec '$root/compiler/scripts/compile_wasm_unit' "\$@" SHIM chmod +x "$root/compile" @@ -97,9 +110,19 @@ printf '%s\n' 'COMPONENT(Request& context) { print("component"); }' >"$site/comp timeout 40s env UCE_PRECOMPILE_FILES_IN="$site" UCE_PRECOMPILE_BIN_DIRECTORY="$work" UCE_PRECOMPILE_JOBS=1 bin/uce_fastcgi.linux.bin --precompile >"$root/precompile.log" 2>&1 || { cat "$root/precompile.log" >&2; exit 1; } cache="$(scripts/unit_cache_directory "$work")$(realpath "$site")" generation=$(scripts/unit_cache_directory "$work") +service_user=${UCE_TEST_SERVICE_USER:-www-data} +service_uid=$(id -u "$service_user") +service_gid=$(id -g "$service_user") +generation_before=$(<"$generation/source-generation.txt") +[[ $(sysctl -n fs.protected_hardlinks) == 1 ]] +[[ $(stat -c %u "$cache/slow.uce.wasm") == 0 && $(stat -c %u "$generation/source-generation.txt") == 0 ]] +[[ $(stat -c %u "$cache/slow.uce.wasm.lock") == 0 && $(stat -c %u "$generation/source-generation.txt.lock") == 0 ]] +find "$work" -type d -exec chown "$service_uid:$service_gid" {} + +chown -R "$service_uid:$service_gid" "$root/run" "$root/session" "$root/upload" +chown "$service_uid:$service_gid" "$root" rm -f "$cache/cold-entry.uce."* "$cache/components/slow-component.uce."* -timeout --signal=TERM --kill-after=5s 90s bin/uce_fastcgi.linux.bin >"$log" 2>&1 & +timeout --signal=TERM --kill-after=5s 90s setpriv --reuid="$service_uid" --regid="$service_gid" --clear-groups bin/uce_fastcgi.linux.bin >"$log" 2>&1 & server_pid=$! for _ in $(seq 1 400); do [[ -S "$root/run/cli.sock" ]] && break; sleep 0.02; done [[ -S "$root/run/cli.sock" ]] || { cat "$log" >&2; exit 1; } @@ -127,8 +150,29 @@ done worker_pid=${driver%%|*} [[ "$driver" == "$worker_pid|health" ]] [[ "$(request /driver.uce)" == compiled ]] +[[ $(stat -c %u "$cache/slow.uce.wasm") == "$service_uid" ]] +[[ $(stat -c %u "$generation/source-generation.txt") == "$service_uid" ]] +[[ $(<"$generation/source-generation.txt") != "$generation_before" ]] +! grep -q 'Could not write.*source-generation.txt' "$log" before=$(sha256sum "$cache/slow.uce.wasm" "$cache/slow.uce.wasm.source-map" "$cache/slow.uce.cpp" "$cache/slow.uce.exports.txt" "$cache/slow.uce.meta.txt") +truncate -s 1073741824 "$cache/slow.uce.cwasm" +printf '%s\n' "$site/slow.uce" >"$root/copy-delay" +rm -f "$root/copy-delay-complete" +started=$(date +%s%N) +set +e +copy_timeout=$(request /driver.uce 2>&1) +set -e +elapsed=$(( ($(date +%s%N) - started) / 1000000 )) +rm -f "$root/copy-delay" +[[ "$copy_timeout" == *UCE_INVOCATION_TIMEOUT:* ]] || { echo "foreign rollback copy lacked timeout: $copy_timeout" >&2; exit 1; } +(( elapsed >= 1700 && elapsed <= 3500 )) || { echo "foreign rollback copy took ${elapsed}ms" >&2; exit 1; } +[[ -e "$root/copy-delay-complete" ]] || { echo "foreign rollback compiler did not finish before timeout" >&2; exit 1; } +[[ $(stat -c '%u:%s' "$cache/slow.uce.cwasm") == '0:1073741824' ]] +[[ "$before" == "$(sha256sum "$cache/slow.uce.wasm" "$cache/slow.uce.wasm.source-map" "$cache/slow.uce.cpp" "$cache/slow.uce.exports.txt" "$cache/slow.uce.meta.txt")" ]] +! find "$cache" -name '*.invocation-*' -print -quit | grep -q . +rm -f "$cache/slow.uce.cwasm" + assert_timeout() { local url="$1" source="$2" started elapsed output printf '%s\n' "$source" >"$root/hold" @@ -203,4 +247,4 @@ elapsed=$(( ($(date +%s%N) - started) / 1000000 )) flock -u 9 [[ "$locked" == *UCE_INVOCATION_TIMEOUT:* && "$elapsed" -le 3500 ]] -echo "wasm compile timeout passed (entry, explicit, dynamic, unit/registry locks, error page, process group, staging, native precompile)" +echo "wasm compile timeout passed (entry, explicit, dynamic, unit/registry locks, error page, process group, staging, native precompile, foreign-owned artifacts)" diff --git a/src/lib/compiler.cpp b/src/lib/compiler.cpp index e5be256..39b83ac 100644 --- a/src/lib/compiler.cpp +++ b/src/lib/compiler.cpp @@ -386,6 +386,97 @@ static bool compiler_publish_staged_artifacts(SharedUnit* su, String staged_pre_ file_unlink(it->previous); } }; + auto copy_previous = [&](const Artifact& artifact, int link_error) { + struct stat path_info; + if(lstat(artifact.canonical.c_str(), &path_info) != 0 || !S_ISREG(path_info.st_mode)) + { + error = "refusing non-regular bounded compile artifact " + artifact.canonical; + return(false); + } + int input = open(artifact.canonical.c_str(), O_RDONLY | O_CLOEXEC | O_NOFOLLOW); + if(input < 0) + { + error = "could not read previous bounded compile artifact " + artifact.canonical + + " after hard-link failure " + String(std::strerror(link_error)) + ": " + std::strerror(errno); + return(false); + } + struct stat source_info; + if(fstat(input, &source_info) != 0) + { + error = "could not inspect previous bounded compile artifact " + artifact.canonical + ": " + std::strerror(errno); + close(input); + return(false); + } + if(source_info.st_dev != path_info.st_dev || source_info.st_ino != path_info.st_ino || !S_ISREG(source_info.st_mode)) + { + error = "bounded compile artifact changed while preserving " + artifact.canonical; + close(input); + return(false); + } + int output = open(artifact.previous.c_str(), O_WRONLY | O_CREAT | O_EXCL | O_CLOEXEC, source_info.st_mode & 07777); + if(output < 0) + { + error = "could not create bounded compile rollback copy " + artifact.previous + ": " + std::strerror(errno); + close(input); + return(false); + } + bool copied = true; + char buffer[65536]; + while(copied) + { + if(deadline && deadline->expire_if_needed()) + { + error = "bounded compile deadline expired while copying previous artifact " + artifact.canonical; + copied = false; + break; + } + ssize_t got = read(input, buffer, sizeof(buffer)); + if(got == 0) + break; + if(got < 0) + { + if(errno == EINTR) + continue; + error = "could not read previous bounded compile artifact " + artifact.canonical + ": " + std::strerror(errno); + copied = false; + break; + } + ssize_t written = 0; + while(written < got) + { + if(deadline && deadline->expire_if_needed()) + { + error = "bounded compile deadline expired while copying previous artifact " + artifact.canonical; + copied = false; + break; + } + ssize_t amount = write(output, buffer + written, got - written); + if(amount < 0 && errno == EINTR) + continue; + if(amount <= 0) + { + error = "could not copy previous bounded compile artifact " + artifact.canonical + ": " + std::strerror(errno); + copied = false; + break; + } + written += amount; + } + } + if(copied && fchmod(output, source_info.st_mode & 07777) != 0) + { + error = "could not preserve mode for bounded compile artifact " + artifact.canonical + ": " + std::strerror(errno); + copied = false; + } + if(close(output) != 0 && copied) + { + error = "could not close bounded compile rollback copy " + artifact.previous + ": " + std::strerror(errno); + copied = false; + } + close(input); + if(!copied) + file_unlink(artifact.previous); + return(copied); + }; for(auto& artifact : artifacts) { if(deadline && deadline->expire_if_needed()) @@ -399,10 +490,20 @@ static bool compiler_publish_staged_artifacts(SharedUnit* su, String staged_pre_ artifact.existed = file_exists(artifact.canonical); if(artifact.existed && link(artifact.canonical.c_str(), artifact.previous.c_str()) != 0) { - error = "could not preserve previous bounded compile artifacts: " + String(std::strerror(errno)); - for(auto& cleanup : artifacts) - file_unlink(cleanup.previous); - return(false); + int link_error = errno; + if(link_error != EPERM && link_error != EACCES && link_error != EMLINK && link_error != EXDEV) + { + error = "could not preserve previous bounded compile artifact " + artifact.canonical + ": " + std::strerror(link_error); + for(auto& cleanup : artifacts) + file_unlink(cleanup.previous); + return(false); + } + if(!copy_previous(artifact, link_error)) + { + for(auto& cleanup : artifacts) + file_unlink(cleanup.previous); + return(false); + } } } for(auto& artifact : artifacts) @@ -417,7 +518,7 @@ static bool compiler_publish_staged_artifacts(SharedUnit* su, String staged_pre_ file_unlink(artifact.canonical); else if(rename(artifact.staged.c_str(), artifact.canonical.c_str()) != 0) { - error = "could not publish bounded compile artifacts: " + String(std::strerror(errno)); + error = "could not publish bounded compile artifact " + artifact.canonical + ": " + std::strerror(errno); rollback(); return(false); } @@ -547,6 +648,8 @@ int compiler_open_lock_file(String file_name, String purpose, bool nonblocking = if(lock_dir != "") mkdir(lock_dir); int fdlock = open(file_name.c_str(), O_RDWR | O_CREAT, 0666); + if(fdlock == -1 && (errno == EACCES || errno == EPERM)) + fdlock = open(file_name.c_str(), O_RDONLY | O_CLOEXEC); if(fdlock == -1) { printf("(!) Could not open lock file %s\n", file_name.c_str()); @@ -575,6 +678,8 @@ int compiler_open_lock_file_bounded(String file_name, String purpose, CompilerDe if(lock_dir != "") mkdir(lock_dir); int fdlock = open(file_name.c_str(), O_RDWR | O_CREAT, 0666); + if(fdlock == -1 && (errno == EACCES || errno == EPERM)) + fdlock = open(file_name.c_str(), O_RDONLY | O_CLOEXEC); if(fdlock == -1) return(-1); fcntl(fdlock, F_SETFD, FD_CLOEXEC); @@ -604,6 +709,16 @@ void compiler_close_lock_file(int fdlock) close(fdlock); } +static void compiler_publish_source_generation(String file_name) +{ + String staged_file_name = file_name + ".stage-" + std::to_string((u64)getpid()); + file_unlink(staged_file_name); + if(file_put_contents(staged_file_name, std::to_string(getpid()) + ":" + std::to_string((u64)(time_precise() * 1000000.0)) + "\n") && + rename(staged_file_name.c_str(), file_name.c_str()) != 0) + printf("(!) Could not publish %s: %s\n", file_name.c_str(), std::strerror(errno)); + file_unlink(staged_file_name); +} + static void compiler_mark_source_generation_nonblocking(Request* context) { if(!context || !context->server) @@ -612,7 +727,7 @@ static void compiler_mark_source_generation_nonblocking(Request* context) int fdlock = compiler_open_lock_file(file_name + ".lock", "source-generation", true); if(fdlock < 0) return; - file_put_contents(file_name, std::to_string(getpid()) + ":" + std::to_string((u64)(time_precise() * 1000000.0)) + "\n"); + compiler_publish_source_generation(file_name); compiler_close_lock_file(fdlock); } @@ -1520,7 +1635,7 @@ void compiler_mark_source_generation(Request* context) int fdlock = compiler_open_lock_file(file_name + ".lock", "source-generation"); if(fdlock < 0) return; - file_put_contents(file_name, std::to_string(getpid()) + ":" + std::to_string((u64)(time_precise() * 1000000.0)) + "\n"); + compiler_publish_source_generation(file_name); compiler_close_lock_file(fdlock); } @@ -1983,10 +2098,12 @@ bool unit_compile(String path) return(su && trim(su->compiler_messages) == "" && file_exists(su->wasm_name)); } -bool unit_compile_bounded(Request* request, String path, u64 timeout_ms, bool* timed_out) +bool unit_compile_bounded(Request* request, String path, u64 timeout_ms, bool* timed_out, String* error) { if(timed_out) *timed_out = false; + if(error) + error->clear(); if(!request || timeout_ms == 0) { if(timed_out) @@ -2006,5 +2123,7 @@ bool unit_compile_bounded(Request* request, String path, u64 timeout_ms, bool* t auto su = compiler_get_shared_unit_internal(request, resolved_path, true, false, &deadline); if(timed_out) *timed_out = deadline.timed_out; + if(error) + *error = first(deadline.operational_error, su ? trim(su->compiler_messages) : ""); return(su && trim(su->compiler_messages) == "" && file_exists(su->wasm_name)); } diff --git a/src/lib/compiler.h b/src/lib/compiler.h index 053d17f..d4e8ca8 100644 --- a/src/lib/compiler.h +++ b/src/lib/compiler.h @@ -28,7 +28,7 @@ SharedUnit* get_shared_unit(Request* context, String file_name); #ifndef __UCE_WASM_UNIT__ SharedUnit* get_shared_unit_for_preprocess(Request* context, String file_name); SharedUnit* get_shared_unit_bounded(Request* context, String file_name, u64 timeout_ms, bool* timed_out); -bool unit_compile_bounded(Request* context, String path, u64 timeout_ms, bool* timed_out); +bool unit_compile_bounded(Request* context, String path, u64 timeout_ms, bool* timed_out, String* error = 0); #endif String compiler_error_page_unit(Request* context, String config_key); bool compiler_unit_compile_pending(Request* context, String file_name); diff --git a/src/wasm/worker.cpp b/src/wasm/worker.cpp index f4241fc..f93b6ea 100644 --- a/src/wasm/worker.cpp +++ b/src/wasm/worker.cpp @@ -3801,9 +3801,15 @@ private: { u64 remaining_ms = self->invocation_remaining_ms(); bool timed_out = false; - bool ok = remaining_ms > 0 && unit_compile_bounded(context, request["path"].to_string(), remaining_ms, &timed_out); + String compile_error; + bool ok = remaining_ms > 0 && unit_compile_bounded(context, request["path"].to_string(), remaining_ms, &timed_out, &compile_error); if(timed_out || remaining_ms == 0) return(Trap(self->invocation_timeout_error())); + if(!ok && compile_error != "") + { + response["error"] = compile_error; + printf("(!) unit_compile failed for %s: %s\n", request["path"].to_string().c_str(), compile_error.c_str()); + } response["ok"].set_bool(ok); } else if(op == "call")