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

Use seccomp policy to avoid necessary sync operations #44

Merged
merged 2 commits into from
Nov 25, 2020
Merged
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
3 changes: 3 additions & 0 deletions .run-travis-tests.sh
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@
set -eux
export OPAMYES=true

sudo wget https://github.com/opencontainers/runc/releases/download/v1.0.0-rc92/runc.amd64 -O /usr/local/bin/runc
sudo chmod a+x /usr/local/bin/runc

ZFS_LOOP=$(sudo losetup -f)
dd if=/dev/zero of=/tmp/zfs.img bs=100M count=50
sudo losetup -P $ZFS_LOOP /tmp/zfs.img
Expand Down
7 changes: 7 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,13 @@ in the context of some container. The store should therefore be configured so
that other processes on the host (which might have the same IDs by coincidence)
cannot reach them, e.g. by `chmod go-rwx /path/to/store`.

Sync operations can be very slow, especially on btrfs. They're also
unnecessary, since if the computer crashes then we'll just discard the whole
build and start again. If you have runc version `v1.0.0-rc92` or later, you can
pass the `--fast-sync` option, which installs a seccomp filter that skips all
sync syscalls. However, if you attempt to use this with an earlier version of
runc then sync operations will instead fail with `EPERM`.

## The build specification language

The spec files are loosly based on the [Dockerfile][] format.
Expand Down
61 changes: 56 additions & 5 deletions lib/runc_sandbox.ml
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,27 @@ let ( / ) = Filename.concat

type t = {
runc_state_dir : string;
fast_sync : bool;
arches : string list;
}

let get_machine () =
let ch = Unix.open_process_in "uname -m" in
let arch = input_line ch in
match Unix.close_process_in ch with
| Unix.WEXITED 0 -> String.trim arch
| _ -> failwith "Failed to get arch with 'uname -m'"

let get_arches () =
if Sys.unix then (
match get_machine () with
| "x86_64" -> ["SCMP_ARCH_X86_64"; "SCMP_ARCH_X86"; "SCMP_ARCH_X32"]
| "aarch64" -> ["SCMP_ARCH_AARCH64"; "SCMP_ARCH_ARM"]
| _ -> []
Copy link
Member

Choose a reason for hiding this comment

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

could we enumerate this somehow so that it'll fail on an unknown arch? Otherwise we'll run into this when adding riscv-32 in the future. (or could just make a note to remember to update this somewhere when we get around to riscv32)

Copy link
Member

Choose a reason for hiding this comment

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

there is logic in https://github.com/avsm/osrelease/blob/master/lib/osrelease.ml that i could release that does all the arch detection (based on opams), if that helps

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Could do. But when we add a new multi-arch platform then we'll test it and discover the problem immediately anyway.

) else (
[]
)

module Json_config = struct
let mount ?(options=[]) ~ty ~src dst =
`Assoc [
Expand Down Expand Up @@ -51,7 +70,36 @@ module Json_config = struct
*)
]

let make {Config.cwd; argv; hostname; user; env; mounts; network} ~config_dir ~results_dir : Yojson.Safe.t =
let seccomp_syscalls ~fast_sync =
if fast_sync then [
`Assoc [
(* Sync calls are pointless for the builder, because if the computer crashes then we'll
just throw the build dir away and start again. And btrfs sync is really slow.
Based on https://bblank.thinkmo.de/using-seccomp-to-filter-sync-operations.html
Note: requires runc >= v1.0.0-rc92. *)
"names", strings [
"fsync";
"fdatasync";
"msync";
"sync";
"syncfs";
"sync_file_range";
];
"action", `String "SCMP_ACT_ERRNO";
"errnoRet", `Int 0; (* Return error "success" *)
];
] else [
]

let seccomp_policy t =
let fields = [
"defaultAction", `String "SCMP_ACT_ALLOW";
"syscalls", `List (seccomp_syscalls ~fast_sync:t.fast_sync);
] @ (if t.arches = [] then [] else ["architectures", strings t.arches])
in
`Assoc fields

let make {Config.cwd; argv; hostname; user; env; mounts; network} t ~config_dir ~results_dir : Yojson.Safe.t =
let user =
let { Obuilder_spec.uid; gid } = user in
`Assoc [
Expand Down Expand Up @@ -199,7 +247,8 @@ module Json_config = struct
"/proc/irq";
"/proc/sys";
"/proc/sysrq-trigger"
]
];
"seccomp", seccomp_policy t;
];
]
end
Expand All @@ -217,7 +266,7 @@ let copy_to_log ~src ~dst =

