diff --git a/scripts/test/harnesses/wasmtestreport.py b/scripts/test/harnesses/wasmtestreport.py index e8c486238..861fd8b46 100644 --- a/scripts/test/harnesses/wasmtestreport.py +++ b/scripts/test/harnesses/wasmtestreport.py @@ -402,14 +402,14 @@ def add_test_result(result, file_path, status, error_type, output, timing_info=N if status.lower() == "success": result["number_of_success"] += 1 result["success"].append(file_path) - logger.debug("SUCCESS") + logger.info("SUCCESS") else: result["number_of_failures"] += 1 result["failures"].append(file_path) error_message = error_types.get(error_type, "Undefined Failure") - logger.debug(f"FAILURE: {error_message}") + logger.error(f"FAILURE: {error_message}") if error_type in error_types: result[f"number_of_{error_type}"] += 1 result[error_type].append(file_path) @@ -1036,7 +1036,19 @@ def pre_test(tests_to_run=None, allow_precompiled=False): except OSError: # Fallback to copying in case symlink creation fails shutil.copy2(readlinkfile_path, symlink_path) - + ''' + ISO-004: create host-only and cage-only sentinel files with distinguishable content + so symlink_confinement.c can prove confinement by checking which file's content it + read, not just whether and open() succeeded + ''' + host_sentinel_dir = Path("/tmp/lind") + host_sentinel_dir.mkdir(parents=True, exist_ok=True) + (host_sentinel_dir / "sentinel.txt").write_text("LIND_HOST_ONLY") + + cage_sentinel_dir = LINDFS_ROOT / "tmp" / "lind" + cage_sentinel_dir.mkdir(parents=True, exist_ok=True) + (cage_sentinel_dir / "sentinel.txt").write_text("LIND_CAGE_ONLY") + # Create required executables if tests_to_run: executable_deps = analyze_executable_dependencies(tests_to_run) @@ -1298,7 +1310,7 @@ def is_file_in_folder(file_path, folder_list): # ---------------------------------------------------------------------- def should_run_file(file_path, run_folders, skip_folders, skip_test_cases): if file_path in skip_test_cases: - logger.debug(f"Skipping {file_path}") + logger.info(f"Skipping {file_path}") return False if skip_folders and is_file_in_folder(file_path, skip_folders): @@ -1580,97 +1592,24 @@ def get_test_mode(source_file): # results - results dictionary, timeout_sec - timeout for tests # - Output: None (modifies results dictionary) # ---------------------------------------------------------------------- -def _get_new_test_case(result, before_test_cases): - """Return the newly recorded test case after running one test, if any.""" - after_test_cases = set(result["test_cases"].keys()) - new_cases = list(after_test_cases - before_test_cases) - if not new_cases: - return None, None - test_name = new_cases[0] - return test_name, result["test_cases"][test_name] - - -def _print_failure_details(failed_tests): - """Print deferred failure details after the compact progress line.""" - if not failed_tests: - return - - print("\nFailures:", flush=True) - for test_name, test_case in failed_tests: - print(f"\n{test_name}", flush=True) - error_type = test_case.get("error_type") - if error_type: - print(f"Error type: {error_type}", flush=True) - - output = test_case.get("output", "") - if output: - print(output.rstrip(), flush=True) - - def run_tests(config, artifacts_root, results, timeout_sec): - """Execute all tests with compact progress and deferred failure details.""" + """Execute all tests""" total_count = len(config['tests_to_run']) - failed_tests = [] - skipped_count = 0 - - print(f"Running {total_count} tests") - for original_source in config['tests_to_run']: + for i, original_source in enumerate(config['tests_to_run']): + logger.info(f"[{i+1}/{total_count}] {original_source}") + dest_source = setup_test_file_in_artifacts(original_source, artifacts_root) # Determine test type and run appropriate test test_mode = get_test_mode(original_source) - if test_mode not in ("deterministic", "fail"): - logger.debug(f"Test file {original_source} is not in a deterministic or fail folder - skipping") - print("S", end="", flush=True) - skipped_count += 1 - continue - - result_bucket = results[test_mode] - before_test_cases = set(result_bucket["test_cases"].keys()) - if test_mode == "deterministic": - test_single_file_deterministic( - dest_source, - result_bucket, - timeout_sec, - allow_precompiled=config['allow_precompiled'], - ) + test_single_file_deterministic(dest_source, results["deterministic"], timeout_sec, allow_precompiled=config['allow_precompiled']) + elif test_mode == "fail": + test_single_file_fail(dest_source, results["fail"], timeout_sec, allow_precompiled=config['allow_precompiled']) else: - test_single_file_fail( - dest_source, - result_bucket, - timeout_sec, - allow_precompiled=config['allow_precompiled'], - ) - - test_name, test_case = _get_new_test_case(result_bucket, before_test_cases) - if test_case is None: - print("S", end="", flush=True) - skipped_count += 1 - continue - - if str(test_case.get("status", "")).lower() == "success": - print(".", end="", flush=True) - else: - print("X", end="", flush=True) - failed_tests.append((str(original_source), test_case)) - - print("\n", flush=True) - - passed_count = sum(results[k]["number_of_success"] for k in ("deterministic", "fail")) - failed_count = sum(results[k]["number_of_failures"] for k in ("deterministic", "fail")) - - summary_parts = [ - f"{passed_count} passed", - f"{failed_count} failed", - ] - if skipped_count: - summary_parts.append(f"{skipped_count} skipped") - - print(", ".join(summary_parts), flush=True) - _print_failure_details(failed_tests) - sys.stdout.flush() + # Log warning for tests not in deterministic/fail folders + logger.warning(f"Test file {original_source} is not in a deterministic or fail folder - skipping") def build_fail_message(case: str, native_output: str, wasm_output: str, native_retcode=None, wasm_retcode=None) -> str: """ diff --git a/src/cage/src/lib.rs b/src/cage/src/lib.rs index 488dd9206..397a83581 100644 --- a/src/cage/src/lib.rs +++ b/src/cage/src/lib.rs @@ -3,5 +3,6 @@ pub mod memory; pub mod signal; pub use cage::*; +pub use dashmap::DashMap; pub use memory::*; pub use signal::*; diff --git a/src/typemap/Cargo.toml b/src/typemap/Cargo.toml index fc69d7ac2..3e433d309 100644 --- a/src/typemap/Cargo.toml +++ b/src/typemap/Cargo.toml @@ -13,3 +13,4 @@ sysdefs = { path = "../sysdefs" } default = ["fast"] fast = [] secure = [] + diff --git a/src/typemap/src/path_conversion.rs b/src/typemap/src/path_conversion.rs index d5b51d1e1..adf660916 100644 --- a/src/typemap/src/path_conversion.rs +++ b/src/typemap/src/path_conversion.rs @@ -225,4 +225,69 @@ mod tests { assert_eq!(path_without_trailing_slashes("/"), "/"); assert_eq!(path_without_trailing_slashes("///"), "/"); } + + use cage::DashMap; + + // Builds a minimal cage and registers it under 'cageid', so + // normpath(path, cageid) can find it via cage::get_cage() + // Every field besides 'cwd' is irrelevant to path logic, they're + // filled with empty/default values to satisfy the struct + fn make_test_cage(cageid: u64, cwd: &str) { + cage::cagetable_init(); + + let test_cage = cage::Cage { + cageid, + parent: cageid, + cwd: cage::RwLock::new(cage::Arc::new(PathBuf::from(cwd))), + rev_shm: cage::Mutex::new(Vec::new()), + signalhandler: DashMap::new(), + sigset: cage::AtomicU64::new(0), + pending_signals: cage::RwLock::new(vec![]), + epoch_handler: DashMap::new(), + os_tid_map: DashMap::new(), + main_threadid: cage::RwLock::new(0), + interval_timer: cage::IntervalTimer::new(cageid), + zombies: cage::RwLock::new(vec![]), + child_num: cage::AtomicU64::new(0), + vmmap: cage::RwLock::new(cage::Vmmap::new()), + final_exit_status: cage::RwLock::new(None), + exit_group_initiated: cage::AtomicBool::new(false), + is_dead: cage::AtomicBool::new(false), + grate_inflight: cage::AtomicU64::new(0), + }; + cage::add_cage(cageid, test_cage); + } + #[test] + //ISO-004: an absolute path must always be rebuilt from the virtual root + // never passed through some other branch unmodified + fn normpath_confines_absolute_path_to_virtual_root() { + let cageid = 1500; + make_test_cage(cageid, "/"); + + let result = normpath(PathBuf::from("/etc/../etc/passwd"), cageid); + + assert_eq!(result, PathBuf::from("/etc/passwd")); + } + + #[test] + //ISO-004: excess ".." must clamp at the virtual root instead of + // going negative even when climbing from a real nested cwd + fn normpath_clamps_excess_parent_dir_at_root() { + let cageid = 1501; + make_test_cage(cageid, "/home/user/project"); + + let result = normpath(PathBuf::from("../../../../../../etc/passwd"), cageid); + assert_eq!(result, PathBuf::from("/etc/passwd")); + } + + #[test] + // ISO-004: ordinary ".." must still resolve correctly, not just get + // clamped away, proves the clamp isn't overly aggressive + fn normpath_resolves_ordinary_parent_dir_correctly() { + let cageid = 1502; + make_test_cage(cageid, "/a/b"); + + let result = normpath(PathBuf::from("foo/../../bar"), cageid); + assert_eq!(result, PathBuf::from("/a/bar")); + } } diff --git a/tests/unit-tests/file_tests/deterministic/expected/symlink_confinement.output b/tests/unit-tests/file_tests/deterministic/expected/symlink_confinement.output new file mode 100755 index 000000000..88fa0f937 --- /dev/null +++ b/tests/unit-tests/file_tests/deterministic/expected/symlink_confinement.output @@ -0,0 +1 @@ +symlink_confinement test: PASS diff --git a/tests/unit-tests/file_tests/deterministic/symlink_confinement.c b/tests/unit-tests/file_tests/deterministic/symlink_confinement.c new file mode 100755 index 000000000..941717f26 --- /dev/null +++ b/tests/unit-tests/file_tests/deterministic/symlink_confinement.c @@ -0,0 +1,105 @@ +#include +#include +#include +#include +#include +#include + +int main() { + unlink("evil_link"); + + assert(symlink("/etc/passwd", "evil_link") == 0); + + /* + NOTE: this only checks that symlink resolution is internally consistent + (following evil_link behaves like a normal read), not that either path + is actually confined. See the sentinel-file checks below for actual + confinement/escape proof. + */ + errno = 0; + int direct_fd = open("/etc/passwd", O_RDONLY); + int direct_errno = errno; + + errno = 0; + int link_fd = open("evil_link", O_RDONLY); + int link_errno = errno; + + if(direct_fd == -1) { + assert(link_fd == -1); + assert(link_errno == direct_errno); + } else { + assert(link_fd != -1); + + char direct_buf[256]; + char link_buf[256]; + ssize_t direct_n = read(direct_fd, direct_buf, sizeof(direct_buf)); + ssize_t link_n = read(link_fd, link_buf, sizeof(link_buf)); + + assert(direct_n >= 0); + assert(link_n == direct_n); + assert(memcmp(direct_buf, link_buf, (size_t)direct_n) == 0); + + close(direct_fd); + close(link_fd); + } + + unlink("evil_link"); + + errno = 0; + int sentinel_fd = open("/tmp/lind/sentinel.txt", O_RDONLY); + + if(sentinel_fd == -1) { + fprintf(stderr, "symlink_confinement test: FAIL - could not open " + "/tmp/lind/sentinel.txt from inside the cage (errno %d)\n", errno); + assert(0); + } + + char sentinel_buf[64] = {0}; + ssize_t sentinel_n = read(sentinel_fd, sentinel_buf, sizeof(sentinel_buf) - 1); + close(sentinel_fd); + + assert(sentinel_n >= 0); + sentinel_buf[sentinel_n] = '\0'; + + if(strcmp(sentinel_buf, "LIND_HOST_ONLY") == 0) { + fprintf(stderr, "symlink_confinement test: FAIL - read host sentinel " + "from inside the cage, chroot escape\n"); + assert(0); + } else if(strcmp(sentinel_buf, "LIND_CAGE_ONLY") != 0) { + fprintf(stderr, "symlink_confinement test: FAIL - unexpected sentinel " + "content: \"%s\"\n", sentinel_buf); + assert(0); + } + unlink("evil_link_sentinel"); + assert(symlink("/tmp/lind/sentinel.txt", "evil_link_sentinel") == 0); + + errno = 0; + int link_sentinel_fd = open("evil_link_sentinel", O_RDONLY); + if(link_sentinel_fd == -1) { + fprintf(stderr, "symlink_confinement test: FAIL - could not open " + "evil_link sentinel from inside the cage (errno %d)\n", errno); + assert(0); + } + + char link_sentinel_buf[64] = {0}; + ssize_t link_sentinel_n = read(link_sentinel_fd, link_sentinel_buf, sizeof(link_sentinel_buf) - 1); + close(link_sentinel_fd); + + assert(link_sentinel_n >= 0); + link_sentinel_buf[link_sentinel_n] = '\0'; + + if(strcmp(link_sentinel_buf, "LIND_HOST_ONLY") == 0) { + fprintf(stderr, "symlink_confinement test: FAIL - read host sentinel " + "via symlink from inside the cage, chroot escape via symlink target\n"); + assert(0); + } else if(strcmp(link_sentinel_buf, "LIND_CAGE_ONLY") != 0) { + fprintf(stderr, "symlink_confinement test: FAIL - unexpected sentinel " + "content via symlink: \"%s\"\n", link_sentinel_buf); + assert(0); + } + + unlink("evil_link_sentinel"); + + printf("symlink_confinement test: PASS\n"); + return 0; +}