Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Freebsdjail sandbox #168

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion dune-project
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@
logs
(cmdliner (>= 1.2.0))
(tar-unix (>= 2.4.0))
(yojson (>= "1.6.0"))
(yojson (>= 1.6.0))
sexplib
ppx_deriving
ppx_sexp_conv
Expand Down
27 changes: 27 additions & 0 deletions lib/archive_extract.ml
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
open Lwt.Infix

let invoke_fetcher base destdir =
Os.with_pipe_between_children @@ fun ~r ~w ->
let stdin = `FD_move_safely r in
let stdout = `FD_move_safely w in
let stderr = stdout in
let fetcher =
Os.exec ~stdout ~stderr ["fetch"; "-q" ; "-o" ; "-" ; base ]
in
let extracter =
Os.sudo ~stdin [ "tar" ; "-C"; destdir ; "-xzpf"; "-" ]
in
fetcher >>= fun () ->
extracter

let fetch ~log ~rootfs base =
let _ = log in
Lwt.catch
(fun () ->
invoke_fetcher base rootfs >>= fun () ->
let env = [] in
Lwt.return env)
(function
| Sys_error s ->
Fmt.failwith "Archive fetcher encountered a system error: %s" s
| e -> Lwt.fail e)
3 changes: 3 additions & 0 deletions lib/archive_extract.mli
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
(** Fetching of base images as .tar.gz archives *)

include S.FETCHER
17 changes: 16 additions & 1 deletion lib/dune
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,25 @@
(enabled_if (= %{system} macosx))
(action (copy %{deps} %{target})))

(rule
(deps sandbox.jail.ml)
(target sandbox.ml)
(enabled_if (= %{system} freebsd))
(action (copy %{deps} %{target})))


(rule
(deps sandbox.dummy.ml)
(target sandbox.ml)
(enabled_if (= %{system} windows))
(action (copy %{deps} %{target})))

(rule
(deps sandbox.runc.ml)
(target sandbox.ml)
(enabled_if (<> %{system} macosx))
(enabled_if (and (<> %{system} macosx)
(<> %{system} windows)
(<> %{system} freebsd)))
(action (copy %{deps} %{target})))

