Handle foreign-owned compile artifacts

This commit is contained in:
udo
2026-07-19 05:20:08 +00:00
parent 4f010d37a0
commit ccbe945d62
7 changed files with 201 additions and 15 deletions
+8
View File
@@ -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 (`<UCE_REPO>` = checkout root):
```ini
+8 -1
View File
@@ -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
+3 -1
View File
@@ -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
+47 -3
View File
@@ -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" <<SHIM
#!/usr/bin/env bash
set -euo pipefail
@@ -66,6 +72,13 @@ if [[ -r '$root/stage-timeout' ]] && grep -Fxq "\$source_file" '$root/stage-time
cp "\$2/\$(basename "\$source_file").wasm.source-map" "\$2/\$5.source-map"
sleep 60
fi
if [[ -r '$root/copy-delay' ]] && grep -Fxq "\$source_file" '$root/copy-delay'; then
cp "\$2/\$(basename "\$source_file").wasm" "\$2/\$5"
cp "\$2/\$(basename "\$source_file").wasm.source-map" "\$2/\$5.source-map"
sleep 1.5
touch '$root/copy-delay-complete'
exit 0
fi
if [[ -r '$root/hold' ]] && grep -Fxq "\$source_file" '$root/hold'; then
printf '%s\n' "\$\$" >'$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)"
+127 -8
View File
@@ -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));
}
+1 -1
View File
@@ -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);
+7 -1
View File
@@ -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")