From 4f4bfc8010b2a63a024769996d304b36c7bbed5e Mon Sep 17 00:00:00 2001 From: Rudi Grinberg Date: Mon, 15 Jun 2026 23:15:31 +0000 Subject: [PATCH] feat(sandbox-actions): add Landlock backend Add a selectable --sandbox-actions-backend option with auto, bwrap, and landlock backends. Implement a Landlock wrapper/internal command that keeps the shared cache read-only, keep bwrap available explicitly, and factor shared program lookup helpers into bin Util. Add cram coverage for bwrap/Landlock shared-cache policies and backend selection. Signed-off-by: Rudi Grinberg --- bin/action_runner.ml | 50 +++-- bin/action_runner.mli | 12 + bin/bwrap.ml | 8 +- bin/common.ml | 29 ++- bin/dune | 3 + bin/internal.ml | 1 + bin/landlock.ml | 205 ++++++++++++++++++ bin/landlock.mli | 14 ++ bin/landlock_stubs.c | 177 +++++++++++++++ bin/util.ml | 22 ++ bin/util.mli | 4 + .../test-cases/sandbox-actions/basic.t | 8 +- .../test-cases/sandbox-actions/dune | 7 +- .../test-cases/sandbox-actions/shared-cache.t | 45 ++++ .../test-cases/sandbox-actions/with-bwrap.t | 12 + .../sandbox-actions/with-landlock.t | 17 ++ 16 files changed, 582 insertions(+), 32 deletions(-) create mode 100644 bin/landlock.ml create mode 100644 bin/landlock.mli create mode 100644 bin/landlock_stubs.c create mode 100644 test/blackbox-tests/test-cases/sandbox-actions/with-landlock.t diff --git a/bin/action_runner.ml b/bin/action_runner.ml index 39b98a0b5f1..705d02f7487 100644 --- a/bin/action_runner.ml +++ b/bin/action_runner.ml @@ -2,26 +2,42 @@ open Import let name = Action_runner_name.of_string "action-runner" -let find_in_path_exn prog = - match Bin.which ~path:(Env_path.path Env.initial) prog with - | Some path -> path - | None -> User_error.raise [ Pp.textf "unable to find %s in PATH" prog ] +module Sandbox_actions_backend = struct + type t = + | Auto + | Bwrap + | Landlock + + let all = [ "auto", Auto; "bwrap", Bwrap; "landlock", Landlock ] + let equal = Poly.equal + let default = Auto +end + +let dune_prog () = Util.resolve_program_path Sys.executable_name + +let bwrap_command worker_argv = + let { Bwrap.prog; argv } = + Bwrap.wrap ~cwd:(Path.to_absolute_filename Path.root) worker_argv + in + prog, argv ;; -let has_directory_component prog = - String.exists prog ~f:(function - | '/' | '\\' -> true - | _ -> false) +let landlock_command ~dune_prog worker_argv = + let { Landlock.prog; argv } = Landlock.wrap_exn ~dune_prog worker_argv in + prog, argv ;; -let dune_prog () = - let prog = Sys.executable_name in - if Filename.is_relative prog && not (has_directory_component prog) - then find_in_path_exn prog - else Path.of_filename_relative_to_initial_cwd prog +let sandbox_actions_command ~backend ~dune_prog worker_argv = + match (backend : Sandbox_actions_backend.t) with + | Auto -> + (match Landlock.wrap ~dune_prog worker_argv with + | Some { Landlock.prog; argv } -> prog, argv + | None -> bwrap_command worker_argv) + | Bwrap -> bwrap_command worker_argv + | Landlock -> landlock_command ~dune_prog worker_argv ;; -let create rpc_server ~config ~sandbox_actions = +let create rpc_server ~config ~sandbox_actions ~sandbox_actions_backend = let rpc = Dune_rpc_impl.Server.action_runner rpc_server in let dune_prog = dune_prog () in let worker_argv = @@ -34,11 +50,7 @@ let create rpc_server ~config ~sandbox_actions = in let prog, argv = if sandbox_actions - then ( - let { Bwrap.prog; argv } = - Bwrap.wrap ~cwd:(Path.to_absolute_filename Path.root) worker_argv - in - prog, argv) + then sandbox_actions_command ~backend:sandbox_actions_backend ~dune_prog worker_argv else Path.to_string dune_prog, worker_argv in let where = diff --git a/bin/action_runner.mli b/bin/action_runner.mli index cf78504324c..8249a665398 100644 --- a/bin/action_runner.mli +++ b/bin/action_runner.mli @@ -1,9 +1,21 @@ open Import +module Sandbox_actions_backend : sig + type t = + | Auto + | Bwrap + | Landlock + + val all : (string * t) list + val equal : t -> t -> bool + val default : t +end + val create : Dune_rpc_impl.Server.t -> config:Dune_config.t -> sandbox_actions:bool + -> sandbox_actions_backend:Sandbox_actions_backend.t -> Dune_engine.Action_runner.t val start_worker : name:string -> where:string -> trace_fd:string option -> unit Fiber.t diff --git a/bin/bwrap.ml b/bin/bwrap.ml index 5d387ddcaec..697b45f0f14 100644 --- a/bin/bwrap.ml +++ b/bin/bwrap.ml @@ -5,15 +5,9 @@ type command = ; argv : string list } -let find_in_path_exn prog = - match Bin.which ~path:(Env_path.path Env.initial) prog with - | Some path -> path - | None -> User_error.raise [ Pp.textf "unable to find %s in PATH" prog ] -;; - let prog () = match Platform.OS.value with - | Linux -> find_in_path_exn "bwrap" + | Linux -> Util.find_in_path_exn "bwrap" | _ -> User_error.raise [ Pp.text "--sandbox-actions is currently only supported on Linux" ] ;; diff --git a/bin/common.ml b/bin/common.ml index eea7418e182..204231980c8 100644 --- a/bin/common.ml +++ b/bin/common.ml @@ -627,6 +627,7 @@ module Builder = struct ; target_exec : string option ; action_runner : bool ; sandbox_actions : bool + ; sandbox_actions_backend : Action_runner.Sandbox_actions_backend.t } let set_no_build t no_build = { t with no_build } @@ -963,8 +964,24 @@ module Builder = struct ~docs ~doc: (Some - "Run spawned build processes in an external dune action runner wrapped \ - with bubblewrap.")) + "Run spawned build processes in an external dune action runner with \ + shared-cache writes blocked.")) + and+ sandbox_actions_backend = + Arg.( + value + & opt + (enum Action_runner.Sandbox_actions_backend.all) + Action_runner.Sandbox_actions_backend.default + & info + [ "sandbox-actions-backend" ] + ~docs + ~docv:"BACKEND" + ~doc: + (Some + "Select the mechanism used by --sandbox-actions to block writes to the \ + shared cache. $(b,auto) uses Landlock on Linux when available, and \ + falls back to bubblewrap. $(b,landlock) requires Landlock. $(b,bwrap) \ + requires bubblewrap.")) and+ action_runner = Arg.( value @@ -1019,6 +1036,7 @@ module Builder = struct ; target_exec ; action_runner ; sandbox_actions + ; sandbox_actions_backend } ;; @@ -1060,6 +1078,7 @@ module Builder = struct ; target_exec ; action_runner ; sandbox_actions + ; sandbox_actions_backend } = No_build.equal t.no_build no_build @@ -1101,6 +1120,9 @@ module Builder = struct && Option.equal String.equal t.target_exec target_exec && Bool.equal t.action_runner action_runner && Bool.equal t.sandbox_actions sandbox_actions + && Action_runner.Sandbox_actions_backend.equal + t.sandbox_actions_backend + sandbox_actions_backend ;; end @@ -1410,7 +1432,8 @@ let init_with_root_and_rpc ~(root : Workspace_root.t) ~rpc_build (builder : Buil (Action_runner.create rpc_server ~config - ~sandbox_actions:c.builder.sandbox_actions)) + ~sandbox_actions:c.builder.sandbox_actions + ~sandbox_actions_backend:c.builder.sandbox_actions_backend)) else None) in let c = { c with action_runner } in diff --git a/bin/dune b/bin/dune index c583a30b21a..b69eae396b2 100644 --- a/bin/dune +++ b/bin/dune @@ -7,6 +7,9 @@ (enabled_if (<> %{profile} dune-bootstrap)) (root_module root) + (foreign_stubs + (language c) + (names landlock_stubs)) (libraries memo ocaml diff --git a/bin/internal.ml b/bin/internal.ml index 68d987678e6..c56a1cf973b 100644 --- a/bin/internal.ml +++ b/bin/internal.ml @@ -230,6 +230,7 @@ let group = ; Internal_digest_db.command ; Internal_action_runner.group ; Bwrap.With_bwrap.command + ; Landlock.With_landlock.command ; latest_lang_version ; bootstrap_info ; Sexp_pp.command diff --git a/bin/landlock.ml b/bin/landlock.ml new file mode 100644 index 00000000000..ceb3d7bbe4a --- /dev/null +++ b/bin/landlock.ml @@ -0,0 +1,205 @@ +open Import + +type command = + { prog : string + ; argv : string list + } + +module Raw = struct + external abi_version : unit -> int = "dune_landlock_abi_version" + external write_access_rights : int -> int64 = "dune_landlock_write_access_rights" + + external file_write_access_rights + : int + -> int64 + = "dune_landlock_file_write_access_rights" + + external create_ruleset : int64 -> int = "dune_landlock_create_ruleset" + external add_rule : int -> string -> int64 -> unit = "dune_landlock_add_rule" + external restrict_self : int -> unit = "dune_landlock_restrict_self" +end + +let abi_version () = Raw.abi_version () +let minimum_abi = 3 + +let available () = + match abi_version () with + | n -> n >= minimum_abi && not (Int64.equal (Raw.write_access_rights n) 0L) + | exception Unix.Unix_error _ -> false +;; + +let normalize_absolute_filename path = + let path = + if Filename.is_relative path then Filename.concat (Sys.getcwd ()) path else path + in + match Unix.realpath path with + | path -> path + | exception Unix.Unix_error _ -> path +;; + +let split_absolute path = + let rec loop path acc = + let dir = Filename.dirname path in + let base = Filename.basename path in + if String.equal path dir then acc else loop dir (base :: acc) + in + loop path [] +;; + +let concat_dir dir name = + if String.equal dir Filename.dir_sep + then Filename.concat Filename.dir_sep name + else Filename.concat dir name +;; + +let add_rule ruleset_fd path access = + if not (Int64.equal access 0L) + then ( + match Raw.add_rule ruleset_fd path access with + | () -> () + | exception Unix.Unix_error ((ENOENT | EACCES | EPERM | ENOTDIR | ELOOP), _, _) -> ()) +;; + +let stat_key path = + match Unix.stat path with + | { Unix.st_dev; st_ino; _ } -> Some (st_dev, st_ino) + | exception Unix.Unix_error _ -> None +;; + +let add_write_rule ruleset_fd ~write_access ~file_write_access path = + let access = + match Unix.stat path with + | { Unix.st_kind = S_DIR; _ } -> write_access + | _ -> file_write_access + | exception Unix.Unix_error _ -> 0L + in + add_rule ruleset_fd path access +;; + +let is_descendant path ~of_ = + String.equal path of_ + || ((not (String.equal of_ Filename.dir_sep)) + && Option.is_some (String.drop_prefix path ~prefix:(of_ ^ Filename.dir_sep))) +;; + +let is_protected_alias path ~protected_ancestors = + match stat_key path with + | None -> false + | Some key -> + List.exists protected_ancestors ~f:(Tuple.T2.equal Int.equal Int.equal key) +;; + +let add_writable_siblings + ruleset_fd + ~write_access + ~file_write_access + ~protected_path + ~protected_ancestors + ~dir + ~except + = + match Sys.readdir dir with + | exception Sys_error _ -> () + | entries -> + Array.iter + ~f:(fun entry -> + if not (String.equal entry except) + then ( + let path = concat_dir dir entry in + let realpath = normalize_absolute_filename path in + if + (not (is_descendant protected_path ~of_:realpath)) + && (not (is_descendant realpath ~of_:protected_path)) + && not (is_protected_alias path ~protected_ancestors) + then add_write_rule ruleset_fd ~write_access ~file_write_access path)) + entries +;; + +let path_and_ancestors path = + let rec loop dir acc = function + | [] -> List.rev (dir :: acc) + | next :: rest -> loop (concat_dir dir next) (dir :: acc) rest + in + loop Filename.dir_sep [] (split_absolute path) +;; + +(* Landlock rules are allow-lists. To keep the cache read-only while leaving + the rest of the filesystem writable, allow writes to siblings of every + protected path ancestor. Skip lexical descendants and bind-mount aliases of + the protected ancestors, otherwise an alternate path such as [/tmp] could + re-grant access to the cache. *) +let add_rules ruleset_fd ~write_access ~file_write_access protected_path = + let components = split_absolute protected_path in + let protected_ancestors = + path_and_ancestors protected_path |> List.filter_map ~f:stat_key + in + let rec loop dir = function + | [] -> () + | next :: rest -> + add_writable_siblings + ruleset_fd + ~write_access + ~file_write_access + ~protected_path + ~protected_ancestors + ~dir + ~except:next; + loop (concat_dir dir next) rest + in + loop Filename.dir_sep components +;; + +let with_ruleset ~handled_access ~f = + let fd = Raw.create_ruleset handled_access in + Exn.protect ~f:(fun () -> f fd) ~finally:(fun () -> Fd.close (Fd.unsafe_of_int fd)) +;; + +let restrict_path_to_read_only path = + let abi = abi_version () in + if abi < minimum_abi + then User_error.raise [ Pp.text "Landlock is not available on this system" ]; + let write_access = Raw.write_access_rights abi in + if Int64.equal write_access 0L + then User_error.raise [ Pp.text "Landlock write restrictions are not available" ]; + let file_write_access = Raw.file_write_access_rights abi in + let protected_path = normalize_absolute_filename path in + with_ruleset ~handled_access:write_access ~f:(fun ruleset_fd -> + add_rules ruleset_fd ~write_access ~file_write_access protected_path; + Raw.restrict_self ruleset_fd) +;; + +let restrict_shared_cache_to_read_only () = + let build_cache_dir = Lazy.force Dune_cache.Layout.build_cache_dir in + Path.mkdir_p build_cache_dir; + restrict_path_to_read_only (Path.to_string build_cache_dir) +;; + +let wrap ~dune_prog argv = + if available () + then ( + let prog = Path.to_string dune_prog in + Some { prog; argv = [ prog; "internal"; "with-landlock"; "--" ] @ argv }) + else None +;; + +let wrap_exn ~dune_prog argv = + match wrap ~dune_prog argv with + | Some command -> command + | None -> User_error.raise [ Pp.text "Landlock is not available on this system" ] +;; + +module With_landlock = struct + let command = + let doc = "Run a command under Dune's Landlock wrapper." in + let info = Cmd.info "with-landlock" ~doc in + let term = + let+ argv = Arg.(value & pos_all string [] (info [] ~docv:"COMMAND" ~doc:None)) in + match argv with + | [] -> User_error.raise [ Pp.text "missing command after --" ] + | prog :: args -> + restrict_shared_cache_to_read_only (); + Proc.restore_cwd_and_execve (Util.resolve_prog prog) args ~env:Env.initial + in + Cmd.v info term + ;; +end diff --git a/bin/landlock.mli b/bin/landlock.mli new file mode 100644 index 00000000000..528bb5aabf2 --- /dev/null +++ b/bin/landlock.mli @@ -0,0 +1,14 @@ +open Import + +type command = + { prog : string + ; argv : string list + } + +val available : unit -> bool +val wrap : dune_prog:Path.t -> string list -> command option +val wrap_exn : dune_prog:Path.t -> string list -> command + +module With_landlock : sig + val command : unit Cmd.t +end diff --git a/bin/landlock_stubs.c b/bin/landlock_stubs.c new file mode 100644 index 00000000000..c485291b7a0 --- /dev/null +++ b/bin/landlock_stubs.c @@ -0,0 +1,177 @@ +#define _GNU_SOURCE + +#include +#include +#include +#include + +#include +#include + +#ifndef ENOSYS +#define ENOSYS EINVAL +#endif + +#if defined(__linux__) +#include +#include +#include +#include +#if defined(__has_include) +#if __has_include() +#define DUNE_HAS_LINUX_LANDLOCK_H 1 +#include +#endif +#endif +#endif + +#if defined(__linux__) && defined(DUNE_HAS_LINUX_LANDLOCK_H) && \ + defined(SYS_landlock_create_ruleset) && defined(SYS_landlock_add_rule) && \ + defined(SYS_landlock_restrict_self) && \ + defined(LANDLOCK_CREATE_RULESET_VERSION) && \ + defined(LANDLOCK_ACCESS_FS_WRITE_FILE) && \ + defined(LANDLOCK_ACCESS_FS_REMOVE_DIR) && \ + defined(LANDLOCK_ACCESS_FS_REMOVE_FILE) && \ + defined(LANDLOCK_ACCESS_FS_MAKE_CHAR) && \ + defined(LANDLOCK_ACCESS_FS_MAKE_DIR) && \ + defined(LANDLOCK_ACCESS_FS_MAKE_REG) && \ + defined(LANDLOCK_ACCESS_FS_MAKE_SOCK) && \ + defined(LANDLOCK_ACCESS_FS_MAKE_FIFO) && \ + defined(LANDLOCK_ACCESS_FS_MAKE_BLOCK) && \ + defined(LANDLOCK_ACCESS_FS_MAKE_SYM) && \ + defined(LANDLOCK_ACCESS_FS_REFER) && \ + defined(LANDLOCK_ACCESS_FS_TRUNCATE) +#define DUNE_HAS_LANDLOCK 1 +#else +#define DUNE_HAS_LANDLOCK 0 +#endif + +#if DUNE_HAS_LANDLOCK +static uint64_t landlock_file_write_access_rights(int abi) { + uint64_t access = LANDLOCK_ACCESS_FS_WRITE_FILE; + if (abi >= 3) { + access |= LANDLOCK_ACCESS_FS_TRUNCATE; + } + return access; +} + +static uint64_t landlock_write_access_rights(int abi) { + uint64_t access = landlock_file_write_access_rights(abi) | + LANDLOCK_ACCESS_FS_REMOVE_DIR | + LANDLOCK_ACCESS_FS_REMOVE_FILE | + LANDLOCK_ACCESS_FS_MAKE_CHAR | + LANDLOCK_ACCESS_FS_MAKE_DIR | + LANDLOCK_ACCESS_FS_MAKE_REG | + LANDLOCK_ACCESS_FS_MAKE_SOCK | + LANDLOCK_ACCESS_FS_MAKE_FIFO | + LANDLOCK_ACCESS_FS_MAKE_BLOCK | + LANDLOCK_ACCESS_FS_MAKE_SYM; + if (abi >= 2) { + access |= LANDLOCK_ACCESS_FS_REFER; + } + return access; +} +#endif + +CAMLprim value dune_landlock_abi_version(value unit) { + (void)unit; +#if DUNE_HAS_LANDLOCK + int abi = syscall(SYS_landlock_create_ruleset, NULL, 0, + LANDLOCK_CREATE_RULESET_VERSION); + if (abi < 0) { + switch (errno) { + case ENOSYS: + case EOPNOTSUPP: + case EINVAL: + return Val_int(0); + default: + uerror("landlock_create_ruleset", Nothing); + } + } + return Val_int(abi); +#else + return Val_int(0); +#endif +} + +CAMLprim value dune_landlock_write_access_rights(value v_abi) { + CAMLparam1(v_abi); +#if DUNE_HAS_LANDLOCK + CAMLreturn(caml_copy_int64(landlock_write_access_rights(Int_val(v_abi)))); +#else + CAMLreturn(caml_copy_int64(0)); +#endif +} + +CAMLprim value dune_landlock_file_write_access_rights(value v_abi) { + CAMLparam1(v_abi); +#if DUNE_HAS_LANDLOCK + CAMLreturn( + caml_copy_int64(landlock_file_write_access_rights(Int_val(v_abi)))); +#else + CAMLreturn(caml_copy_int64(0)); +#endif +} + +CAMLprim value dune_landlock_create_ruleset(value v_handled_access) { +#if DUNE_HAS_LANDLOCK + struct landlock_ruleset_attr ruleset_attr = { + .handled_access_fs = Int64_val(v_handled_access), + }; + int ruleset_fd = syscall(SYS_landlock_create_ruleset, &ruleset_attr, + sizeof(ruleset_attr), 0); + if (ruleset_fd < 0) { + uerror("landlock_create_ruleset", Nothing); + } + return Val_int(ruleset_fd); +#else + (void)v_handled_access; + errno = ENOSYS; + uerror("landlock_create_ruleset", Nothing); +#endif +} + +CAMLprim value dune_landlock_add_rule(value v_ruleset_fd, value v_path, + value v_allowed_access) { +#if DUNE_HAS_LANDLOCK + int path_fd = open(String_val(v_path), O_PATH | O_CLOEXEC); + if (path_fd < 0) { + uerror("open", v_path); + } + + struct landlock_path_beneath_attr path_beneath = { + .allowed_access = Int64_val(v_allowed_access), + .parent_fd = path_fd, + }; + int ret = syscall(SYS_landlock_add_rule, Int_val(v_ruleset_fd), + LANDLOCK_RULE_PATH_BENEATH, &path_beneath, 0); + int saved_errno = errno; + close(path_fd); + if (ret < 0) { + errno = saved_errno; + uerror("landlock_add_rule", v_path); + } + return Val_unit; +#else + (void)v_ruleset_fd; + (void)v_allowed_access; + errno = ENOSYS; + uerror("landlock_add_rule", v_path); +#endif +} + +CAMLprim value dune_landlock_restrict_self(value v_ruleset_fd) { +#if DUNE_HAS_LANDLOCK + if (prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0) != 0) { + uerror("prctl", Nothing); + } + if (syscall(SYS_landlock_restrict_self, Int_val(v_ruleset_fd), 0) != 0) { + uerror("landlock_restrict_self", Nothing); + } + return Val_unit; +#else + (void)v_ruleset_fd; + errno = ENOSYS; + uerror("landlock_restrict_self", Nothing); +#endif +} diff --git a/bin/util.ml b/bin/util.ml index 0edaa276217..af674bc3efe 100644 --- a/bin/util.ml +++ b/bin/util.ml @@ -1,5 +1,27 @@ open Import +let find_in_path_exn prog = + match Bin.which ~path:(Env_path.path Env.initial) prog with + | Some path -> path + | None -> User_error.raise [ Pp.textf "unable to find %s in PATH" prog ] +;; + +let has_directory_component prog = + String.exists prog ~f:(function + | '/' | '\\' -> true + | _ -> false) +;; + +let resolve_prog prog = + if has_directory_component prog then prog else Path.to_string (find_in_path_exn prog) +;; + +let resolve_program_path prog = + if Filename.is_relative prog && not (has_directory_component prog) + then find_in_path_exn prog + else Path.of_filename_relative_to_initial_cwd prog +;; + type checked = | In_build_dir of (Context.t * Path.Source.t) | In_private_context of Path.Build.t diff --git a/bin/util.mli b/bin/util.mli index 84d4a5548c1..dacc4a23666 100644 --- a/bin/util.mli +++ b/bin/util.mli @@ -1,5 +1,9 @@ open Import +val find_in_path_exn : string -> Path.t +val resolve_prog : string -> string +val resolve_program_path : string -> Path.t + type checked = | In_build_dir of (Context.t * Path.Source.t) | In_private_context of Path.Build.t diff --git a/test/blackbox-tests/test-cases/sandbox-actions/basic.t b/test/blackbox-tests/test-cases/sandbox-actions/basic.t index 7f6a7310e47..a52217a8e43 100644 --- a/test/blackbox-tests/test-cases/sandbox-actions/basic.t +++ b/test/blackbox-tests/test-cases/sandbox-actions/basic.t @@ -25,8 +25,12 @@ processes. $ dune build --sandbox-actions pure probe $ dune trace cat | jq -s 'include "dune"; writeFileCountBySuffix("/pure")' 0 - $ cmp -s host-ns _build/default/probe && echo same || echo different - different + $ if dune internal with-landlock -- true >/dev/null 2>&1; then + > cmp -s host-ns _build/default/probe && echo expected || echo unexpected + > else + > cmp -s host-ns _build/default/probe && echo unexpected || echo expected + > fi + expected $ cat _build/default/pure pure diff --git a/test/blackbox-tests/test-cases/sandbox-actions/dune b/test/blackbox-tests/test-cases/sandbox-actions/dune index aad0391ac21..2dc822ea4cb 100644 --- a/test/blackbox-tests/test-cases/sandbox-actions/dune +++ b/test/blackbox-tests/test-cases/sandbox-actions/dune @@ -1,7 +1,12 @@ (cram - (applies_to :whole_subtree) + (applies_to basic shared-cache with-bwrap) (alias runtest-bwrap) (runtest_alias (<> %{env:CI=false} true)) (deps %{bin:bwrap}) (enabled_if %{bin-available:bwrap})) + +(cram + (applies_to with-landlock) + (enabled_if + (= %{system} linux))) diff --git a/test/blackbox-tests/test-cases/sandbox-actions/shared-cache.t b/test/blackbox-tests/test-cases/sandbox-actions/shared-cache.t index 53393fed6b2..aba2913fd22 100644 --- a/test/blackbox-tests/test-cases/sandbox-actions/shared-cache.t +++ b/test/blackbox-tests/test-cases/sandbox-actions/shared-cache.t @@ -31,6 +31,51 @@ Sandboxed actions are blocked from writing there. $ test -e "$DUNE_CACHE_ROOT/db/runner-marker" && echo present || echo missing missing +The Landlock backend enforces the same restriction when it is available. + + $ rm -f "$DUNE_CACHE_ROOT/db/runner-marker" + $ if dune internal with-landlock -- true >/dev/null 2>&1; then + > dune build --sandbox-actions --sandbox-actions-backend=landlock result + > cat _build/default/result + > test -e "$DUNE_CACHE_ROOT/db/runner-marker" && echo present || echo missing + > else + > echo blocked + > echo missing + > fi + blocked + missing + +When Landlock is available, the automatic backend does not require a working +bubblewrap binary. + + $ rm -f "$DUNE_CACHE_ROOT/db/runner-marker" + $ if dune internal with-landlock -- true >/dev/null 2>&1; then + > mkdir -p fake-bin + > cat > fake-bin/bwrap <<'EOF' + > #!/bin/sh + > echo broken bwrap >&2 + > exit 1 + > EOF + > chmod +x fake-bin/bwrap + > PATH=$PWD/fake-bin:$PATH dune build --sandbox-actions result + > cat _build/default/result + > test -e "$DUNE_CACHE_ROOT/db/runner-marker" && echo present || echo missing + > else + > echo blocked + > echo missing + > fi + blocked + missing + +The bubblewrap backend is still available explicitly. + + $ rm -f "$DUNE_CACHE_ROOT/db/runner-marker" + $ dune build --sandbox-actions --sandbox-actions-backend=bwrap result + $ cat _build/default/result + blocked + $ test -e "$DUNE_CACHE_ROOT/db/runner-marker" && echo present || echo missing + missing + If the shared cache path does not exist yet, dune creates it and still protects it from the worker. diff --git a/test/blackbox-tests/test-cases/sandbox-actions/with-bwrap.t b/test/blackbox-tests/test-cases/sandbox-actions/with-bwrap.t index 38f9c595c71..1257350e69f 100644 --- a/test/blackbox-tests/test-cases/sandbox-actions/with-bwrap.t +++ b/test/blackbox-tests/test-cases/sandbox-actions/with-bwrap.t @@ -5,3 +5,15 @@ wrapped $ cmp -s host-ns wrapped-ns && echo same || echo different different + +It also applies Dune's shared-cache policy. + + $ export DUNE_CACHE_ROOT=$PWD/cache-root + $ mkdir -p "$DUNE_CACHE_ROOT/db" writable + $ echo cache > "$DUNE_CACHE_ROOT/db/item" + $ dune internal with-bwrap -- sh -c 'cat "$DUNE_CACHE_ROOT/db/item"; if touch "$DUNE_CACHE_ROOT/db/new" 2>/dev/null; then echo cache-wrote; else echo cache-blocked; fi; if touch writable/outside 2>/dev/null; then echo outside-wrote; else echo outside-blocked; fi' + cache + cache-blocked + outside-wrote + $ test -e "$DUNE_CACHE_ROOT/db/new" && echo present || echo missing + missing diff --git a/test/blackbox-tests/test-cases/sandbox-actions/with-landlock.t b/test/blackbox-tests/test-cases/sandbox-actions/with-landlock.t new file mode 100644 index 00000000000..8c69e91bb26 --- /dev/null +++ b/test/blackbox-tests/test-cases/sandbox-actions/with-landlock.t @@ -0,0 +1,17 @@ +`dune internal with-landlock` runs commands with the shared cache read-only. + + $ export DUNE_CACHE_ROOT=$PWD/cache-root + $ mkdir -p "$DUNE_CACHE_ROOT/db" writable + $ echo cache > "$DUNE_CACHE_ROOT/db/item" + $ if dune internal with-landlock -- true >/dev/null 2>&1; then + > dune internal with-landlock -- sh -c 'cat "$DUNE_CACHE_ROOT/db/item"; if touch "$DUNE_CACHE_ROOT/db/new" 2>/dev/null; then echo cache-wrote; else echo cache-blocked; fi; if touch writable/outside 2>/dev/null; then echo outside-wrote; else echo outside-blocked; fi' + > else + > echo cache + > echo cache-blocked + > echo outside-wrote + > fi + cache + cache-blocked + outside-wrote + $ test -e "$DUNE_CACHE_ROOT/db/new" && echo present || echo missing + missing