Reject unreadable UCE source units

This commit is contained in:
udo 2026-07-13 15:28:35 +00:00
parent cd2fa163e6
commit a288b539c6
4 changed files with 71 additions and 7 deletions

View File

@ -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 also keeps concurrent synchronous compilers from waiting across a transitive
graph when a last complete artifact is available. 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 ## 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 `scripts/test_dependency_invalidation.sh`. The latter changes a transitive
`#load`, then replaces a warmed worker artifact while preserving its `#load`, then replaces a warmed worker artifact while preserving its
whole-second mtime to prove both compiler and worker caches invalidate it. It 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 `WORKER_COUNT`, guarding against accidental reintroduction of request-count
recycling. recycling.
`scripts/test_cold_component_deadline.sh` separately compiles a deliberately `scripts/test_cold_component_deadline.sh` separately compiles a deliberately

View File

@ -54,6 +54,23 @@ if [[ "$(http_marker)" != *"dependency-marker-a"* ]]; then
exit 1 exit 1
fi 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" sed -i 's/dependency-marker-a/dependency-marker-b/' "$source_dir/child.uce"
started_at=$(date +%s%N) started_at=$(date +%s%N)
http_during_rebuild=$(http_marker) http_during_rebuild=$(http_marker)

View File

@ -2,8 +2,10 @@
#include "compiler-parser.h" #include "compiler-parser.h"
#include "hash.h" #include "hash.h"
#include <algorithm> #include <algorithm>
#include <cerrno>
#include <cstdlib> #include <cstdlib>
#include <cctype> #include <cctype>
#include <cstring>
#include <filesystem> #include <filesystem>
#include <fcntl.h> #include <fcntl.h>
#include <mutex> #include <mutex>
@ -50,6 +52,7 @@ struct UnitSourceSignatureEntry
u64 modified_ns = 0; u64 modified_ns = 0;
u64 changed_ns = 0; u64 changed_ns = 0;
u64 size = 0; u64 size = 0;
bool readable = false;
String content_hash; String content_hash;
StringList loaded_paths; StringList loaded_paths;
}; };
@ -128,6 +131,18 @@ StringList compiler_unit_load_paths(String file_name, String content)
return(paths); 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) UnitSourceSignatureEntry compiler_unit_source_entry(String file_name)
{ {
struct stat info; 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.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.changed_ns = (u64)info.st_ctim.tv_sec * 1000000000ull + (u64)info.st_ctim.tv_nsec;
entry.size = (u64)info.st_size; entry.size = (u64)info.st_size;
entry.readable = compiler_source_readable(file_name, info);
{ {
std::lock_guard<std::mutex> lock(unit_source_signature_cache_mutex); std::lock_guard<std::mutex> lock(unit_source_signature_cache_mutex);
auto cached = unit_source_signature_cache.find(file_name); 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); return(cached->second);
} }
String content = file_get_contents(file_name); String content = file_get_contents(file_name);
@ -165,7 +181,7 @@ void compiler_append_unit_source_signature(String file_name, std::set<String>& v
visited.insert(normalized); visited.insert(normalized);
UnitSourceSignatureEntry entry = compiler_unit_source_entry(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) for(String loaded : entry.loaded_paths)
compiler_append_unit_source_signature(loaded, visited, signature); 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); compiler_record_compile_result(su, time_precise() - comp_start, false, "missing_source", su->compiler_messages);
return; 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)); shell_exec("mkdir -p " + shell_escape(su->pre_path));
file_put_contents(su->pre_path + "/" + su->pre_file_name, preprocess_shared_unit(context, su)); file_put_contents(su->pre_path + "/" + su->pre_file_name, preprocess_shared_unit(context, su));

View File

@ -438,8 +438,9 @@ int handle_cli_complete(FastCGIRequest& request)
// W7e: compile a cold/stale unit on demand; native execution has // 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 // been removed, so a unit that still cannot be served by wasm is a
// request failure instead of a fallback path. // request failure instead of a fallback path.
SharedUnit* cli_compile_state = 0;
if(!wasm_backend_should_handle(request, cli_unit)) 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)) if(wasm_backend_should_handle(request, cli_unit))
{ {
String wasm_error = wasm_backend_serve(request, cli_unit, "cli"); String wasm_error = wasm_backend_serve(request, cli_unit, "cli");
@ -452,7 +453,11 @@ int handle_cli_complete(FastCGIRequest& request)
else else
{ {
request.set_status(500, "Internal Server Error"); 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 // unit on demand — get_shared_unit() builds the .wasm side-module — and
// recheck. Native execution has been removed, so a unit that still cannot // recheck. Native execution has been removed, so a unit that still cannot
// be served by wasm becomes a clean 500 request failure. // be served by wasm becomes a clean 500 request failure.
SharedUnit* entry_compile_state = 0;
auto wasm_ready = [&](const String& unit) -> bool { auto wasm_ready = [&](const String& unit) -> bool {
if(!wasm_backend_should_handle(request, unit)) 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)); return(wasm_backend_should_handle(request, unit));
}; };
auto fail_wasm_unavailable = [&](const String& handler) { auto fail_wasm_unavailable = [&](const String& handler) {
failure_title = "wasm unit unavailable after compile"; 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"]; failure_trace = "source: " + request.params["SCRIPT_FILENAME"];
}; };