diff --git a/README.md b/README.md index d21318a98..ad90b5db5 100644 --- a/README.md +++ b/README.md @@ -838,6 +838,25 @@ A program that operates on the current directory will probably want to use `cwd` whereas a program that accepts a path from the user will probably want to use `fs`, perhaps with `open_dir` to constrain all access to be within that directory. +On systems that provide the [cap_enter][] system call, you can ask the OS to reject accesses +that don't use capabilities. +[examples/capsicum/](./examples/capsicum/) contains an example that +restricts itself to using a directory passed on the command-line, and then +tries reading `/etc/passwd` via the stdlib. +Running on FreeBSD, you should see: + +``` +mkdir /tmp/cap +dune exec -- ./examples/capsicum/main.exe /tmp/cap ++Opened directory ++Capsicum mode enabled ++Using the file-system via the directory resource works: ++Writing ... ++Read: "A test file" ++Bypassing Eio and accessing other resources should fail in Capsicum mode: +Fatal error: exception Sys_error("/etc/passwd: Not permitted in capability mode") +``` + ## Running processes Spawning a child process can be done using the [Eio.Process][] module: @@ -1810,3 +1829,4 @@ Some background about the effects system can be found in: [Dev meetings]: https://docs.google.com/document/d/1ZBfbjAkvEkv9ldumpZV5VXrEc_HpPeYjHPW_TiwJe4Q [Olly]: https://github.com/tarides/runtime_events_tools [eio-trace]: https://github.com/ocaml-multicore/eio-trace +[cap_enter]: https://man.freebsd.org/cgi/man.cgi?query=cap_enter diff --git a/examples/capsicum/dune b/examples/capsicum/dune new file mode 100644 index 000000000..fb95515c7 --- /dev/null +++ b/examples/capsicum/dune @@ -0,0 +1,3 @@ +(executable + (name main) + (libraries eio_main)) diff --git a/examples/capsicum/main.ml b/examples/capsicum/main.ml new file mode 100644 index 000000000..f6fe71f1f --- /dev/null +++ b/examples/capsicum/main.ml @@ -0,0 +1,40 @@ +open Eio.Std + +let ( / ) = Eio.Path.( / ) + +let test_eio dir = + traceln "Using the file-system via the directory resource works:"; + let test_file = dir / "capsicum-test.txt" in + traceln "Writing %a..." Eio.Path.pp test_file; + Eio.Path.save test_file "A test file" ~create:(`Exclusive 0o644); + traceln "Read: %S" (Eio.Path.load test_file); + Eio.Path.unlink test_file + +let test_legacy () = + traceln "Bypassing Eio and accessing other resources should fail in Capsicum mode:"; + let ch = open_in "/etc/passwd" in + let len = in_channel_length ch in + let data = really_input_string ch len in + close_in ch; + traceln "Was able to read /etc/passwd:@.%s" (String.trim data) + +let () = + Eio_main.run @@ fun env -> + (* Parse command-line arguments *) + let path = + match Sys.argv with + | [| _; dir |] -> Eio.Stdenv.fs env / dir + | _ -> failwith "Usage: main.exe DIR" + in + if not (Eio.Path.is_directory path) then Fmt.failwith "%a is not a directory" Eio.Path.pp path; + (* Get access to resources before calling cap_enter: *) + Eio.Path.with_open_dir path @@ fun dir -> + traceln "Opened directory %a" Eio.Path.pp path; + (* Switch to capability mode, if possible: *) + begin match Eio_unix.Cap.enter () with + | Ok () -> traceln "Capsicum mode enabled" + | Error `Not_supported -> traceln "!! CAPSICUM PROTECTION NOT AVAILABLE !!" + end; + (* Run tests: *) + test_eio dir; + test_legacy () diff --git a/lib_eio/unix/cap.c b/lib_eio/unix/cap.c new file mode 100644 index 000000000..cae6b6468 --- /dev/null +++ b/lib_eio/unix/cap.c @@ -0,0 +1,27 @@ +#include "primitives.h" + +#include +#include + +#ifdef __FreeBSD__ +# define HAVE_CAPSICUM +#endif + +#ifdef HAVE_CAPSICUM +# include +#endif + +#include +#include + +CAMLprim value eio_unix_cap_enter(value v_unit) { +#ifdef HAVE_CAPSICUM + int r = cap_enter(); + if (r == -1 && errno != ENOSYS) + caml_uerror("cap_enter", Nothing); + + return Val_bool(r == 0); +#else + return Val_bool(0); +#endif +} diff --git a/lib_eio/unix/cap.ml b/lib_eio/unix/cap.ml new file mode 100644 index 000000000..fbb7841f0 --- /dev/null +++ b/lib_eio/unix/cap.ml @@ -0,0 +1,5 @@ +external eio_cap_enter : unit -> bool = "eio_unix_cap_enter" + +let enter () = + if eio_cap_enter () then Ok () + else Error `Not_supported diff --git a/lib_eio/unix/cap.mli b/lib_eio/unix/cap.mli new file mode 100644 index 000000000..fd256c437 --- /dev/null +++ b/lib_eio/unix/cap.mli @@ -0,0 +1,11 @@ +val enter : unit -> (unit, [`Not_supported]) result +(** Call {{:https://man.freebsd.org/cgi/man.cgi?query=cap_enter}cap_enter}. + + Once in capability mode, access to global name spaces, such as file system + or IPC name spaces, is prevented by the operating system. A program can call + this after opening any directories, files or network sockets that it will need, + to prevent accidental access to other resources. + + The standard environment directories {!Eio.Stdenv.fs} and {!Eio.Stdenv.cwd} cannot + be used after calling this, but directories opened from them via {!Eio.Path.with_open_dir} + will continue to work. *) diff --git a/lib_eio/unix/dune b/lib_eio/unix/dune index 7d779efb0..d067e815c 100644 --- a/lib_eio/unix/dune +++ b/lib_eio/unix/dune @@ -4,7 +4,7 @@ (foreign_stubs (language c) (include_dirs include) - (names fork_action stubs)) + (names fork_action stubs cap)) (libraries eio eio.utils unix threads mtime.clock.os)) (rule @@ -15,6 +15,7 @@ (run %{bin:lintcstubs_arity_cmt} %{dep:.eio_unix.objs/byte/eio_unix__Fd.cmt} %{dep:.eio_unix.objs/byte/eio_unix__Private.cmt} + %{dep:.eio_unix.objs/byte/eio_unix__Cap.cmt} %{dep:.eio_unix.objs/byte/eio_unix__Fork_action.cmt})))) (rule diff --git a/lib_eio/unix/eio_unix.ml b/lib_eio/unix/eio_unix.ml index 78bd48037..14f818193 100644 --- a/lib_eio/unix/eio_unix.ml +++ b/lib_eio/unix/eio_unix.ml @@ -28,6 +28,7 @@ module Ipaddr = Net.Ipaddr module Process = Process module Net = Net +module Cap = Cap module Pi = Pi module Stdenv = struct diff --git a/lib_eio/unix/eio_unix.mli b/lib_eio/unix/eio_unix.mli index 10ba6d48a..774ba5f6b 100644 --- a/lib_eio/unix/eio_unix.mli +++ b/lib_eio/unix/eio_unix.mli @@ -66,6 +66,9 @@ val pipe : Switch.t -> source_ty r * sink_ty r module Process = Process (** Spawning child processes with extra control. *) +module Cap = Cap +(** Capsicum security. *) + (** The set of resources provided to a process on a Unix-compatible system. *) module Stdenv : sig type base = < diff --git a/lib_eio/unix/primitives.h b/lib_eio/unix/primitives.h index 9313f98cb..d43166a92 100644 --- a/lib_eio/unix/primitives.h +++ b/lib_eio/unix/primitives.h @@ -7,5 +7,6 @@ CAMLprim value eio_unix_fork_execve(value); CAMLprim value eio_unix_fork_chdir(value); CAMLprim value eio_unix_fork_fchdir(value); CAMLprim value eio_unix_fork_dups(value); +CAMLprim value eio_unix_cap_enter(value); CAMLprim value eio_unix_readlinkat(value, value, value); CAMLprim value eio_unix_is_blocking(value);