(rule
Expand Down
1 change: 1 addition & 0 deletions lib/obuilder.ml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ module Docker_store = Docker_store
(** {2 Fetchers} *)
module User_temp = User_temp
module Docker_extract = Docker.Extract
module Archive_extract = Archive_extract

(** {2 Sandboxes} *)

Expand Down
5 changes: 5 additions & 0 deletions lib/sandbox.dummy.ml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
type config = unit

let cmdliner = failwith "Sandbox not available"

let create ~state_dir:_ _ = failwith "Sandbox not available"
157 changes: 157 additions & 0 deletions lib/sandbox.jail.ml
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
open Lwt.Infix
open Sexplib.Conv

let ( / ) = Filename.concat

type t = {
lock : Lwt_mutex.t;
jail_name : string;
}

type config = unit [@@deriving sexp]

(* Find out the user name to use within the jail, by parsing the
/etc/passwd file within the jail filesystem. This is roughly
equivalent to what Unix.getpwuid would do.
Note that the gid is currently ignored. *)
let jail_username rootdir config =
match config.Config.user with
| `Windows w -> w.name
| `Unix { uid; _ } ->
let pwdfile = rootdir / "etc" / "passwd" in
let uidstr = string_of_int uid in
let rec parse_line ch =
match In_channel.input_line ch with
| None -> None
| Some line ->
let fields = String.split_on_char ':' line in begin
match fields with
| name :: _pass :: uid :: _ ->
if uid = uidstr then Some name else parse_line ch
| _ -> parse_line ch
end
in
match In_channel.with_open_text pwdfile parse_line with
| None -> Fmt.failwith "No user found for uid %d" uid
| Some name -> name

(* Compute the complete set of arguments passed to the jail(8) command:
jail username, jail path, command to run, etc. *)
let jail_options config rootdir =
let username = jail_username rootdir config in
let commandline =
let env = List.rev_map (fun (k, v) -> k ^ "=" ^ v) config.env in
let commandline =
match config.argv with
| _shell :: "-c" :: args ->
(* This overrides the default shell set in build.ml, since bash is
not installed by default (and would be in /usr/local/bin anyway) *)
[ "/bin/sh" ; "-c" ] @ List.rev (List.rev_map Filename.quote args)
| args -> List.rev (List.rev_map Filename.quote args)
in
let commandline =
match env with
| [] -> commandline
| _ -> "env" :: List.rev_append env commandline
in
let commandline =
String.concat " "
([ "cd" ; Filename.quote config.cwd ; "&&" ] @ commandline)
in
(* Ask for a login shell in order to properly source opam settings. *)
[ "command=/usr/bin/su" ; "-l" ; username ; "-c" ; commandline ]
in
let path = "path=" ^ rootdir in
let devfs_setup = "mount.devfs" in
let options =
let options = [ path ; devfs_setup ] in
match config.network with
| [ "host" ] ->
"ip4=inherit" :: "ip6=inherit" :: "host=inherit" :: options
| _ -> options
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we restrict the jail networking to only access itself here?
The runc implementation overwrites hosts with 127.0.0.1 localhost builder https://github.com/ocurrent/obuilder/blob/master/lib/sandbox.runc.ml#L283

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, one can specify a list of allowed IP addresses in a jail.

Are the allowed values for the "network" stanzas documented somewhere? The example.spec file uses network host to run apt-get update, shouldn't this command be allowed to access network beyond localhost?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the network stanza in OBuilder matches the run --network stanza in Dockerfiles. Is that right? If so, I will update PR#156 to behave the same way.

in
List.rev_append options commandline

let copy_to_log ~src ~dst =
let buf = Bytes.create 4096 in
let rec aux () =
Lwt_unix.read src buf 0 (Bytes.length buf) >>= function
| 0 -> Lwt.return_unit
| n -> Build_log.write dst (Bytes.sub_string buf 0 n) >>= aux
in
aux ()

let run ~cancelled ?stdin:stdin ~log (t : t) config rootdir =
let cwd = rootdir in
Lwt_mutex.with_lock t.lock (fun () ->
Os.with_pipe_from_child @@ fun ~r:out_r ~w:out_w ->
let rootdir = rootdir / "rootfs" in
let workdir = rootdir / config.Config.cwd in
(* Make sure the work directory exists prior to starting the jail. *)
begin
match Os.check_dir workdir with
| `Present -> Lwt.return_unit
| `Missing -> Os.sudo [ "mkdir" ; "-p" ; workdir ]
end >>= fun () ->
let stdout = `FD_move_safely out_w in
let stderr = stdout in
let copy_log = copy_to_log ~src:out_r ~dst:log in
let proc =
let cmd =
let options = jail_options config rootdir in
"jail" :: "-c" :: t.jail_name :: options
in
let stdin = Option.map (fun x -> `FD_move_safely x) stdin in
let pp f = Os.pp_cmd f ("", cmd) in
(* This is similar to
Os.sudo_result ~cwd ?stdin ~stdout ~stderr ~pp cmd
but also unmounting the in-jail devfs if necessary, see below. *)
let cmd = if Os.running_as_root then cmd else "sudo" :: "--" :: cmd in
Logs.info (fun f -> f "Exec %a" Os.pp_cmd ("", cmd));
!Os.lwt_process_exec ~cwd ?stdin ~stdout ~stderr ~pp
("", Array.of_list cmd) >>= function
| Ok 0 ->
(* If the command within the jail completes, the jail is automatically
removed, but without performing any of the stop and release actions,
thus we can not use "exec.stop" to unmount the in-jail devfs
filesystem. Do this here, ignoring the exit code of umount(8). *)
let cmd = [ "sudo" ; "/sbin/umount" ; rootdir / "dev" ] in
Os.exec ~is_success:(fun _ -> true) cmd >>= fun () ->
Lwt_result.ok Lwt.return_unit
| Ok n -> Lwt.return @@ Fmt.error_msg "%t failed with exit status %d" pp n
| Error e -> Lwt_result.fail e
in
Lwt.on_termination cancelled (fun () ->
let rec aux () =
if Lwt.is_sleeping proc then (
let pp f = Fmt.pf f "jail -r %s" t.jail_name in
Os.sudo_result ~cwd [ "jail" ; "-r" ; t.jail_name ] ~pp >>= function
| Ok () -> Lwt.return_unit
| Error (`Msg _) ->
Lwt_unix.sleep 10.0 >>= aux
) else Lwt.return_unit (* Process has already finished *)
in
Lwt.async aux
);
proc >>= fun r ->
copy_log >>= fun () ->
if Lwt.is_sleeping cancelled then
Lwt.return (r :> (unit, [`Msg of string | `Cancelled]) result)
else
Lwt_result.fail `Cancelled)

let create ~state_dir:_ _c =
Lwt.return {
lock = Lwt_mutex.create ();
(* Compute a unique (accross obuilder instances) name for the jail.
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
(* Compute a unique (accross obuilder instances) name for the jail.
(* Compute a unique (across obuilder instances) name for the jail.

Due to the above mutex, only one jail may be started by a given
obuilder process, so appending the obuilder pid is enough to
guarantee uniqueness. *)
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This needs to handle multiple jails concurrently from the ocluster-worker this will need a different approach to generating the jail name and removing the mutex.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There have been changes in the original PR to address this. Can you rebase your PR above mine and give this a new try? (let me know when you want me to squash the commits in my PR)

jail_name = "name=obuilder_" ^ (Int.to_string (Unix.getpid ()));
}

open Cmdliner

(* FIXME Find out how to make this simpler - no special arguments needed here *)
let cmdliner : config Term.t =
Term.(const ())
3 changes: 3 additions & 0 deletions lib/zfs_store.ml
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,9 @@ module Zfs = struct
| `And_snapshots -> ["-r"; "-f"]
| `And_snapshots_and_clones -> ["-R"; "-f"]
in
(* At least under FreeBSD, pass the -f option to make sure any dangling
devfs mount within the filesystem gets automatically unmounted. *)
let opts = "-f" :: opts in
Os.sudo (["zfs"; "destroy"] @ opts @ ["--"; Dataset.full_name t ds])

let destroy_snapshot t ds snapshot mode =
Expand Down
1 change: 1 addition & 0 deletions main.ml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ module Native_sandbox = Obuilder.Native_sandbox
module Docker_sandbox = Obuilder.Docker_sandbox
module Docker_store = Obuilder.Docker_store
module Docker_extract = Obuilder.Docker_extract
module Archive_extract = Obuilder.Archive_extract
module Store_spec = Obuilder.Store_spec

type builder = Builder : (module Obuilder.BUILDER with type t = 'a) * 'a -> builder
Expand Down