let run ~cancelled ?stdin:stdin ~log t config results_dir =
Lwt_io.with_temp_dir ~prefix:"obuilder-runc-" @@ fun tmp ->
let json_config = Json_config.make config ~config_dir:tmp ~results_dir in
let json_config = Json_config.make config ~config_dir:tmp ~results_dir t in
Os.write_file ~path:(tmp / "config.json") (Yojson.Safe.pretty_to_string json_config ^ "\n") >>= fun () ->
Os.write_file ~path:(tmp / "hosts") "127.0.0.1 localhost builder" >>= fun () ->
let id = string_of_int !next_id in
Expand Down Expand Up @@ -253,6 +302,8 @@ let run ~cancelled ?stdin:stdin ~log t config results_dir =
if Lwt.is_sleeping cancelled then Lwt.return (r :> (unit, [`Msg of string | `Cancelled]) result)
else Lwt_result.fail `Cancelled

let create ~runc_state_dir =
let create ?(fast_sync=false) ~runc_state_dir () =
Os.ensure_dir runc_state_dir;
{ runc_state_dir }
let arches = get_arches () in
Log.info (fun f -> f "Architectures for multi-arch system: %a" Fmt.(Dump.list string) arches);
{ runc_state_dir; fast_sync; arches }
8 changes: 7 additions & 1 deletion lib/runc_sandbox.mli
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
include S.SANDBOX

val create : runc_state_dir:string -> t
val create : ?fast_sync:bool -> runc_state_dir:string -> unit -> t
(** [create dir] is a runc sandboxing system that keeps state in [dir].
@param fast_sync Use seccomp to skip all sync syscalls. This is fast (and
safe, since we discard builds after a crash), but requires
runc version 1.0.0-rc92 or later. Note that the runc version
is not the same as the spec version. If "runc --version"
only prints the spec version, then it's too old. *)
17 changes: 12 additions & 5 deletions main.ml
Original file line number Diff line number Diff line change
Expand Up @@ -20,16 +20,16 @@ let log tag msg =
| `Note -> Fmt.pr "%a@." Fmt.(styled (`Fg `Yellow) string) msg
| `Output -> output_string stdout msg; flush stdout

let create_builder spec =
let create_builder ?fast_sync spec =
Obuilder.Store_spec.to_store spec >|= fun (Store ((module Store), store)) ->
let module Builder = Obuilder.Builder(Store)(Sandbox) in
let sandbox = Sandbox.create ~runc_state_dir:(Store.state_dir store / "runc") in
let sandbox = Sandbox.create ~runc_state_dir:(Store.state_dir store / "runc") ?fast_sync () in
let builder = Builder.v ~store ~sandbox in
Builder ((module Builder), builder)

let build store spec src_dir =
let build fast_sync store spec src_dir =
Lwt_main.run begin
create_builder store >>= fun (Builder ((module Builder), builder)) ->
create_builder ~fast_sync store >>= fun (Builder ((module Builder), builder)) ->
let spec = Obuilder.Spec.stage_of_sexp (Sexplib.Sexp.load_sexp spec) in
let context = Obuilder.Context.v ~log ~src_dir () in
Builder.build builder context spec >>= function
Expand Down Expand Up @@ -94,9 +94,16 @@ let id =
~docv:"ID"
[]

let fast_sync =
Arg.value @@
Arg.flag @@
Arg.info
~doc:"Ignore sync syscalls (requires runc >= 1.0.0-rc92)"
["fast-sync"]

let build =
let doc = "Build a spec file." in
Term.(const build $ store $ spec_file $ src_dir),
Term.(const build $ fast_sync $ store $ spec_file $ src_dir),
Term.info "build" ~doc

let delete =
Expand Down
4 changes: 2 additions & 2 deletions stress/stress.ml
Original file line number Diff line number Diff line change
Expand Up @@ -155,7 +155,7 @@ module Test(Store : S.STORE) = struct
| Error `Cancelled -> assert false

let stress_builds store =
let sandbox = Sandbox.create ~runc_state_dir:(Store.state_dir store / "runc") in
let sandbox = Sandbox.create ~runc_state_dir:(Store.state_dir store / "runc") ~fast_sync:true () in
let builder = Build.v ~store ~sandbox in
let pending = ref n_jobs in
let running = ref 0 in
Expand Down Expand Up @@ -194,7 +194,7 @@ module Test(Store : S.STORE) = struct
else Lwt.return_unit

let prune store =
let sandbox = Sandbox.create ~runc_state_dir:(Store.state_dir store / "runc") in
let sandbox = Sandbox.create ~runc_state_dir:(Store.state_dir store / "runc") () in
let builder = Build.v ~store ~sandbox in
let log id = Logs.info (fun f -> f "Deleting %S" id) in
let end_time = Unix.(gettimeofday () +. 60.0 |> gmtime) in
Expand Down