diff --git a/docs/wasm-runtime-architecture.md b/docs/wasm-runtime-architecture.md index ac4005e..0834bfb 100644 --- a/docs/wasm-runtime-architecture.md +++ b/docs/wasm-runtime-architecture.md @@ -146,6 +146,12 @@ compiler error rather than serving the old unit indefinitely. The per-unit lock also keeps concurrent synchronous compilers from waiting across a transitive graph when a last complete artifact is available. +Before preprocessing, the compiler verifies that the worker can actually read +the source. An unreadable path is a compile failure with a persisted diagnostic; +it never becomes an apparently valid empty side module. Source signatures mark +unreadable inputs, so correcting access invalidates that failure and permits a +normal retry without changing signatures for ordinary readable files. + --- ## 4. The workspace runtime @@ -351,7 +357,9 @@ header free-functions are `inline`. The wasm backend exposes only declarations `scripts/test_dependency_invalidation.sh`. The latter changes a transitive `#load`, then replaces a warmed worker artifact while preserving its whole-second mtime to prove both compiler and worker caches invalidate it. It - also sends 48 requests and asserts the observed worker PID set does not exceed + also rejects an unreadable unit without publishing a wasm artifact, restores + its permissions and proves the next CLI request compiles it, then sends 48 + requests and asserts the observed worker PID set does not exceed `WORKER_COUNT`, guarding against accidental reintroduction of request-count recycling. `scripts/test_cold_component_deadline.sh` separately compiles a deliberately diff --git a/scripts/test_dependency_invalidation.sh b/scripts/test_dependency_invalidation.sh index f49fec0..e595a3a 100755 --- a/scripts/test_dependency_invalidation.sh +++ b/scripts/test_dependency_invalidation.sh @@ -54,6 +54,23 @@ if [[ "$(http_marker)" != *"dependency-marker-a"* ]]; then exit 1 fi +printf '%s\n' 'CLI(Request& context) { print("readable-source-marker"); }' >"$source_dir/unreadable.uce" +chmod 000 "$source_dir/unreadable.uce" +if unreadable_output=$(scripts/uce-cli "/$test_name/unreadable.uce" 2>&1); then + echo "unreadable source unexpectedly compiled: $unreadable_output" >&2 + exit 1 +fi +if [[ "$unreadable_output" != *"source file is not readable"* ]]; then + echo "unreadable source did not report its actual compile error: $unreadable_output" >&2 + exit 1 +fi +if [[ -e "$cache_dir/unreadable.uce.wasm" ]]; then + echo "unreadable source published a wasm artifact" >&2 + exit 1 +fi +chmod 644 "$source_dir/unreadable.uce" +assert_marker unreadable readable-source-marker + sed -i 's/dependency-marker-a/dependency-marker-b/' "$source_dir/child.uce" started_at=$(date +%s%N) http_during_rebuild=$(http_marker) diff --git a/src/lib/compiler.cpp b/src/lib/compiler.cpp index c7103df..9c821f8 100644 --- a/src/lib/compiler.cpp +++ b/src/lib/compiler.cpp @@ -2,8 +2,10 @@ #include "compiler-parser.h" #include "hash.h" #include +#include #include #include +#include #include #include #include @@ -50,6 +52,7 @@ struct UnitSourceSignatureEntry u64 modified_ns = 0; u64 changed_ns = 0; u64 size = 0; + bool readable = false; String content_hash; StringList loaded_paths; }; @@ -128,6 +131,18 @@ StringList compiler_unit_load_paths(String file_name, String content) return(paths); } +bool compiler_source_readable(String file_name, const struct stat& info, int* read_error = 0) +{ + int error = 0; + if((info.st_mode & (S_IRUSR | S_IRGRP | S_IROTH)) == 0) + error = EACCES; + else if(access(file_name.c_str(), R_OK) != 0) + error = errno; + if(read_error) + *read_error = error; + return(error == 0); +} + UnitSourceSignatureEntry compiler_unit_source_entry(String file_name) { struct stat info; @@ -137,10 +152,11 @@ UnitSourceSignatureEntry compiler_unit_source_entry(String file_name) entry.modified_ns = (u64)info.st_mtim.tv_sec * 1000000000ull + (u64)info.st_mtim.tv_nsec; entry.changed_ns = (u64)info.st_ctim.tv_sec * 1000000000ull + (u64)info.st_ctim.tv_nsec; entry.size = (u64)info.st_size; + entry.readable = compiler_source_readable(file_name, info); { std::lock_guard lock(unit_source_signature_cache_mutex); auto cached = unit_source_signature_cache.find(file_name); - if(cached != unit_source_signature_cache.end() && cached->second.modified_ns == entry.modified_ns && cached->second.changed_ns == entry.changed_ns && cached->second.size == entry.size) + if(cached != unit_source_signature_cache.end() && cached->second.modified_ns == entry.modified_ns && cached->second.changed_ns == entry.changed_ns && cached->second.size == entry.size && cached->second.readable == entry.readable) return(cached->second); } String content = file_get_contents(file_name); @@ -165,7 +181,7 @@ void compiler_append_unit_source_signature(String file_name, std::set& v visited.insert(normalized); UnitSourceSignatureEntry entry = compiler_unit_source_entry(normalized); - signature += normalized + ":" + entry.content_hash + "\n"; + signature += normalized + ":" + entry.content_hash + (entry.readable ? String("") : String(":unreadable")) + "\n"; for(String loaded : entry.loaded_paths) compiler_append_unit_source_signature(loaded, visited, signature); } @@ -793,6 +809,22 @@ void compile_shared_unit(Request* context, SharedUnit* su) compiler_record_compile_result(su, time_precise() - comp_start, false, "missing_source", su->compiler_messages); return; } + struct stat source_info; + int read_error = 0; + bool source_readable = stat(su->file_name.c_str(), &source_info) == 0 && compiler_source_readable(su->file_name, source_info, &read_error); + if(!source_readable && read_error == 0) + read_error = errno == 0 ? ENOENT : errno; + if(!source_readable) + { + su->compiler_messages = "source file is not readable (" + su->file_name + "): " + std::strerror(read_error); + file_put_contents(su->compile_output_file_name, su->compiler_messages + "\n"); + file_put_contents(su->wasm_check_file_name, su->compiler_messages + "\n"); + file_put_contents(su->meta_file_name, compiler_unit_metadata_text(context, su)); + file_unlink(su->wasm_name); + compiler_record_compile_result(su, time_precise() - comp_start, false, "compile_error", su->compiler_messages); + printf("%s \n", compiler_format_compile_failure(su, su->compiler_messages).c_str()); + return; + } shell_exec("mkdir -p " + shell_escape(su->pre_path)); file_put_contents(su->pre_path + "/" + su->pre_file_name, preprocess_shared_unit(context, su)); diff --git a/src/linux_fastcgi.cpp b/src/linux_fastcgi.cpp index 53242d0..cf09390 100644 --- a/src/linux_fastcgi.cpp +++ b/src/linux_fastcgi.cpp @@ -438,8 +438,9 @@ int handle_cli_complete(FastCGIRequest& request) // W7e: compile a cold/stale unit on demand; native execution has // been removed, so a unit that still cannot be served by wasm is a // request failure instead of a fallback path. + SharedUnit* cli_compile_state = 0; if(!wasm_backend_should_handle(request, cli_unit)) - get_shared_unit(&request, cli_unit); + cli_compile_state = get_shared_unit(&request, cli_unit); if(wasm_backend_should_handle(request, cli_unit)) { String wasm_error = wasm_backend_serve(request, cli_unit, "cli"); @@ -452,7 +453,11 @@ int handle_cli_complete(FastCGIRequest& request) else { request.set_status(500, "Internal Server Error"); - print("UCE CLI wasm unit unavailable after compile: ", script_filename, "\n"); + String compile_error = cli_compile_state ? first(cli_compile_state->compiler_messages, cli_compile_state->compile_error_status) : String(""); + print("UCE CLI wasm unit unavailable after compile: ", script_filename); + if(compile_error != "") + print(": ", compile_error); + print("\n"); } } } @@ -575,14 +580,16 @@ int handle_complete(FastCGIRequest& request) { // unit on demand — get_shared_unit() builds the .wasm side-module — and // recheck. Native execution has been removed, so a unit that still cannot // be served by wasm becomes a clean 500 request failure. + SharedUnit* entry_compile_state = 0; auto wasm_ready = [&](const String& unit) -> bool { if(!wasm_backend_should_handle(request, unit)) - get_shared_unit(&request, unit); + entry_compile_state = get_shared_unit(&request, unit); return(wasm_backend_should_handle(request, unit)); }; auto fail_wasm_unavailable = [&](const String& handler) { failure_title = "wasm unit unavailable after compile"; - failure_details = handler + " handler could not be served by wasm"; + String compile_error = entry_compile_state ? first(entry_compile_state->compiler_messages, entry_compile_state->compile_error_status) : String(""); + failure_details = compile_error != "" ? compile_error : handler + " handler could not be served by wasm"; failure_trace = "source: " + request.params["SCRIPT_FILENAME"]; };