Handle foreign-owned compile artifacts
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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")
|
||||
|
||||
Reference in New Issue
Block a user