diff --git a/MODULE.bazel b/MODULE.bazel index 253e1b4c8b..d5bdbd6767 100644 --- a/MODULE.bazel +++ b/MODULE.bazel @@ -16,6 +16,7 @@ bazel_dep(name = "bazel_lib", version = "3.0.0") bazel_dep(name = "bazel_features", version = "1.41.0") bazel_dep(name = "bazel_skylib", version = "1.5.0") bazel_dep(name = "platforms", version = "1.0.0") +bazel_dep(name = "rules_cc", version = "0.1.0") bazel_dep(name = "rules_nodejs", version = "6.7.3") # Ensure any version of aspect_bazel_lib used includes: diff --git a/e2e/esm_sandbox/.bazelignore b/e2e/esm_sandbox/.bazelignore new file mode 100644 index 0000000000..3c3629e647 --- /dev/null +++ b/e2e/esm_sandbox/.bazelignore @@ -0,0 +1 @@ +node_modules diff --git a/e2e/esm_sandbox/.bazelrc b/e2e/esm_sandbox/.bazelrc new file mode 100644 index 0000000000..58d7130238 --- /dev/null +++ b/e2e/esm_sandbox/.bazelrc @@ -0,0 +1,2 @@ +import %workspace%/../../tools/preset.bazelrc +import %workspace%/../e2e.bazelrc diff --git a/e2e/esm_sandbox/.bazelversion b/e2e/esm_sandbox/.bazelversion new file mode 120000 index 0000000000..96cf94962b --- /dev/null +++ b/e2e/esm_sandbox/.bazelversion @@ -0,0 +1 @@ +../../.bazelversion \ No newline at end of file diff --git a/e2e/esm_sandbox/BUILD.bazel b/e2e/esm_sandbox/BUILD.bazel new file mode 100644 index 0000000000..081039bd00 --- /dev/null +++ b/e2e/esm_sandbox/BUILD.bazel @@ -0,0 +1,7 @@ +load("@aspect_rules_js//js:defs.bzl", "js_test") + +js_test( + name = "esm_sandbox_test", + data = ["dep.mjs"], + entry_point = "entry.mjs", +) diff --git a/e2e/esm_sandbox/MODULE.bazel b/e2e/esm_sandbox/MODULE.bazel new file mode 100644 index 0000000000..12974c0032 --- /dev/null +++ b/e2e/esm_sandbox/MODULE.bazel @@ -0,0 +1,7 @@ +module(name = "e2e_esm_sandbox") + +bazel_dep(name = "aspect_rules_js", version = "0.0.0", dev_dependency = True) +local_path_override( + module_name = "aspect_rules_js", + path = "../..", +) diff --git a/e2e/esm_sandbox/dep.mjs b/e2e/esm_sandbox/dep.mjs new file mode 100644 index 0000000000..a8375ec980 --- /dev/null +++ b/e2e/esm_sandbox/dep.mjs @@ -0,0 +1,4 @@ +// dep.mjs -- Simple ESM dependency that exports its own import.meta.url +// Used by the ESM sandbox test to verify ESM imports resolve correctly. + +export const depUrl = import.meta.url; diff --git a/e2e/esm_sandbox/entry.mjs b/e2e/esm_sandbox/entry.mjs new file mode 100644 index 0000000000..3c19736f0b --- /dev/null +++ b/e2e/esm_sandbox/entry.mjs @@ -0,0 +1,121 @@ +// entry.mjs -- ESM sandbox integration test for issue #362 +// +// Verifies that ESM imports resolve correctly within the Bazel sandbox +// and that fs.realpathSync.native() does not escape the sandbox. +// +// Issue #362: Node.js ESM resolver captures realpathSync.native() via +// destructuring BEFORE --require patches run, so resolved paths escape +// the Bazel sandbox. The native FS sandbox (LD_PRELOAD) fixes this by +// intercepting libc realpath() at the C level. + +import { depUrl } from "./dep.mjs"; +import { realpathSync } from "node:fs"; +import { fileURLToPath } from "node:url"; +import { dirname } from "node:path"; + +let passed = true; + +function check(description, condition) { + if (condition) { + console.log(` PASS: ${description}`); + } else { + console.log(` FAIL: ${description}`); + passed = false; + } +} + +console.log("ESM sandbox test (issue #362):"); + +// The FS_PATCH_ROOTS env var contains the sandbox roots (colon-separated). +// Paths returned by realpathSync must stay within these roots. +// This env var MUST be set by the js_binary launcher — if it's missing, +// the native sandbox is not active and the test should fail. +const rootsEnv = process.env.JS_BINARY__FS_PATCH_ROOTS; +if (!rootsEnv) { + console.log(" FAIL: JS_BINARY__FS_PATCH_ROOTS is not set — native sandbox not active"); + process.exit(1); +} +const roots = rootsEnv.split(":").filter(Boolean); +if (roots.length === 0) { + console.log(" FAIL: JS_BINARY__FS_PATCH_ROOTS is empty — no sandbox roots configured"); + process.exit(1); +} +console.log(` configured roots: ${roots.length}`); + +function isWithinRoots(p) { + return roots.some(root => p.startsWith(root)); +} + +// Get file paths from import.meta.url +const entryPath = fileURLToPath(import.meta.url); +const depPath = fileURLToPath(depUrl); +const entryDir = dirname(entryPath); + +console.log(` entry path: ${entryPath}`); +console.log(` dep path: ${depPath}`); + +// Verify import.meta.url is a file:// URL +check( + "entry import.meta.url is file:// URL", + import.meta.url.startsWith("file://") +); +check( + "dep import.meta.url is file:// URL", + depUrl.startsWith("file://") +); + +// Verify dep.mjs is in the same directory as entry.mjs +check( + "dep.mjs is in same directory as entry.mjs", + depPath.startsWith(entryDir) +); + +// ---- CORE TEST for issue #362 ---- +// realpathSync.native() is what the ESM resolver uses internally. +// Without the native FS sandbox, this would resolve through symlinks +// to the real execroot OUTSIDE the sandbox. With our fix, it should +// return a path that stays within the configured roots. +try { + const realNative = realpathSync.native(entryPath); + console.log(` realpathSync.native: ${realNative}`); + + check( + "realpathSync.native() returns a valid path", + typeof realNative === "string" && realNative.length > 0 + ); + + check( + "realpathSync.native() stays within sandbox roots", + isWithinRoots(realNative) + ); +} catch (err) { + console.log(` FAIL: realpathSync.native() threw: ${err.message}`); + passed = false; +} + +// Also verify the JS-level realpathSync (patched by --require) +try { + const realJS = realpathSync(entryPath); + console.log(` realpathSync: ${realJS}`); + + check( + "realpathSync() returns a valid path", + typeof realJS === "string" && realJS.length > 0 + ); + + check( + "realpathSync() stays within sandbox roots", + isWithinRoots(realJS) + ); +} catch (err) { + console.log(` FAIL: realpathSync() threw: ${err.message}`); + passed = false; +} + +if (passed) { + console.log("PASS: All ESM sandbox checks passed."); + process.exit(0); +} else { + console.log("FAIL: Some ESM sandbox checks failed."); + process.exit(1); +} diff --git a/js/private/fs_patches_native/BUILD.bazel b/js/private/fs_patches_native/BUILD.bazel new file mode 100644 index 0000000000..c829f9f2e1 --- /dev/null +++ b/js/private/fs_patches_native/BUILD.bazel @@ -0,0 +1,99 @@ +load("@rules_cc//cc:defs.bzl", "cc_binary", "cc_library") + +package(default_visibility = ["//visibility:private"]) + +cc_library( + name = "fs_patch_common", + srcs = [ + "fs_patch_common.c", + "fs_patch_init.c", + ], + hdrs = ["fs_patch.h"], + copts = [ + "-fPIC", + "-Wall", + "-Wextra", + "-std=c11", + ], + linkopts = ["-ldl"], + visibility = [ + "//js/private:__pkg__", + "//js/private/fs_patches_native/test:__pkg__", + ], +) + +cc_binary( + name = "fs_patch_linux.so", + srcs = ["fs_patch_linux.c"], + copts = [ + "-fPIC", + "-Wall", + "-Wextra", + "-std=c11", + ], + linkopts = ["-ldl"], + linkshared = True, + target_compatible_with = ["@platforms//os:linux"], + visibility = ["//visibility:public"], + deps = [":fs_patch_common"], +) + +cc_binary( + name = "fs_patch_macos.dylib", + srcs = ["fs_patch_macos.c"], + copts = [ + "-fPIC", + "-Wall", + "-Wextra", + "-std=c11", + ], + linkopts = ["-ldl"], + linkshared = True, + target_compatible_with = ["@platforms//os:macos"], + visibility = ["//visibility:public"], + deps = [":fs_patch_common"], +) + +config_setting( + name = "_is_linux_x86_64", + constraint_values = [ + "@platforms//os:linux", + "@platforms//cpu:x86_64", + ], +) + +config_setting( + name = "_is_macos_arm64", + constraint_values = [ + "@platforms//os:macos", + "@platforms//cpu:arm64", + ], +) + +config_setting( + name = "_is_macos_x86_64", + constraint_values = [ + "@platforms//os:macos", + "@platforms//cpu:x86_64", + ], +) + +# Noop placeholder for unsupported platforms. +# Native FS patching is supported on Linux x86_64 and macOS (arm64 + x86_64). +# ARM Linux cross-compilation requires a CC toolchain for the target platform. +genrule( + name = "_fs_patch_noop", + outs = ["fs_patch_noop"], + cmd = "touch $@", +) + +alias( + name = "fs_patch_native", + actual = select({ + ":_is_linux_x86_64": ":fs_patch_linux.so", + ":_is_macos_arm64": ":fs_patch_macos.dylib", + ":_is_macos_x86_64": ":fs_patch_macos.dylib", + "//conditions:default": ":_fs_patch_noop", + }), + visibility = ["//visibility:public"], +) diff --git a/js/private/fs_patches_native/fs_patch.h b/js/private/fs_patches_native/fs_patch.h new file mode 100644 index 0000000000..34010b605a --- /dev/null +++ b/js/private/fs_patches_native/fs_patch.h @@ -0,0 +1,118 @@ +/* + * fs_patch.h — Native FS sandbox for rules_js + * + * Intercepts libc filesystem calls via LD_PRELOAD / DYLD_INSERT_LIBRARIES + * to prevent Node.js (and especially ESM imports) from escaping the Bazel + * sandbox / runfiles tree. + * + * See: https://github.com/aspect-build/rules_js/issues/362 + */ +#ifndef FS_PATCH_H_ +#define FS_PATCH_H_ + +#ifndef _GNU_SOURCE +#define _GNU_SOURCE +#endif + +#include +#include +#include +#include +#include +#include + +#ifndef PATH_MAX +#define PATH_MAX 4096 +#endif + +/* Maximum number of sandbox roots (execroot + runfiles + sandbox paths) */ +#define FS_PATCH_MAX_ROOTS 16 + +/* Maximum symlink resolution depth to prevent ELOOP */ +#define FS_PATCH_MAX_SYMLINK_DEPTH 256 + +/* -------------------------------------------------------------------------- + * Configuration + * -------------------------------------------------------------------------- */ + +typedef struct { + char *roots[FS_PATCH_MAX_ROOTS]; + int num_roots; + int enabled; + int debug; +} fs_patch_config_t; + +extern fs_patch_config_t g_config; + +/* -------------------------------------------------------------------------- + * Core logic (fs_patch_common.c) + * -------------------------------------------------------------------------- */ + +/* Returns 1 if child is equal to parent or is under parent/ */ +int is_sub_path(const char *parent, const char *child); + +/* Returns root index (>=0) if link_path is in a root but target_path escapes it. + * Returns -1 if no escape detected. */ +int check_escape(const char *link_path, const char *target_path); + +/* Returns 1 if path is under any configured root */ +int can_escape(const char *path); + +/* Normalize path: resolve . and .. without following symlinks. + * Input must be absolute. Writes to buf (must be PATH_MAX). Returns buf or NULL. */ +char *normalize_path(const char *path, char *buf); + +/* Make path absolute: if relative, prepend cwd. Writes to buf. Returns buf or NULL. */ +char *make_absolute(const char *path, char *buf); + +/* Core guarded realpath: resolves path but stops at sandbox-escaping symlinks. + * If resolved_path is NULL, allocates result (caller must free). + * Returns resolved path or NULL on error (sets errno). */ +char *guarded_realpath(const char *path, char *resolved_path); + +/* -------------------------------------------------------------------------- + * Initialization (fs_patch_init.c) + * -------------------------------------------------------------------------- */ + +/* Called automatically via __attribute__((constructor)). + * Reads env vars, resolves original function pointers. */ +void fs_patch_init(void); + +/* -------------------------------------------------------------------------- + * Original function pointer typedefs — T1 (realpath) only + * -------------------------------------------------------------------------- */ + +typedef char *(*orig_realpath_fn)(const char *restrict, char *restrict); +typedef int (*orig_lstat_fn)(const char *restrict, struct stat *restrict); +typedef ssize_t (*orig_readlink_fn)(const char *restrict, char *restrict, size_t); + +#ifdef __linux__ +typedef char *(*orig___realpath_chk_fn)(const char *, char *, size_t); +typedef char *(*orig_canonicalize_file_name_fn)(const char *); +#endif /* __linux__ */ + +/* -------------------------------------------------------------------------- + * Original function pointer declarations + * -------------------------------------------------------------------------- */ + +extern orig_realpath_fn orig_realpath; +extern orig_lstat_fn orig_lstat; +extern orig_readlink_fn orig_readlink; + +#ifdef __linux__ +extern orig___realpath_chk_fn orig___realpath_chk; +extern orig_canonicalize_file_name_fn orig_canonicalize_file_name; +#endif /* __linux__ */ + +/* -------------------------------------------------------------------------- + * Debug logging + * -------------------------------------------------------------------------- */ + +#define FS_PATCH_DEBUG(fmt, ...) \ + do { \ + if (g_config.debug) { \ + fprintf(stderr, "DEBUG: fs_patch: " fmt "\n", ##__VA_ARGS__); \ + } \ + } while (0) + +#endif /* FS_PATCH_H_ */ diff --git a/js/private/fs_patches_native/fs_patch_common.c b/js/private/fs_patches_native/fs_patch_common.c new file mode 100644 index 0000000000..dca35eca4f --- /dev/null +++ b/js/private/fs_patches_native/fs_patch_common.c @@ -0,0 +1,326 @@ +#define _GNU_SOURCE +#include "fs_patch.h" +#include +#include +#include +#include +#include +#include +#include + +/* ========================================================================== + * is_sub_path — string prefix check with boundary awareness + * ========================================================================== */ + +int is_sub_path(const char *parent, const char *child) { + size_t parent_len = strlen(parent); + size_t child_len = strlen(child); + + if (parent_len > child_len) { + return 0; + } + + if (strncmp(parent, child, parent_len) != 0) { + return 0; + } + + /* Exact match */ + if (parent_len == child_len) { + return 1; + } + + /* Root "/" is parent of everything */ + if (parent_len == 1 && parent[0] == '/') { + return 1; + } + + /* child must have '/' right after parent prefix + * (prevents "/a/bc" matching parent "/a/b") */ + return child[parent_len] == '/'; +} + +/* ========================================================================== + * check_escape — does following link_path -> target_path escape a root? + * ========================================================================== */ + +int check_escape(const char *link_path, const char *target_path) { + for (int i = 0; i < g_config.num_roots; i++) { + if (is_sub_path(g_config.roots[i], link_path) && + !is_sub_path(g_config.roots[i], target_path)) { + return i; + } + } + return -1; +} + +/* ========================================================================== + * can_escape — is path under any configured root? + * ========================================================================== */ + +int can_escape(const char *path) { + for (int i = 0; i < g_config.num_roots; i++) { + if (is_sub_path(g_config.roots[i], path)) { + return 1; + } + } + return 0; +} + +/* ========================================================================== + * normalize_path — resolve . and .. without following symlinks + * ========================================================================== */ + +char *normalize_path(const char *path, char *buf) { + if (!path || path[0] != '/') { + return NULL; + } + + size_t path_len = strlen(path); + if (path_len >= PATH_MAX) { + return NULL; + } + + /* Tokenize a copy */ + char tmp[PATH_MAX]; + memcpy(tmp, path, path_len + 1); + + const char *components[PATH_MAX / 2]; + int depth = 0; + + char *saveptr = NULL; + char *token = strtok_r(tmp, "/", &saveptr); + while (token) { + if (token[0] == '.' && token[1] == '\0') { + /* skip "." */ + } else if (token[0] == '.' && token[1] == '.' && token[2] == '\0') { + if (depth > 0) depth--; + } else { + components[depth++] = token; + } + token = strtok_r(NULL, "/", &saveptr); + } + + /* Reconstruct */ + if (depth == 0) { + buf[0] = '/'; + buf[1] = '\0'; + return buf; + } + + char *p = buf; + for (int i = 0; i < depth; i++) { + *p++ = '/'; + size_t clen = strlen(components[i]); + memcpy(p, components[i], clen); + p += clen; + } + *p = '\0'; + + return buf; +} + +/* ========================================================================== + * make_absolute — prepend cwd if path is relative + * ========================================================================== */ + +char *make_absolute(const char *path, char *buf) { + if (!path) { + return NULL; + } + + if (path[0] == '/') { + size_t len = strlen(path); + if (len >= PATH_MAX) { + errno = ENAMETOOLONG; + return NULL; + } + memcpy(buf, path, len + 1); + return buf; + } + + /* Relative — prepend cwd */ + char cwd[PATH_MAX]; + if (!getcwd(cwd, PATH_MAX)) { + return NULL; + } + + size_t cwd_len = strlen(cwd); + size_t path_len = strlen(path); + + if (cwd_len + 1 + path_len >= PATH_MAX) { + errno = ENAMETOOLONG; + return NULL; + } + + memcpy(buf, cwd, cwd_len); + buf[cwd_len] = '/'; + memcpy(buf + cwd_len + 1, path, path_len + 1); + return buf; +} + +/* ========================================================================== + * guarded_realpath — THE CORE ALGORITHM + * + * Resolves path but stops following symlinks that escape the sandbox. + * + * Strategy: + * 1. Make path absolute, normalize it + * 2. Fast path: call orig_realpath(); if no escape, return it + * 3. Slow path: walk component-by-component with lstat+readlink, + * stopping at the first symlink hop that escapes a root + * ========================================================================== */ + +char *guarded_realpath(const char *path, char *resolved_path) { + if (!g_config.enabled) { + return orig_realpath(path, resolved_path); + } + + /* Make path absolute */ + char abs_input[PATH_MAX]; + if (!make_absolute(path, abs_input)) { + /* ENAMETOOLONG already set by make_absolute, or getcwd failed */ + if (errno != ENAMETOOLONG) { + return orig_realpath(path, resolved_path); + } + return NULL; + } + + /* Normalize (resolve . and ..) */ + char norm_input[PATH_MAX]; + if (!normalize_path(abs_input, norm_input)) { + return orig_realpath(path, resolved_path); + } + + /* Fast path: try real realpath first */ + int saved_errno = errno; + char real_resolved[PATH_MAX]; + char *result = orig_realpath(path, real_resolved); + if (result) { + int escaped_root = check_escape(norm_input, real_resolved); + if (escaped_root < 0) { + /* No escape — return the real resolved path */ + errno = saved_errno; + if (resolved_path) { + memcpy(resolved_path, real_resolved, strlen(real_resolved) + 1); + return resolved_path; + } else { + return strdup(real_resolved); + } + } + /* Escape detected — fall through to slow path */ + } else { + /* orig_realpath failed (e.g., ENOENT for a dangling symlink or + * non-existent path). We still need to handle this — fall through + * to the slow path which can handle partially-resolvable paths. */ + if (errno == ENOENT || errno == EACCES) { + /* These are expected — proceed to slow path */ + } else { + /* Unexpected error — propagate it */ + return NULL; + } + } + + /* ---- Slow path: component-by-component walk ---- */ + char current[PATH_MAX]; + current[0] = '/'; + current[1] = '\0'; + + /* We need a mutable copy for tokenization */ + char remaining[PATH_MAX]; + memcpy(remaining, norm_input, strlen(norm_input) + 1); + + char *saveptr = NULL; + /* Skip leading '/' by starting at remaining+1 */ + char *component = strtok_r(remaining + 1, "/", &saveptr); + + int loop_count = 0; + + while (component) { + if (++loop_count > FS_PATCH_MAX_SYMLINK_DEPTH) { + errno = ELOOP; + return NULL; + } + + /* Build the next path */ + char next[PATH_MAX]; + if (current[0] == '/' && current[1] == '\0') { + snprintf(next, PATH_MAX, "/%s", component); + } else { + snprintf(next, PATH_MAX, "%s/%s", current, component); + } + + struct stat st; + if (orig_lstat(next, &st) != 0) { + /* Component doesn't exist — propagate ENOENT like real realpath(). */ + errno = ENOENT; + return NULL; + } + + if (S_ISLNK(st.st_mode)) { + char link_target[PATH_MAX]; + ssize_t link_len = orig_readlink(next, link_target, PATH_MAX - 1); + if (link_len < 0) { + errno = ENOENT; + return NULL; + } + link_target[link_len] = '\0'; + + /* Make symlink target absolute */ + char abs_target[PATH_MAX]; + if (link_target[0] != '/') { + /* Relative to directory containing the symlink */ + snprintf(abs_target, PATH_MAX, "%s/%s", current, link_target); + } else { + memcpy(abs_target, link_target, link_len + 1); + } + + char norm_target[PATH_MAX]; + if (!normalize_path(abs_target, norm_target)) { + errno = EINVAL; + return NULL; + } + + if (check_escape(next, norm_target) >= 0) { + /* This hop escapes! Stop here — return symlink path + remaining */ + FS_PATCH_DEBUG("guarded_realpath: escape at %s -> %s", + next, norm_target); + + char *rest = strtok_r(NULL, "", &saveptr); + char final[PATH_MAX]; + if (rest && *rest) { + snprintf(final, PATH_MAX, "%s/%s", next, rest); + char renorm[PATH_MAX]; + if (normalize_path(final, renorm)) { + memcpy(final, renorm, strlen(renorm) + 1); + } + } else { + memcpy(final, next, strlen(next) + 1); + } + + if (resolved_path) { + memcpy(resolved_path, final, strlen(final) + 1); + return resolved_path; + } else { + return strdup(final); + } + } + + /* Non-escaping symlink — follow it */ + memcpy(current, norm_target, strlen(norm_target) + 1); + } else { + /* Regular file or directory — just advance */ + memcpy(current, next, strlen(next) + 1); + } + + component = strtok_r(NULL, "/", &saveptr); + } + + /* Reached the end without escaping */ + if (resolved_path) { + memcpy(resolved_path, current, strlen(current) + 1); + return resolved_path; + } else { + return strdup(current); + } +} + diff --git a/js/private/fs_patches_native/fs_patch_init.c b/js/private/fs_patches_native/fs_patch_init.c new file mode 100644 index 0000000000..6c1931d56e --- /dev/null +++ b/js/private/fs_patches_native/fs_patch_init.c @@ -0,0 +1,152 @@ +#define _GNU_SOURCE +#include "fs_patch.h" +#include +#include +#include +#include +#include + +/* Global config instance (zero-initialized) */ +fs_patch_config_t g_config = {0}; + +/* -------------------------------------------------------------------------- + * Original function pointers — T1 (realpath) only + * -------------------------------------------------------------------------- */ + +orig_realpath_fn orig_realpath = NULL; +orig_lstat_fn orig_lstat = NULL; +orig_readlink_fn orig_readlink = NULL; + +#ifdef __linux__ +orig___realpath_chk_fn orig___realpath_chk = NULL; +orig_canonicalize_file_name_fn orig_canonicalize_file_name = NULL; +#endif /* __linux__ */ + +/* -------------------------------------------------------------------------- + * resolve_originals — resolve all original function pointers via dlsym + * -------------------------------------------------------------------------- */ + +static void resolve_originals(void) { + orig_realpath = (orig_realpath_fn)dlsym(RTLD_NEXT, "realpath"); + orig_lstat = (orig_lstat_fn)dlsym(RTLD_NEXT, "lstat"); + orig_readlink = (orig_readlink_fn)dlsym(RTLD_NEXT, "readlink"); + + if (!orig_realpath || !orig_lstat || !orig_readlink) { + fprintf(stderr, + "rules_js fs_patch: FATAL: failed to resolve core libc functions\n"); + abort(); + } + +#ifdef __linux__ + orig___realpath_chk = (orig___realpath_chk_fn)dlsym(RTLD_NEXT, "__realpath_chk"); + orig_canonicalize_file_name = (orig_canonicalize_file_name_fn)dlsym(RTLD_NEXT, "canonicalize_file_name"); +#endif /* __linux__ */ +} + +/* -------------------------------------------------------------------------- + * parse_roots — split colon-separated roots, resolve, sort longest-first + * -------------------------------------------------------------------------- */ + +static void parse_roots(const char *roots_env) { + if (!roots_env || !*roots_env) { + g_config.num_roots = 0; + return; + } + + char *roots_copy = strdup(roots_env); + if (!roots_copy) { + fprintf(stderr, "rules_js fs_patch: failed to allocate memory for roots\n"); + return; + } + + char *saveptr = NULL; + char *token = strtok_r(roots_copy, ":", &saveptr); + int count = 0; + + while (token) { + if (count >= FS_PATCH_MAX_ROOTS) { + fprintf(stderr, "rules_js fs_patch: WARNING: more than %d roots configured, extras ignored\n", + FS_PATCH_MAX_ROOTS); + break; + } + if (*token == '\0') { + token = strtok_r(NULL, ":", &saveptr); + continue; + } + + char resolved[PATH_MAX]; + if (orig_realpath(token, resolved)) { + /* Strip trailing slash (unless it's the root "/") */ + size_t len = strlen(resolved); + if (len > 1 && resolved[len - 1] == '/') { + resolved[len - 1] = '\0'; + } + + g_config.roots[count] = strdup(resolved); + if (g_config.roots[count]) { + count++; + } + } else { + /* Root path doesn't exist — skip (matches JS: roots.filter(existsSync)) */ + FS_PATCH_DEBUG("skipping non-existent root: %s", token); + } + + token = strtok_r(NULL, ":", &saveptr); + } + + g_config.num_roots = count; + free(roots_copy); + + /* Sort roots by length, longest first (most-specific match wins) */ + for (int i = 0; i < count - 1; i++) { + for (int j = i + 1; j < count; j++) { + if (strlen(g_config.roots[j]) > strlen(g_config.roots[i])) { + char *tmp = g_config.roots[i]; + g_config.roots[i] = g_config.roots[j]; + g_config.roots[j] = tmp; + } + } + } +} + +/* -------------------------------------------------------------------------- + * fs_patch_init — library constructor + * -------------------------------------------------------------------------- */ + +__attribute__((constructor)) +void fs_patch_init(void) { + /* Resolve original function pointers FIRST — before anything else */ + resolve_originals(); + + /* Check if patching is enabled */ + const char *patch_enabled = getenv("JS_BINARY__PATCH_NODE_FS"); + if (!patch_enabled || strcmp(patch_enabled, "0") == 0) { + g_config.enabled = 0; + return; + } + + /* Check for debug logging */ + const char *debug_env = getenv("JS_BINARY__LOG_DEBUG"); + g_config.debug = (debug_env && *debug_env) ? 1 : 0; + + /* Parse roots */ + const char *roots_env = getenv("JS_BINARY__FS_PATCH_ROOTS"); + parse_roots(roots_env); + + if (g_config.num_roots == 0) { + FS_PATCH_DEBUG("no valid roots found, disabling patches"); + g_config.enabled = 0; + return; + } + + g_config.enabled = 1; + + if (g_config.debug) { + fprintf(stderr, "DEBUG: fs_patch: native library initialized with %d roots:\n", + g_config.num_roots); + for (int i = 0; i < g_config.num_roots; i++) { + fprintf(stderr, "DEBUG: fs_patch: root[%d]: %s\n", + i, g_config.roots[i]); + } + } +} diff --git a/js/private/fs_patches_native/fs_patch_linux.c b/js/private/fs_patches_native/fs_patch_linux.c new file mode 100644 index 0000000000..8265a8cf0d --- /dev/null +++ b/js/private/fs_patches_native/fs_patch_linux.c @@ -0,0 +1,50 @@ +#define _GNU_SOURCE +#include "fs_patch.h" +#include +#include +#include +#include +#include +#include +#include + +#ifdef __linux__ + +/* ************************************************************************** + * TIER 1 — realpath interpositions + * ************************************************************************** */ + +char *realpath(const char *restrict path, char *restrict resolved_path) { + if (!g_config.enabled) { + return orig_realpath(path, resolved_path); + } + return guarded_realpath(path, resolved_path); +} + +char *__realpath_chk(const char *restrict path, char *restrict resolved_path, + size_t resolved_len) { + if (!g_config.enabled) { + if (orig___realpath_chk) { + return orig___realpath_chk(path, resolved_path, resolved_len); + } + return orig_realpath(path, resolved_path); + } + /* Respect _FORTIFY_SOURCE buffer size check */ + if (resolved_path && resolved_len < PATH_MAX) { + errno = ERANGE; + return NULL; + } + return guarded_realpath(path, resolved_path); +} + +char *canonicalize_file_name(const char *path) { + if (!g_config.enabled) { + if (orig_canonicalize_file_name) { + return orig_canonicalize_file_name(path); + } + return orig_realpath(path, NULL); + } + return guarded_realpath(path, NULL); +} + +#endif /* __linux__ */ diff --git a/js/private/fs_patches_native/fs_patch_macos.c b/js/private/fs_patches_native/fs_patch_macos.c new file mode 100644 index 0000000000..d9367b40ae --- /dev/null +++ b/js/private/fs_patches_native/fs_patch_macos.c @@ -0,0 +1,116 @@ +#include "fs_patch.h" + +#ifdef __APPLE__ + +#include +#include +#include +#include +#include + +/* ************************************************************************** + * macOS DYLD interpose helpers + * + * DYLD __DATA,__interpose causes dlsym(RTLD_NEXT, "lstat") to return the + * INTERPOSED function (our own my_lstat), leading to infinite recursion. + * We use fstatat/readlinkat as non-interposed equivalents. + * ************************************************************************** */ + +static int real_lstat(const char *path, struct stat *buf) { + return fstatat(AT_FDCWD, path, buf, AT_SYMLINK_NOFOLLOW); +} + +static ssize_t real_readlink(const char *path, char *buf, size_t bufsiz) { + return readlinkat(AT_FDCWD, path, buf, bufsiz); +} + +/* ************************************************************************** + * TIER 1 — realpath + * ************************************************************************** */ + +static char *my_realpath(const char *restrict path, + char *restrict resolved_path) { + if (!g_config.enabled) { + return orig_realpath(path, resolved_path); + } + return guarded_realpath(path, resolved_path); +} + +/* ************************************************************************** + * TIER 2 — lstat + * + * If a symlink inside the sandbox would escape when followed, return stat() + * data instead (making it look like a regular file). This prevents Node's + * ESM resolver from following the symlink. + * + * On macOS we don't need seccomp BPF — DYLD_INSERT_LIBRARIES directly + * interposes lstat for all dynamically-linked processes (including esbuild). + * ************************************************************************** */ + +static int my_lstat(const char *restrict path, struct stat *restrict buf) { + /* Use fstatat to avoid recursion — DYLD interposing makes orig_lstat + * point back to my_lstat, but fstatat is not interposed. */ + int ret = real_lstat(path, buf); + if (ret != 0 || !g_config.enabled) { + return ret; + } + + if (S_ISLNK(buf->st_mode) && can_escape(path)) { + char target[PATH_MAX]; + ssize_t len = real_readlink(path, target, PATH_MAX - 1); + if (len > 0) { + target[len] = '\0'; + + char abs_target[PATH_MAX]; + if (target[0] != '/') { + /* Relative target — resolve against directory containing the symlink */ + char dir[PATH_MAX]; + strncpy(dir, path, PATH_MAX - 1); + dir[PATH_MAX - 1] = '\0'; + char *slash = strrchr(dir, '/'); + if (slash) { + slash[1] = '\0'; + snprintf(abs_target, PATH_MAX, "%s%s", dir, target); + } else { + strncpy(abs_target, target, PATH_MAX - 1); + abs_target[PATH_MAX - 1] = '\0'; + } + } else { + strncpy(abs_target, target, PATH_MAX - 1); + abs_target[PATH_MAX - 1] = '\0'; + } + + char norm[PATH_MAX]; + if (normalize_path(abs_target, norm) && check_escape(path, norm) >= 0) { + /* Would escape — make it look like a regular file */ + FS_PATCH_DEBUG("lstat: masking symlink escape at %s -> %s", path, norm); + struct stat real_stat; + if (fstatat(AT_FDCWD, path, &real_stat, 0) == 0) { + *buf = real_stat; + } else { + buf->st_mode = (buf->st_mode & ~S_IFMT) | S_IFREG; + } + } + } + } + + return ret; +} + +/* ************************************************************************** + * DYLD_INSERT_LIBRARIES interpose section + * ************************************************************************** */ + +typedef struct { + const void *replacement; + const void *replacee; +} interpose_t; + +__attribute__((used)) +static const interpose_t interposers[] + __attribute__((section("__DATA,__interpose"))) = { + { (const void *)my_realpath, (const void *)realpath }, + { (const void *)my_lstat, (const void *)lstat }, +}; + +#endif /* __APPLE__ */ diff --git a/js/private/fs_patches_native/test/BUILD.bazel b/js/private/fs_patches_native/test/BUILD.bazel new file mode 100644 index 0000000000..a4b9e65a21 --- /dev/null +++ b/js/private/fs_patches_native/test/BUILD.bazel @@ -0,0 +1,15 @@ +load("@rules_cc//cc:defs.bzl", "cc_test") + +package(default_visibility = ["//visibility:private"]) + +cc_test( + name = "test_common", + srcs = ["test_common.c"], + deps = ["//js/private/fs_patches_native:fs_patch_common"], +) + +cc_test( + name = "test_realpath", + srcs = ["test_realpath.c"], + deps = ["//js/private/fs_patches_native:fs_patch_common"], +) diff --git a/js/private/fs_patches_native/test/test_common.c b/js/private/fs_patches_native/test/test_common.c new file mode 100644 index 0000000000..1600aeb4f1 --- /dev/null +++ b/js/private/fs_patches_native/test/test_common.c @@ -0,0 +1,244 @@ +/* + * test_common.c — Unit tests for fs_patch_common.c core logic + * + * Tests: is_sub_path, check_escape, normalize_path, make_absolute + */ +#define _GNU_SOURCE +#include +#include +#include +#include +#include +#include +#include "../fs_patch.h" + +/* -------------------------------------------------------------------------- + * Minimal test harness + * -------------------------------------------------------------------------- */ + +#define TEST(name) static void name(void) +#define RUN_TEST(name) do { printf(" %s...", #name); name(); printf(" OK\n"); } while(0) +#define ASSERT_EQ(a, b) do { \ + long long _a = (long long)(a), _b = (long long)(b); \ + if (_a != _b) { \ + fprintf(stderr, "FAIL: %s:%d: %lld != %lld\n", __FILE__, __LINE__, _a, _b); \ + exit(1); \ + } \ +} while(0) +#define ASSERT_STREQ(a, b) do { \ + const char *_a = (a), *_b = (b); \ + if (strcmp(_a, _b) != 0) { \ + fprintf(stderr, "FAIL: %s:%d: \"%s\" != \"%s\"\n", __FILE__, __LINE__, _a, _b); \ + exit(1); \ + } \ +} while(0) +#define ASSERT_NULL(a) do { \ + if ((a) != NULL) { \ + fprintf(stderr, "FAIL: %s:%d: expected NULL\n", __FILE__, __LINE__); \ + exit(1); \ + } \ +} while(0) +#define ASSERT_NOT_NULL(a) do { \ + if ((a) == NULL) { \ + fprintf(stderr, "FAIL: %s:%d: unexpected NULL\n", __FILE__, __LINE__); \ + exit(1); \ + } \ +} while(0) + +/* -------------------------------------------------------------------------- + * Helper: reset g_config to a known state + * -------------------------------------------------------------------------- */ + +static void reset_config(void) { + for (int i = 0; i < g_config.num_roots; i++) { + free(g_config.roots[i]); + g_config.roots[i] = NULL; + } + g_config.num_roots = 0; + g_config.enabled = 0; + g_config.debug = 0; +} + +static void set_roots(const char **roots, int count) { + reset_config(); + for (int i = 0; i < count && i < FS_PATCH_MAX_ROOTS; i++) { + g_config.roots[i] = strdup(roots[i]); + } + g_config.num_roots = count; + g_config.enabled = 1; +} + +/* -------------------------------------------------------------------------- + * is_sub_path tests + * -------------------------------------------------------------------------- */ + +TEST(test_is_sub_path_exact_match) { + ASSERT_EQ(is_sub_path("/a/b", "/a/b"), 1); +} + +TEST(test_is_sub_path_child) { + ASSERT_EQ(is_sub_path("/a/b", "/a/b/c/d"), 1); +} + +TEST(test_is_sub_path_parent_longer) { + ASSERT_EQ(is_sub_path("/a/b", "/a"), 0); +} + +TEST(test_is_sub_path_false_prefix) { + /* Critical edge case: "/a/b" must NOT match "/a/bc" */ + ASSERT_EQ(is_sub_path("/a/b", "/a/bc"), 0); +} + +TEST(test_is_sub_path_different_subtree) { + ASSERT_EQ(is_sub_path("/a/b", "/a/c/b"), 0); +} + +TEST(test_is_sub_path_root_matches_all) { + ASSERT_EQ(is_sub_path("/", "/anything"), 1); +} + +TEST(test_is_sub_path_completely_different) { + ASSERT_EQ(is_sub_path("/a/b", "/x/y"), 0); +} + +/* -------------------------------------------------------------------------- + * check_escape tests + * -------------------------------------------------------------------------- */ + +TEST(test_check_escape_in_root_to_in_root) { + const char *roots[] = {"/sandbox", "/runfiles"}; + set_roots(roots, 2); + /* Link in /sandbox, target also in /sandbox — no escape */ + ASSERT_EQ(check_escape("/sandbox/link", "/sandbox/target"), -1); +} + +TEST(test_check_escape_in_root_to_outside) { + const char *roots[] = {"/sandbox", "/runfiles"}; + set_roots(roots, 2); + /* Link in /sandbox, target outside — escape detected */ + int result = check_escape("/sandbox/link", "/outside/target"); + ASSERT_EQ(result >= 0, 1); /* Should return root index */ +} + +TEST(test_check_escape_outside_to_anywhere) { + const char *roots[] = {"/sandbox", "/runfiles"}; + set_roots(roots, 2); + /* Link outside any root — not our concern, returns -1 */ + ASSERT_EQ(check_escape("/other/link", "/outside/target"), -1); +} + +TEST(test_check_escape_root_a_to_root_b) { + const char *roots[] = {"/sandbox", "/runfiles"}; + set_roots(roots, 2); + /* Link in /sandbox, target in /runfiles — escaped from sandbox */ + int result = check_escape("/sandbox/link", "/runfiles/target"); + ASSERT_EQ(result >= 0, 1); /* Should return index of /sandbox root */ +} + +/* -------------------------------------------------------------------------- + * normalize_path tests + * -------------------------------------------------------------------------- */ + +TEST(test_normalize_path_dotdot) { + char buf[PATH_MAX]; + char *result = normalize_path("/a/b/../c", buf); + ASSERT_NOT_NULL(result); + ASSERT_STREQ(result, "/a/c"); +} + +TEST(test_normalize_path_dot) { + char buf[PATH_MAX]; + char *result = normalize_path("/a/./b/./c", buf); + ASSERT_NOT_NULL(result); + ASSERT_STREQ(result, "/a/b/c"); +} + +TEST(test_normalize_path_double_slash) { + char buf[PATH_MAX]; + char *result = normalize_path("/a//b///c", buf); + ASSERT_NOT_NULL(result); + ASSERT_STREQ(result, "/a/b/c"); +} + +TEST(test_normalize_path_root) { + char buf[PATH_MAX]; + char *result = normalize_path("/", buf); + ASSERT_NOT_NULL(result); + ASSERT_STREQ(result, "/"); +} + +TEST(test_normalize_path_multiple_dotdot) { + char buf[PATH_MAX]; + char *result = normalize_path("/a/b/../../c", buf); + ASSERT_NOT_NULL(result); + ASSERT_STREQ(result, "/c"); +} + +/* -------------------------------------------------------------------------- + * make_absolute tests + * -------------------------------------------------------------------------- */ + +TEST(test_make_absolute_already_absolute) { + char buf[PATH_MAX]; + char *result = make_absolute("/already/absolute", buf); + ASSERT_NOT_NULL(result); + ASSERT_STREQ(result, "/already/absolute"); +} + +TEST(test_make_absolute_relative) { + char buf[PATH_MAX]; + char cwd[PATH_MAX]; + ASSERT_NOT_NULL(getcwd(cwd, sizeof(cwd))); + + char *result = make_absolute("relative/path", buf); + ASSERT_NOT_NULL(result); + + /* Should start with cwd */ + char expected[PATH_MAX]; + snprintf(expected, sizeof(expected), "%s/relative/path", cwd); + ASSERT_STREQ(result, expected); +} + +/* -------------------------------------------------------------------------- + * main + * -------------------------------------------------------------------------- */ + +int main(void) { + /* Initialize orig_* function pointers to real libc functions. + * We do this manually instead of calling fs_patch_init() to avoid + * needing environment variables set up. */ + orig_realpath = realpath; + orig_lstat = lstat; + orig_readlink = readlink; + + printf("test_common:\n"); + + printf(" is_sub_path:\n"); + RUN_TEST(test_is_sub_path_exact_match); + RUN_TEST(test_is_sub_path_child); + RUN_TEST(test_is_sub_path_parent_longer); + RUN_TEST(test_is_sub_path_false_prefix); + RUN_TEST(test_is_sub_path_different_subtree); + RUN_TEST(test_is_sub_path_root_matches_all); + RUN_TEST(test_is_sub_path_completely_different); + + printf(" check_escape:\n"); + RUN_TEST(test_check_escape_in_root_to_in_root); + RUN_TEST(test_check_escape_in_root_to_outside); + RUN_TEST(test_check_escape_outside_to_anywhere); + RUN_TEST(test_check_escape_root_a_to_root_b); + + printf(" normalize_path:\n"); + RUN_TEST(test_normalize_path_dotdot); + RUN_TEST(test_normalize_path_dot); + RUN_TEST(test_normalize_path_double_slash); + RUN_TEST(test_normalize_path_root); + RUN_TEST(test_normalize_path_multiple_dotdot); + + printf(" make_absolute:\n"); + RUN_TEST(test_make_absolute_already_absolute); + RUN_TEST(test_make_absolute_relative); + + printf("All tests passed.\n"); + return 0; +} diff --git a/js/private/fs_patches_native/test/test_realpath.c b/js/private/fs_patches_native/test/test_realpath.c new file mode 100644 index 0000000000..af505fafb0 --- /dev/null +++ b/js/private/fs_patches_native/test/test_realpath.c @@ -0,0 +1,274 @@ +/* + * test_realpath.c — Unit tests for guarded_realpath + * + * Creates real symlinks in temp directories to test that guarded_realpath + * correctly follows in-root symlinks and stops at escaping symlinks. + */ +#define _GNU_SOURCE +#include +#include +#include +#include +#include +#include +#include +#include "../fs_patch.h" + +/* -------------------------------------------------------------------------- + * Minimal test harness + * -------------------------------------------------------------------------- */ + +#define TEST(name) static void name(void) +#define RUN_TEST(name) do { printf(" %s...", #name); name(); printf(" OK\n"); } while(0) +#define ASSERT_EQ(a, b) do { \ + long long _a = (long long)(a), _b = (long long)(b); \ + if (_a != _b) { \ + fprintf(stderr, "FAIL: %s:%d: %lld != %lld\n", __FILE__, __LINE__, _a, _b); \ + exit(1); \ + } \ +} while(0) +#define ASSERT_STREQ(a, b) do { \ + const char *_a = (a), *_b = (b); \ + if (strcmp(_a, _b) != 0) { \ + fprintf(stderr, "FAIL: %s:%d: \"%s\" != \"%s\"\n", __FILE__, __LINE__, _a, _b); \ + exit(1); \ + } \ +} while(0) +#define ASSERT_NULL(a) do { \ + if ((a) != NULL) { \ + fprintf(stderr, "FAIL: %s:%d: expected NULL, got \"%s\"\n", \ + __FILE__, __LINE__, (const char *)(a)); \ + exit(1); \ + } \ +} while(0) +#define ASSERT_NOT_NULL(a) do { \ + if ((a) == NULL) { \ + fprintf(stderr, "FAIL: %s:%d: unexpected NULL (errno=%d: %s)\n", \ + __FILE__, __LINE__, errno, strerror(errno)); \ + exit(1); \ + } \ +} while(0) + +/* -------------------------------------------------------------------------- + * Test fixtures + * -------------------------------------------------------------------------- */ + +static char sandbox_dir[PATH_MAX]; /* resolved path to sandbox temp dir */ +static char external_dir[PATH_MAX]; /* resolved path to external temp dir */ + +static void create_file(const char *path) { + FILE *f = fopen(path, "w"); + if (!f) { + fprintf(stderr, "Failed to create file: %s (%s)\n", path, strerror(errno)); + exit(1); + } + fprintf(f, "test content\n"); + fclose(f); +} + +static void setup(void) { + char sandbox_tmpl[PATH_MAX]; + char external_tmpl[PATH_MAX]; + + snprintf(sandbox_tmpl, sizeof(sandbox_tmpl), "/tmp/test_sandbox_XXXXXX"); + snprintf(external_tmpl, sizeof(external_tmpl), "/tmp/test_external_XXXXXX"); + + char *sd = mkdtemp(sandbox_tmpl); + char *ed = mkdtemp(external_tmpl); + if (!sd || !ed) { + fprintf(stderr, "mkdtemp failed: %s\n", strerror(errno)); + exit(1); + } + + /* Resolve to canonical paths */ + if (!realpath(sd, sandbox_dir) || !realpath(ed, external_dir)) { + fprintf(stderr, "realpath failed: %s\n", strerror(errno)); + exit(1); + } + + /* Create sandbox structure: + * sandbox_dir/a.txt + * sandbox_dir/subdir/ + * sandbox_dir/link_in -> a.txt (in-root symlink) + * sandbox_dir/link_out -> external/b.txt (escaping symlink) + */ + char path[PATH_MAX]; + + snprintf(path, sizeof(path), "%s/a.txt", sandbox_dir); + create_file(path); + + snprintf(path, sizeof(path), "%s/subdir", sandbox_dir); + if (mkdir(path, 0755) != 0) { + fprintf(stderr, "mkdir failed: %s (%s)\n", path, strerror(errno)); + exit(1); + } + + /* In-root symlink: link_in -> a.txt (relative) */ + char linkpath[PATH_MAX]; + snprintf(linkpath, sizeof(linkpath), "%s/link_in", sandbox_dir); + if (symlink("a.txt", linkpath) != 0) { + fprintf(stderr, "symlink failed: %s (%s)\n", linkpath, strerror(errno)); + exit(1); + } + + /* External directory with a file */ + snprintf(path, sizeof(path), "%s/b.txt", external_dir); + create_file(path); + + /* Escaping symlink: link_out -> external_dir/b.txt (absolute) */ + char target[PATH_MAX]; + snprintf(target, sizeof(target), "%s/b.txt", external_dir); + snprintf(linkpath, sizeof(linkpath), "%s/link_out", sandbox_dir); + if (symlink(target, linkpath) != 0) { + fprintf(stderr, "symlink failed: %s -> %s (%s)\n", linkpath, target, strerror(errno)); + exit(1); + } + + /* Configure g_config with sandbox_dir as root */ + g_config.roots[0] = strdup(sandbox_dir); + g_config.num_roots = 1; + g_config.enabled = 1; + g_config.debug = 0; +} + +static void teardown(void) { + char cmd[PATH_MAX * 2 + 16]; + snprintf(cmd, sizeof(cmd), "rm -rf %s %s", sandbox_dir, external_dir); + (void)system(cmd); + + for (int i = 0; i < g_config.num_roots; i++) { + free(g_config.roots[i]); + g_config.roots[i] = NULL; + } + g_config.num_roots = 0; + g_config.enabled = 0; +} + +/* -------------------------------------------------------------------------- + * Tests + * -------------------------------------------------------------------------- */ + +TEST(test_guarded_realpath_regular_file) { + /* Regular file in sandbox should resolve normally */ + char path[PATH_MAX]; + snprintf(path, sizeof(path), "%s/a.txt", sandbox_dir); + + char resolved[PATH_MAX]; + char *result = guarded_realpath(path, resolved); + ASSERT_NOT_NULL(result); + + /* Should resolve to the real path of a.txt */ + char expected[PATH_MAX]; + snprintf(expected, sizeof(expected), "%s/a.txt", sandbox_dir); + ASSERT_STREQ(result, expected); +} + +TEST(test_guarded_realpath_in_root_symlink) { + /* In-root symlink: link_in -> a.txt — should follow and resolve */ + char path[PATH_MAX]; + snprintf(path, sizeof(path), "%s/link_in", sandbox_dir); + + char resolved[PATH_MAX]; + char *result = guarded_realpath(path, resolved); + ASSERT_NOT_NULL(result); + + /* Should resolve to the real path of a.txt (followed the symlink) */ + char expected[PATH_MAX]; + snprintf(expected, sizeof(expected), "%s/a.txt", sandbox_dir); + ASSERT_STREQ(result, expected); +} + +TEST(test_guarded_realpath_escaping_symlink) { + /* Escaping symlink: link_out -> external_dir/b.txt + * Should NOT follow to the external target. + * Instead should return the link path itself. */ + char path[PATH_MAX]; + snprintf(path, sizeof(path), "%s/link_out", sandbox_dir); + + char resolved[PATH_MAX]; + char *result = guarded_realpath(path, resolved); + ASSERT_NOT_NULL(result); + + /* The result should be the link path, not the external target */ + char not_expected[PATH_MAX]; + snprintf(not_expected, sizeof(not_expected), "%s/b.txt", external_dir); + + /* Result should NOT be the external path */ + if (strcmp(result, not_expected) == 0) { + fprintf(stderr, "FAIL: %s:%d: guarded_realpath followed escaping symlink to %s\n", + __FILE__, __LINE__, result); + exit(1); + } + + /* Result should still be something under sandbox_dir or the link path itself */ + snprintf(path, sizeof(path), "%s/link_out", sandbox_dir); + ASSERT_STREQ(result, path); +} + +TEST(test_guarded_realpath_outside_root) { + /* Path entirely outside sandbox — guarded_realpath should resolve normally + * (we only guard symlinks that escape FROM a root, not paths already outside) */ + char path[PATH_MAX]; + snprintf(path, sizeof(path), "%s/b.txt", external_dir); + + char resolved[PATH_MAX]; + char *result = guarded_realpath(path, resolved); + ASSERT_NOT_NULL(result); + + /* Should resolve normally since it's not in any root */ + char expected[PATH_MAX]; + snprintf(expected, sizeof(expected), "%s/b.txt", external_dir); + ASSERT_STREQ(result, expected); +} + +TEST(test_guarded_realpath_nonexistent) { + /* Non-existent path should return NULL with ENOENT */ + char path[PATH_MAX]; + snprintf(path, sizeof(path), "%s/does_not_exist.txt", sandbox_dir); + + errno = 0; + char *result = guarded_realpath(path, NULL); + ASSERT_NULL(result); + ASSERT_EQ(errno, ENOENT); +} + +TEST(test_guarded_realpath_null_resolved) { + /* When resolved_path is NULL, guarded_realpath should allocate the result */ + char path[PATH_MAX]; + snprintf(path, sizeof(path), "%s/a.txt", sandbox_dir); + + char *result = guarded_realpath(path, NULL); + ASSERT_NOT_NULL(result); + + char expected[PATH_MAX]; + snprintf(expected, sizeof(expected), "%s/a.txt", sandbox_dir); + ASSERT_STREQ(result, expected); + + free(result); +} + +/* -------------------------------------------------------------------------- + * main + * -------------------------------------------------------------------------- */ + +int main(void) { + /* Initialize orig_* function pointers to real libc functions */ + orig_realpath = realpath; + orig_lstat = lstat; + orig_readlink = readlink; + + printf("test_realpath:\n"); + setup(); + + RUN_TEST(test_guarded_realpath_regular_file); + RUN_TEST(test_guarded_realpath_in_root_symlink); + RUN_TEST(test_guarded_realpath_escaping_symlink); + RUN_TEST(test_guarded_realpath_outside_root); + RUN_TEST(test_guarded_realpath_nonexistent); + RUN_TEST(test_guarded_realpath_null_resolved); + + teardown(); + + printf("All tests passed.\n"); + return 0; +} diff --git a/js/private/js_binary.bzl b/js/private/js_binary.bzl index 2c67f7219b..8d5ade6974 100644 --- a/js/private/js_binary.bzl +++ b/js/private/js_binary.bzl @@ -322,6 +322,10 @@ _ATTRS = { allow_single_file = True, default = Label("@aspect_rules_js//js/private/node-patches:register.cjs"), ), + "_fs_patch_native": attr.label( + allow_single_file = True, + default = Label("@aspect_rules_js//js/private/fs_patches_native:fs_patch_native"), + ), } _ENV_SET = """export {var}={quoted_value}""" @@ -486,6 +490,7 @@ def _bash_launcher(ctx, nodeinfo, entry_point_path, log_prefix_rule_set, log_pre "{{node_wrapper}}": node_wrapper.short_path, "{{node}}": node_path, "{{npm}}": npm_path, + "{{fs_patch_native}}": ctx.file._fs_patch_native.short_path if (ctx.attr.patch_node_fs and not is_windows and (ctx.file._fs_patch_native.basename.endswith(".so") or ctx.file._fs_patch_native.basename.endswith(".dylib"))) else "", "{{workspace_name}}": ctx.workspace_name, } @@ -531,6 +536,11 @@ def _create_launcher(ctx, log_prefix_rule_set, log_prefix_rule, fixed_args = [], launcher_files.append(nodeinfo.node) launcher_files.extend(ctx.files._node_patches_files + [ctx.file._node_patches]) + if ctx.attr.patch_node_fs and not is_windows and ctx.file._fs_patch_native: + # Only include the native fs patch library (not the noop placeholder). + # Native FS patching is currently only supported on Linux x86_64. + if ctx.file._fs_patch_native.basename.endswith(".so") or ctx.file._fs_patch_native.basename.endswith(".dylib"): + launcher_files.append(ctx.file._fs_patch_native) transitive_launcher_files = None if ctx.attr.include_npm: transitive_launcher_files = nodeinfo.npm_sources diff --git a/js/private/js_binary.sh.tpl b/js/private/js_binary.sh.tpl index 366bded72f..368962323e 100644 --- a/js/private/js_binary.sh.tpl +++ b/js/private/js_binary.sh.tpl @@ -357,6 +357,39 @@ if [ -z "${JS_BINARY__FS_PATCH_ROOTS:-}" ]; then fi export JS_BINARY__FS_PATCH_ROOTS +# Configure native fs patch library (LD_PRELOAD on Linux, DYLD_INSERT_LIBRARIES on macOS) +fs_patch_native="{{fs_patch_native}}" +if [ "$fs_patch_native" ] && [ "${JS_BINARY__PATCH_NODE_FS:-}" != "0" ]; then + if [ "${JS_BINARY__NO_RUNFILES:-}" ]; then + fs_patch_native_path=$(resolve_execroot_bin_path "$fs_patch_native") + else + fs_patch_native_path="$JS_BINARY__RUNFILES/{{workspace_name}}/$fs_patch_native" + fi + + if [ -f "$fs_patch_native_path" ]; then + case "$(uname -s)" in + Linux*) + if [ -z "${LD_PRELOAD:-}" ]; then + export LD_PRELOAD="$fs_patch_native_path" + else + export LD_PRELOAD="$fs_patch_native_path:$LD_PRELOAD" + fi + logf_debug "LD_PRELOAD %s" "$LD_PRELOAD" + ;; + Darwin*) + # On macOS, DYLD_INSERT_LIBRARIES is stripped by SIP when the exec chain + # passes through system binaries (/bin/bash, /usr/bin/env). Instead, pass + # the path via a SIP-safe env var. The node wrapper will set + # DYLD_INSERT_LIBRARIES right before exec'ing the node binary. + export JS_BINARY__NATIVE_PATCH_PATH="$fs_patch_native_path" + logf_debug "JS_BINARY__NATIVE_PATCH_PATH %s" "$JS_BINARY__NATIVE_PATCH_PATH" + ;; + esac + else + logf_warn "native fs patch library not found at %s" "$fs_patch_native_path" + fi +fi + # Enable coverage if requested if [ "${COVERAGE_DIR:-}" ]; then logf_debug "enabling v8 coverage support ${COVERAGE_DIR}" diff --git a/js/private/node_wrapper.sh b/js/private/node_wrapper.sh index fae2c2a56f..bceae6eae7 100755 --- a/js/private/node_wrapper.sh +++ b/js/private/node_wrapper.sh @@ -2,4 +2,17 @@ set -o pipefail -o errexit -o nounset +# On macOS, restore DYLD_INSERT_LIBRARIES from the SIP-safe env var. +# macOS SIP strips DYLD_* vars when exec goes through /bin/bash or /usr/bin/env, +# so the launcher passes the native patch library path via JS_BINARY__NATIVE_PATCH_PATH. +# We set DYLD_INSERT_LIBRARIES here, right before exec'ing the node binary (which is +# not SIP-restricted), so the dynamic linker will load our interpose library. +if [ "${JS_BINARY__NATIVE_PATCH_PATH:-}" ]; then + if [ -z "${DYLD_INSERT_LIBRARIES:-}" ]; then + export DYLD_INSERT_LIBRARIES="$JS_BINARY__NATIVE_PATCH_PATH" + else + export DYLD_INSERT_LIBRARIES="$JS_BINARY__NATIVE_PATCH_PATH:$DYLD_INSERT_LIBRARIES" + fi +fi + exec "$JS_BINARY__NODE_BINARY" --require "$JS_BINARY__NODE_PATCHES" "$@" diff --git a/js/private/test/image/asserts.bzl b/js/private/test/image/asserts.bzl index 87e42207cc..abc52364c4 100644 --- a/js/private/test/image/asserts.bzl +++ b/js/private/test/image/asserts.bzl @@ -5,8 +5,11 @@ load("//js:defs.bzl", "js_image_layer") # buildifier: disable=function-docstring def assert_tar_listing(name, actual, expected): - # Either of these two file sizes may be observed on a file like /js/private/test/image/bin - sanitize_cmd = "sed -E 's/239[0-9]{2}|24[0-9]{3}/xxxxx/g'" + # Binary sizes vary across compilers/environments. Replace known variable-size ranges with xxxxx: + # - 239XX, 24XXX: launcher binary (bin) + # - 17XXX: native fs patch library (fs_patch_linux.so) + # Space-anchored to avoid matching substrings of larger numbers (e.g. 217950) + sanitize_cmd = "sed -E 's/ (239[0-9]{2}|24[0-9]{3}|17[0-9]{3}) / xxxxx /g'" actual_listing = "_{}_listing".format(name) native.genrule( name = actual_listing, diff --git a/js/private/test/image/custom_owner_test_app.listing b/js/private/test/image/custom_owner_test_app.listing index db40c9011b..90d13c81ff 100644 --- a/js/private/test/image/custom_owner_test_app.listing +++ b/js/private/test/image/custom_owner_test_app.listing @@ -2,7 +2,7 @@ drwxr-xr-x 0 100 0 0 Jan 1 1970 ./js/ drwxr-xr-x 0 100 0 0 Jan 1 1970 ./js/private/ drwxr-xr-x 0 100 0 0 Jan 1 1970 ./js/private/test/ drwxr-xr-x 0 100 0 0 Jan 1 1970 ./js/private/test/image/ --r-xr-xr-x 0 100 0 xxxxx Jan 1 1970 ./js/private/test/image/bin +-r-xr-xr-x 0 100 0 25132 Jan 1 1970 ./js/private/test/image/bin drwxr-xr-x 0 100 0 0 Jan 1 1970 ./js/private/test/image/bin.runfiles/ drwxr-xr-x 0 100 0 0 Jan 1 1970 ./js/private/test/image/bin.runfiles/_main/ drwxr-xr-x 0 100 0 0 Jan 1 1970 ./js/private/test/image/bin.runfiles/_main/examples/ @@ -14,10 +14,12 @@ drwxr-xr-x 0 100 0 0 Jan 1 1970 ./js/private/test/image/bin.runf -r-xr-xr-x 0 100 0 336 Jan 1 1970 ./js/private/test/image/bin.runfiles/_main/examples/npm_package/packages/pkg_d/package.json drwxr-xr-x 0 100 0 0 Jan 1 1970 ./js/private/test/image/bin.runfiles/_main/js/ drwxr-xr-x 0 100 0 0 Jan 1 1970 ./js/private/test/image/bin.runfiles/_main/js/private/ +drwxr-xr-x 0 100 0 0 Jan 1 1970 ./js/private/test/image/bin.runfiles/_main/js/private/fs_patches_native/ +-r-xr-xr-x 0 100 0 xxxxx Jan 1 1970 ./js/private/test/image/bin.runfiles/_main/js/private/fs_patches_native/fs_patch_linux.so drwxr-xr-x 0 100 0 0 Jan 1 1970 ./js/private/test/image/bin.runfiles/_main/js/private/test/ drwxr-xr-x 0 100 0 0 Jan 1 1970 ./js/private/test/image/bin.runfiles/_main/js/private/test/image/ drwxr-xr-x 0 100 0 0 Jan 1 1970 ./js/private/test/image/bin.runfiles/_main/js/private/test/image/bin_/ --r-xr-xr-x 0 100 0 xxxxx Jan 1 1970 ./js/private/test/image/bin.runfiles/_main/js/private/test/image/bin_/bin +-r-xr-xr-x 0 100 0 25132 Jan 1 1970 ./js/private/test/image/bin.runfiles/_main/js/private/test/image/bin_/bin drwxr-xr-x 0 100 0 0 Jan 1 1970 ./js/private/test/image/bin.runfiles/_main/js/private/test/image/bin_node_bin/ -r-xr-xr-x 0 100 0 133 Jan 1 1970 ./js/private/test/image/bin.runfiles/_main/js/private/test/image/bin_node_bin/node -r-xr-xr-x 0 100 0 20 Jan 1 1970 ./js/private/test/image/bin.runfiles/_main/js/private/test/image/main.js diff --git a/js/private/test/image/default_test_app.listing b/js/private/test/image/default_test_app.listing index 3c596c1d21..4f65d7c919 100644 --- a/js/private/test/image/default_test_app.listing +++ b/js/private/test/image/default_test_app.listing @@ -2,7 +2,7 @@ drwxr-xr-x 0 0 0 0 Jan 1 1970 ./js/ drwxr-xr-x 0 0 0 0 Jan 1 1970 ./js/private/ drwxr-xr-x 0 0 0 0 Jan 1 1970 ./js/private/test/ drwxr-xr-x 0 0 0 0 Jan 1 1970 ./js/private/test/image/ --r-xr-xr-x 0 0 0 xxxxx Jan 1 1970 ./js/private/test/image/bin +-r-xr-xr-x 0 0 0 25132 Jan 1 1970 ./js/private/test/image/bin drwxr-xr-x 0 0 0 0 Jan 1 1970 ./js/private/test/image/bin.runfiles/ drwxr-xr-x 0 0 0 0 Jan 1 1970 ./js/private/test/image/bin.runfiles/_main/ drwxr-xr-x 0 0 0 0 Jan 1 1970 ./js/private/test/image/bin.runfiles/_main/examples/ @@ -14,10 +14,12 @@ drwxr-xr-x 0 0 0 0 Jan 1 1970 ./js/private/test/image/bin.runf -r-xr-xr-x 0 0 0 336 Jan 1 1970 ./js/private/test/image/bin.runfiles/_main/examples/npm_package/packages/pkg_d/package.json drwxr-xr-x 0 0 0 0 Jan 1 1970 ./js/private/test/image/bin.runfiles/_main/js/ drwxr-xr-x 0 0 0 0 Jan 1 1970 ./js/private/test/image/bin.runfiles/_main/js/private/ +drwxr-xr-x 0 0 0 0 Jan 1 1970 ./js/private/test/image/bin.runfiles/_main/js/private/fs_patches_native/ +-r-xr-xr-x 0 0 0 xxxxx Jan 1 1970 ./js/private/test/image/bin.runfiles/_main/js/private/fs_patches_native/fs_patch_linux.so drwxr-xr-x 0 0 0 0 Jan 1 1970 ./js/private/test/image/bin.runfiles/_main/js/private/test/ drwxr-xr-x 0 0 0 0 Jan 1 1970 ./js/private/test/image/bin.runfiles/_main/js/private/test/image/ drwxr-xr-x 0 0 0 0 Jan 1 1970 ./js/private/test/image/bin.runfiles/_main/js/private/test/image/bin_/ --r-xr-xr-x 0 0 0 xxxxx Jan 1 1970 ./js/private/test/image/bin.runfiles/_main/js/private/test/image/bin_/bin +-r-xr-xr-x 0 0 0 25132 Jan 1 1970 ./js/private/test/image/bin.runfiles/_main/js/private/test/image/bin_/bin drwxr-xr-x 0 0 0 0 Jan 1 1970 ./js/private/test/image/bin.runfiles/_main/js/private/test/image/bin_node_bin/ -r-xr-xr-x 0 0 0 133 Jan 1 1970 ./js/private/test/image/bin.runfiles/_main/js/private/test/image/bin_node_bin/node -r-xr-xr-x 0 0 0 20 Jan 1 1970 ./js/private/test/image/bin.runfiles/_main/js/private/test/image/main.js diff --git a/js/private/test/image/non_ascii/custom_layer_groups_test_app.listing b/js/private/test/image/non_ascii/custom_layer_groups_test_app.listing index 6418c3c821..bb678808b8 100644 --- a/js/private/test/image/non_ascii/custom_layer_groups_test_app.listing +++ b/js/private/test/image/non_ascii/custom_layer_groups_test_app.listing @@ -4,17 +4,19 @@ drwxr-xr-x 0 0 0 0 Jan 1 1970 ./app/js/private/ drwxr-xr-x 0 0 0 0 Jan 1 1970 ./app/js/private/test/ drwxr-xr-x 0 0 0 0 Jan 1 1970 ./app/js/private/test/image/ drwxr-xr-x 0 0 0 0 Jan 1 1970 ./app/js/private/test/image/non_ascii/ --r-xr-xr-x 0 0 0 xxxxx Jan 1 1970 ./app/js/private/test/image/non_ascii/bin2 +-r-xr-xr-x 0 0 0 25227 Jan 1 1970 ./app/js/private/test/image/non_ascii/bin2 drwxr-xr-x 0 0 0 0 Jan 1 1970 ./app/js/private/test/image/non_ascii/bin2.runfiles/ drwxr-xr-x 0 0 0 0 Jan 1 1970 ./app/js/private/test/image/non_ascii/bin2.runfiles/_main/ drwxr-xr-x 0 0 0 0 Jan 1 1970 ./app/js/private/test/image/non_ascii/bin2.runfiles/_main/js/ drwxr-xr-x 0 0 0 0 Jan 1 1970 ./app/js/private/test/image/non_ascii/bin2.runfiles/_main/js/private/ +drwxr-xr-x 0 0 0 0 Jan 1 1970 ./app/js/private/test/image/non_ascii/bin2.runfiles/_main/js/private/fs_patches_native/ +-r-xr-xr-x 0 0 0 xxxxx Jan 1 1970 ./app/js/private/test/image/non_ascii/bin2.runfiles/_main/js/private/fs_patches_native/fs_patch_linux.so drwxr-xr-x 0 0 0 0 Jan 1 1970 ./app/js/private/test/image/non_ascii/bin2.runfiles/_main/js/private/test/ drwxr-xr-x 0 0 0 0 Jan 1 1970 ./app/js/private/test/image/non_ascii/bin2.runfiles/_main/js/private/test/image/ drwxr-xr-x 0 0 0 0 Jan 1 1970 ./app/js/private/test/image/non_ascii/bin2.runfiles/_main/js/private/test/image/non_ascii/ -r-xr-xr-x 0 0 0 0 Jan 1 1970 ./app/js/private/test/image/non_ascii/bin2.runfiles/_main/js/private/test/image/non_ascii/ㅑㅕㅣㅇ.ㄴㅅ drwxr-xr-x 0 0 0 0 Jan 1 1970 ./app/js/private/test/image/non_ascii/bin2.runfiles/_main/js/private/test/image/non_ascii/bin2_/ --r-xr-xr-x 0 0 0 xxxxx Jan 1 1970 ./app/js/private/test/image/non_ascii/bin2.runfiles/_main/js/private/test/image/non_ascii/bin2_/bin2 +-r-xr-xr-x 0 0 0 25227 Jan 1 1970 ./app/js/private/test/image/non_ascii/bin2.runfiles/_main/js/private/test/image/non_ascii/bin2_/bin2 drwxr-xr-x 0 0 0 0 Jan 1 1970 ./app/js/private/test/image/non_ascii/bin2.runfiles/_main/js/private/test/image/non_ascii/bin2_node_bin/ -r-xr-xr-x 0 0 0 133 Jan 1 1970 ./app/js/private/test/image/non_ascii/bin2.runfiles/_main/js/private/test/image/non_ascii/bin2_node_bin/node -r-xr-xr-x 0 0 0 0 Jan 1 1970 ./app/js/private/test/image/non_ascii/bin2.runfiles/_main/js/private/test/image/non_ascii/empty empty.ㄴㅅ diff --git a/js/private/test/image/regex_edge_cases_test_app.listing b/js/private/test/image/regex_edge_cases_test_app.listing index db8326f49e..8b16fb2d7b 100644 --- a/js/private/test/image/regex_edge_cases_test_app.listing +++ b/js/private/test/image/regex_edge_cases_test_app.listing @@ -3,7 +3,7 @@ drwxr-xr-x 0 0 0 0 Jan 1 1970 ./app/js/ drwxr-xr-x 0 0 0 0 Jan 1 1970 ./app/js/private/ drwxr-xr-x 0 0 0 0 Jan 1 1970 ./app/js/private/test/ drwxr-xr-x 0 0 0 0 Jan 1 1970 ./app/js/private/test/image/ --r-xr-xr-x 0 0 0 xxxxx Jan 1 1970 ./app/js/private/test/image/bin +-r-xr-xr-x 0 0 0 25132 Jan 1 1970 ./app/js/private/test/image/bin drwxr-xr-x 0 0 0 0 Jan 1 1970 ./app/js/private/test/image/bin.runfiles/ drwxr-xr-x 0 0 0 0 Jan 1 1970 ./app/js/private/test/image/bin.runfiles/_main/ drwxr-xr-x 0 0 0 0 Jan 1 1970 ./app/js/private/test/image/bin.runfiles/_main/examples/ @@ -15,10 +15,12 @@ drwxr-xr-x 0 0 0 0 Jan 1 1970 ./app/js/private/test/image/bin. -r-xr-xr-x 0 0 0 336 Jan 1 1970 ./app/js/private/test/image/bin.runfiles/_main/examples/npm_package/packages/pkg_d/package.json drwxr-xr-x 0 0 0 0 Jan 1 1970 ./app/js/private/test/image/bin.runfiles/_main/js/ drwxr-xr-x 0 0 0 0 Jan 1 1970 ./app/js/private/test/image/bin.runfiles/_main/js/private/ +drwxr-xr-x 0 0 0 0 Jan 1 1970 ./app/js/private/test/image/bin.runfiles/_main/js/private/fs_patches_native/ +-r-xr-xr-x 0 0 0 xxxxx Jan 1 1970 ./app/js/private/test/image/bin.runfiles/_main/js/private/fs_patches_native/fs_patch_linux.so drwxr-xr-x 0 0 0 0 Jan 1 1970 ./app/js/private/test/image/bin.runfiles/_main/js/private/test/ drwxr-xr-x 0 0 0 0 Jan 1 1970 ./app/js/private/test/image/bin.runfiles/_main/js/private/test/image/ drwxr-xr-x 0 0 0 0 Jan 1 1970 ./app/js/private/test/image/bin.runfiles/_main/js/private/test/image/bin_/ --r-xr-xr-x 0 0 0 xxxxx Jan 1 1970 ./app/js/private/test/image/bin.runfiles/_main/js/private/test/image/bin_/bin +-r-xr-xr-x 0 0 0 25132 Jan 1 1970 ./app/js/private/test/image/bin.runfiles/_main/js/private/test/image/bin_/bin drwxr-xr-x 0 0 0 0 Jan 1 1970 ./app/js/private/test/image/bin.runfiles/_main/js/private/test/image/bin_node_bin/ -r-xr-xr-x 0 0 0 133 Jan 1 1970 ./app/js/private/test/image/bin.runfiles/_main/js/private/test/image/bin_node_bin/node -r-xr-xr-x 0 0 0 20 Jan 1 1970 ./app/js/private/test/image/bin.runfiles/_main/js/private/test/image/main.js diff --git a/js/private/test/snapshots/launcher.sh b/js/private/test/snapshots/launcher.sh index a028d63b4d..38f2944fab 100644 --- a/js/private/test/snapshots/launcher.sh +++ b/js/private/test/snapshots/launcher.sh @@ -474,6 +474,33 @@ if [ -z "${JS_BINARY__FS_PATCH_ROOTS:-}" ]; then fi export JS_BINARY__FS_PATCH_ROOTS +# Configure native fs patch library (LD_PRELOAD on Linux) +# Note: DYLD_INSERT_LIBRARIES on macOS is not used due to arm64/arm64e architecture +# incompatibility on macOS 15+ and SIP restrictions. macOS relies on JS-level patches. +fs_patch_native="js/private/fs_patches_native/fs_patch_linux.so" +if [ "$fs_patch_native" ] && [ "${JS_BINARY__PATCH_NODE_FS:-}" != "0" ]; then + if [ "${JS_BINARY__NO_RUNFILES:-}" ]; then + fs_patch_native_path=$(resolve_execroot_bin_path "$fs_patch_native") + else + fs_patch_native_path="$JS_BINARY__RUNFILES/_main/$fs_patch_native" + fi + + if [ -f "$fs_patch_native_path" ]; then + case "$(uname -s)" in + Linux*) + if [ -z "${LD_PRELOAD:-}" ]; then + export LD_PRELOAD="$fs_patch_native_path" + else + export LD_PRELOAD="$fs_patch_native_path:$LD_PRELOAD" + fi + logf_debug "LD_PRELOAD %s" "$LD_PRELOAD" + ;; + esac + else + logf_warn "native fs patch library not found at %s" "$fs_patch_native_path" + fi +fi + # Enable coverage if requested if [ "${COVERAGE_DIR:-}" ]; then logf_debug "enabling v8 coverage support ${COVERAGE_DIR}"