Protect native cache paths and snapshot gameplay sources

This commit is contained in:
Emil
2026-09-24 01:55:50 +03:00
parent 4ec5a3cd8a
commit abbb71d0c1
5 changed files with 302 additions and 38 deletions
+72 -3
View File
@@ -59,19 +59,88 @@ void run(const fs::path& root) {
"Lua entry declaration changes the source snapshot");
const auto native = config.build_directory / "Debug";
editor::ensure_native_toolchain_stamp(native, changed_declaration);
editor::ensure_native_toolchain_stamp(config, native, changed_declaration);
atomic_write(native / "sentinel", "keep\n");
editor::ensure_native_toolchain_stamp(native, changed_declaration);
editor::ensure_native_toolchain_stamp(config, native, changed_declaration);
check(fs::exists(native / "sentinel"), "Unchanged toolchain preserves the native tree");
atomic_write(compiler, "compiler-v2\n");
const auto changed_tool = editor::capture_build_inputs(config, lua);
check(changed_tool.source_hash == changed_declaration.source_hash &&
changed_tool.toolchain_hash != changed_declaration.toolchain_hash,
"Changing compiler bytes at the same path changes toolchain identity");
editor::ensure_native_toolchain_stamp(native, changed_tool);
editor::ensure_native_toolchain_stamp(config, native, changed_tool);
check(!fs::exists(native / "sentinel"),
"Changed toolchain invalidates only the generated native tree");
const auto user_directory = root / "important-user-data";
atomic_write(user_directory / "do-not-delete.txt", "valuable\n");
bool refused_unowned{};
try {
editor::ensure_native_toolchain_stamp(config, user_directory, changed_tool);
} catch (const std::exception&) {
refused_unowned = true;
}
check(refused_unowned && fs::is_regular_file(user_directory / "do-not-delete.txt"),
"Unowned native directory is never cleared");
std::error_code native_link_error;
fs::create_directory_symlink(user_directory, config.build_directory / "Release",
native_link_error);
if (!native_link_error) {
bool refused_link{};
try {
editor::ensure_native_toolchain_stamp(config, config.build_directory / "Release",
changed_tool);
} catch (const std::exception&) {
refused_link = true;
}
check(refused_link && fs::is_regular_file(user_directory / "do-not-delete.txt"),
"Symlinked native directory cannot redirect cleanup into user data");
}
const auto relative_tool = project / "tools/cmake-fixture";
atomic_write(relative_tool, "relative-cmake-v1\n");
auto relative_config = config;
relative_config.cmake = "tools/cmake-fixture";
const auto relative_before = editor::capture_build_inputs(relative_config, lua);
atomic_write(relative_tool, "relative-cmake-v2\n");
check(relative_before.toolchain_hash !=
editor::capture_build_inputs(relative_config, lua).toolchain_hash,
"Relative CMake executable resolves from the project working directory");
auto typed_config = config;
const auto typed_compiler = root / "typed-compiler";
atomic_write(typed_compiler, "typed-v1\n");
typed_config.configure_arguments = {
"-DCMAKE_C_COMPILER:FILEPATH=" + path_to_utf8(typed_compiler)};
const auto typed_before = editor::capture_build_inputs(typed_config, lua);
atomic_write(typed_compiler, "typed-v2\n");
check(typed_before.toolchain_hash !=
editor::capture_build_inputs(typed_config, lua).toolchain_hash,
"Typed CMake compiler overrides are included in toolchain identity");
auto malicious_config = config;
malicious_config.configure_arguments.push_back(
"-DFASET_GAMEPLAY_SOURCE_DIR:PATH=" + path_to_utf8(root / "outside"));
bool refused_source_override{};
try {
(void)editor::capture_build_inputs(malicious_config, lua);
} catch (const std::exception&) {
refused_source_override = true;
}
check(refused_source_override,
"Configure arguments cannot redirect gameplay compilation away from hashed Scripts");
const auto staged = editor::stage_gameplay_sources(config, changed_tool, lua,
"snapshot-fixture");
check(staged != scripts && fs::is_regular_file(staged / "Gameplay.cpp") &&
read_text(staged / "Extensions/Extra.hpp") == "#define SPEED 2\n",
"Native gameplay compilation uses a complete staged Scripts snapshot");
atomic_write(scripts / "Extensions/Extra.hpp", "#define SPEED 999\n");
check(read_text(staged / "Extensions/Extra.hpp") == "#define SPEED 2\n",
"A live edit cannot alter the source bytes of an in-flight native build");
atomic_write(scripts / "Extensions/Extra.hpp", "#define SPEED 2\n");
check(editor::stage_gameplay_sources(config, changed_tool, lua, "snapshot-again") ==
staged,
"Identical sources retain a stable native path for incremental Ninja builds");
atomic_write(native / "faset_player", "player-v1\n");
atomic_write(native / "faset_schema_exporter", "exporter-v1\n");
atomic_write(native / "CMakeCache.txt", "recipe-v1\n");
+40
View File
@@ -147,8 +147,48 @@ int test_main(int argc, char** argv) {
authoring::AuthoringService authoring(config.project_root, authoring::builtin_schemas());
const auto valid = manifest();
atomic_write_json(config.project_root / "schema-fixture.json", valid);
{
auto compiler_config = config;
compiler_config.project_root = root / "c-only-compiler-project";
compiler_config.configure_arguments = {
"-DCMAKE_C_COMPILER:FILEPATH=fixture-c-only"};
editor::BuildService compiler_build(compiler_config);
compiler_build.scaffold("Compiler argument fixture", 2);
atomic_write_json(compiler_config.project_root / "schema-fixture.json", valid);
const auto compiler_result = compiler_build.wait(compiler_build.start_build());
check(compiler_result.state == "succeeded",
"C-only compiler override fixture builds: " + compiler_result.error);
const auto compiler_args =
read_json(compiler_config.project_root / "configure-fixture.json");
std::size_t c_overrides{};
bool cxx_default{};
for (const auto& arg : compiler_args) {
const auto text = arg.get<std::string>();
if (text.starts_with("-DCMAKE_C_COMPILER")) {
++c_overrides;
check(text == "-DCMAKE_C_COMPILER:FILEPATH=fixture-c-only",
"Typed C override is not replaced by a default compiler");
}
if (text.starts_with("-DCMAKE_CXX_COMPILER="))
cxx_default = true;
}
check(c_overrides == 1 && cxx_default,
"C and C++ compiler defaults are selected independently");
}
auto first = builds.wait(builds.start_build());
check(first.state == "succeeded", "Valid custom schema v2 publishes: " + first.error);
fs::path compiled_scripts;
for (const auto& arg : read_json(config.project_root / "configure-fixture.json")) {
const auto text = arg.get<std::string>();
constexpr std::string_view prefix = "-DFASET_GAMEPLAY_SOURCE_DIR=";
if (text.starts_with(prefix))
compiled_scripts = path_from_utf8(text.substr(prefix.size()));
}
check(!compiled_scripts.empty() &&
compiled_scripts != config.project_root / "Scripts" &&
read_text(compiled_scripts / "Extensions/BuildOnly.hpp") ==
"#define BUILD_ONLY 1\n",
"Native C++ build receives the immutable source snapshot");
const auto repeated = builds.wait(builds.start_build());
check(repeated.state == "succeeded" &&
repeated.result.at("generation") == first.result.at("generation") &&