Build deterministic OCI sandbox specifications
This commit is contained in:
parent
bf8a3de567
commit
cf5948e8be
6 changed files with 1422 additions and 0 deletions
251
Cargo.lock
generated
251
Cargo.lock
generated
|
|
@ -2,6 +2,15 @@
|
||||||
# It is not intended for manual editing.
|
# It is not intended for manual editing.
|
||||||
version = 4
|
version = 4
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "aho-corasick"
|
||||||
|
version = "1.1.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301"
|
||||||
|
dependencies = [
|
||||||
|
"memchr",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "bitflags"
|
name = "bitflags"
|
||||||
version = "2.13.0"
|
version = "2.13.0"
|
||||||
|
|
@ -38,6 +47,27 @@ version = "0.10.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "a6ef517f0926dd24a1582492c791b6a4818a4d94e789a334894aa15b0d12f55c"
|
checksum = "a6ef517f0926dd24a1582492c791b6a4818a4d94e789a334894aa15b0d12f55c"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "const_format"
|
||||||
|
version = "0.2.36"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "4481a617ad9a412be3b97c5d403fef8ed023103368908b9c50af598ff467cc1e"
|
||||||
|
dependencies = [
|
||||||
|
"const_format_proc_macros",
|
||||||
|
"konst",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "const_format_proc_macros"
|
||||||
|
version = "0.2.34"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1d57c2eccfb16dbac1f4e61e206105db5820c9d26c3c472bc17c774259ef7744"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"unicode-xid",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cpufeatures"
|
name = "cpufeatures"
|
||||||
version = "0.3.0"
|
version = "0.3.0"
|
||||||
|
|
@ -56,6 +86,72 @@ dependencies = [
|
||||||
"hybrid-array",
|
"hybrid-array",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "darling"
|
||||||
|
version = "0.20.11"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee"
|
||||||
|
dependencies = [
|
||||||
|
"darling_core",
|
||||||
|
"darling_macro",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "darling_core"
|
||||||
|
version = "0.20.11"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "0d00b9596d185e565c2207a0b01f8bd1a135483d02d9b7b0a54b11da8d53412e"
|
||||||
|
dependencies = [
|
||||||
|
"fnv",
|
||||||
|
"ident_case",
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"strsim",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "darling_macro"
|
||||||
|
version = "0.20.11"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead"
|
||||||
|
dependencies = [
|
||||||
|
"darling_core",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "derive_builder"
|
||||||
|
version = "0.20.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "507dfb09ea8b7fa618fcf76e953f4f5e192547945816d5358edffe39f6f94947"
|
||||||
|
dependencies = [
|
||||||
|
"derive_builder_macro",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "derive_builder_core"
|
||||||
|
version = "0.20.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "2d5bcf7b024d6835cfb3d473887cd966994907effbe9227e8c8219824d06c4e8"
|
||||||
|
dependencies = [
|
||||||
|
"darling",
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "derive_builder_macro"
|
||||||
|
version = "0.20.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ab63b0e2bf4d5928aff72e83a7dace85d7bba5fe12dcc3c5a572d78caffd3f3c"
|
||||||
|
dependencies = [
|
||||||
|
"derive_builder_core",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "digest"
|
name = "digest"
|
||||||
version = "0.11.3"
|
version = "0.11.3"
|
||||||
|
|
@ -101,6 +197,12 @@ version = "2.4.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6"
|
checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "fnv"
|
||||||
|
version = "1.0.7"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "getrandom"
|
name = "getrandom"
|
||||||
version = "0.4.3"
|
version = "0.4.3"
|
||||||
|
|
@ -112,12 +214,29 @@ dependencies = [
|
||||||
"r-efi",
|
"r-efi",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "getset"
|
||||||
|
version = "0.1.7"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "6cf442baaabe4213ce7d1239afc26c039180b6456da2cededa316ae2c8a77a77"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "hashbrown"
|
name = "hashbrown"
|
||||||
version = "0.17.1"
|
version = "0.17.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a"
|
checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "heck"
|
||||||
|
version = "0.5.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "hybrid-array"
|
name = "hybrid-array"
|
||||||
version = "0.4.13"
|
version = "0.4.13"
|
||||||
|
|
@ -127,6 +246,12 @@ dependencies = [
|
||||||
"typenum",
|
"typenum",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ident_case"
|
||||||
|
version = "1.0.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "indexmap"
|
name = "indexmap"
|
||||||
version = "2.14.0"
|
version = "2.14.0"
|
||||||
|
|
@ -137,6 +262,27 @@ dependencies = [
|
||||||
"hashbrown",
|
"hashbrown",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "itoa"
|
||||||
|
version = "1.0.18"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "konst"
|
||||||
|
version = "0.2.20"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "128133ed7824fcd73d6e7b17957c5eb7bacb885649bd8c69708b2331a10bcefb"
|
||||||
|
dependencies = [
|
||||||
|
"konst_macro_rules",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "konst_macro_rules"
|
||||||
|
version = "0.2.19"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a4933f3f57a8e9d9da04db23fb153356ecaf00cbd14aee46279c33dc80925c37"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "lazaret-client"
|
name = "lazaret-client"
|
||||||
version = "0.0.0"
|
version = "0.0.0"
|
||||||
|
|
@ -193,6 +339,10 @@ name = "lazaret-runtime-supervisor"
|
||||||
version = "0.0.0"
|
version = "0.0.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"lazaret-core",
|
"lazaret-core",
|
||||||
|
"lazaret-linux",
|
||||||
|
"oci-spec",
|
||||||
|
"serde_json",
|
||||||
|
"tempfile",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -219,6 +369,29 @@ version = "0.12.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53"
|
checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "memchr"
|
||||||
|
version = "2.8.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "oci-spec"
|
||||||
|
version = "0.10.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "3df6f876ad774d6a676f7e968f5c3edacc32f90e65fe680a8b686235396556fb"
|
||||||
|
dependencies = [
|
||||||
|
"const_format",
|
||||||
|
"derive_builder",
|
||||||
|
"getset",
|
||||||
|
"regex",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
"strum",
|
||||||
|
"strum_macros",
|
||||||
|
"thiserror",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "once_cell"
|
name = "once_cell"
|
||||||
version = "1.21.4"
|
version = "1.21.4"
|
||||||
|
|
@ -270,6 +443,35 @@ dependencies = [
|
||||||
"libc",
|
"libc",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "regex"
|
||||||
|
version = "1.13.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "2a0e75113e14dc5acb068cd0786884f214f1312650a3d36d269f5c4f3cdee8a2"
|
||||||
|
dependencies = [
|
||||||
|
"aho-corasick",
|
||||||
|
"memchr",
|
||||||
|
"regex-automata",
|
||||||
|
"regex-syntax",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "regex-automata"
|
||||||
|
version = "0.4.15"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1f388202e4b80542a0921078cc23b6333bcf1409c1e3f86404cae4766a6131db"
|
||||||
|
dependencies = [
|
||||||
|
"aho-corasick",
|
||||||
|
"memchr",
|
||||||
|
"regex-syntax",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "regex-syntax"
|
||||||
|
version = "0.8.11"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustix"
|
name = "rustix"
|
||||||
version = "1.1.4"
|
version = "1.1.4"
|
||||||
|
|
@ -313,6 +515,19 @@ dependencies = [
|
||||||
"syn",
|
"syn",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "serde_json"
|
||||||
|
version = "1.0.150"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9"
|
||||||
|
dependencies = [
|
||||||
|
"itoa",
|
||||||
|
"memchr",
|
||||||
|
"serde",
|
||||||
|
"serde_core",
|
||||||
|
"zmij",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "serde_spanned"
|
name = "serde_spanned"
|
||||||
version = "1.1.1"
|
version = "1.1.1"
|
||||||
|
|
@ -339,6 +554,30 @@ version = "1.1.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"
|
checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "strsim"
|
||||||
|
version = "0.11.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "strum"
|
||||||
|
version = "0.27.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "af23d6f6c1a224baef9d3f61e287d2761385a5b88fdab4eb4c6f11aeb54c4bcf"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "strum_macros"
|
||||||
|
version = "0.27.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7695ce3845ea4b33927c055a39dc438a45b059f7c1b3d91d38d10355fb8cbca7"
|
||||||
|
dependencies = [
|
||||||
|
"heck",
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "syn"
|
name = "syn"
|
||||||
version = "2.0.118"
|
version = "2.0.118"
|
||||||
|
|
@ -434,6 +673,12 @@ version = "1.0.24"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
|
checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "unicode-xid"
|
||||||
|
version = "0.2.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "windows-link"
|
name = "windows-link"
|
||||||
version = "0.2.1"
|
version = "0.2.1"
|
||||||
|
|
@ -454,3 +699,9 @@ name = "winnow"
|
||||||
version = "1.0.4"
|
version = "1.0.4"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "23b97319f7b8343df12cc98938e5c3eb436064524c8d2b4e30a1d3a36eecdf81"
|
checksum = "23b97319f7b8343df12cc98938e5c3eb436064524c8d2b4e30a1d3a36eecdf81"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "zmij"
|
||||||
|
version = "1.0.23"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b"
|
||||||
|
|
|
||||||
|
|
@ -18,9 +18,11 @@ rust-version = "1.90"
|
||||||
license = "Apache-2.0"
|
license = "Apache-2.0"
|
||||||
|
|
||||||
[workspace.dependencies]
|
[workspace.dependencies]
|
||||||
|
oci-spec = { version = "=0.10.0", default-features = false, features = ["runtime"] }
|
||||||
postcard = { version = "=1.1.3", default-features = false, features = ["use-std"] }
|
postcard = { version = "=1.1.3", default-features = false, features = ["use-std"] }
|
||||||
redb = "=4.1.0"
|
redb = "=4.1.0"
|
||||||
serde = { version = "=1.0.228", features = ["derive"] }
|
serde = { version = "=1.0.228", features = ["derive"] }
|
||||||
|
serde_json = "=1.0.150"
|
||||||
rustix = { version = "=1.1.4", features = ["event", "fs", "net", "pipe", "process", "thread"] }
|
rustix = { version = "=1.1.4", features = ["event", "fs", "net", "pipe", "process", "thread"] }
|
||||||
sha2 = "=0.11.0"
|
sha2 = "=0.11.0"
|
||||||
toml = { version = "=1.1.3", default-features = false, features = ["parse", "serde", "std"] }
|
toml = { version = "=1.1.3", default-features = false, features = ["parse", "serde", "std"] }
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,12 @@ path = "src/main.rs"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
lazaret-core = { version = "=0.0.0", path = "../lazaret-core" }
|
lazaret-core = { version = "=0.0.0", path = "../lazaret-core" }
|
||||||
|
lazaret-linux = { version = "=0.0.0", path = "../lazaret-linux" }
|
||||||
|
oci-spec.workspace = true
|
||||||
|
serde_json.workspace = true
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
tempfile = "=3.27.0"
|
||||||
|
|
||||||
[lints]
|
[lints]
|
||||||
workspace = true
|
workspace = true
|
||||||
|
|
|
||||||
3
crates/lazaret-runtime-supervisor/src/lib.rs
Normal file
3
crates/lazaret-runtime-supervisor/src/lib.rs
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
#![doc = "Runtime construction interfaces owned by the Lazaret Runtime Supervisor."]
|
||||||
|
|
||||||
|
pub mod sandbox;
|
||||||
727
crates/lazaret-runtime-supervisor/src/sandbox.rs
Normal file
727
crates/lazaret-runtime-supervisor/src/sandbox.rs
Normal file
|
|
@ -0,0 +1,727 @@
|
||||||
|
use std::collections::BTreeMap;
|
||||||
|
use std::ffi::{OsStr, OsString};
|
||||||
|
use std::fmt;
|
||||||
|
use std::fs::Metadata;
|
||||||
|
use std::os::unix::ffi::{OsStrExt, OsStringExt};
|
||||||
|
use std::os::unix::fs::MetadataExt;
|
||||||
|
use std::path::{Component, Path, PathBuf};
|
||||||
|
|
||||||
|
use lazaret_core::configuration::{ResolvedDomainConfiguration, SandboxLayoutV1};
|
||||||
|
use lazaret_core::identity::{ContainerId, NamespaceIdentity};
|
||||||
|
use lazaret_core::records::VolumeAccess;
|
||||||
|
use oci_spec::runtime::{
|
||||||
|
Capabilities, LinuxBuilder, LinuxCapabilitiesBuilder, LinuxIdMappingBuilder,
|
||||||
|
LinuxNamespaceBuilder, LinuxNamespaceType, Mount, MountBuilder, ProcessBuilder, RootBuilder,
|
||||||
|
Spec, SpecBuilder, UserBuilder,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const WORKSPACE_DESTINATION: &str = "/workspace";
|
||||||
|
pub const CONTROL_ROOT_DESTINATION: &str = "/run/lazaret";
|
||||||
|
pub const CREDENTIALS_DESTINATION: &str = "/run/lazaret/credentials";
|
||||||
|
pub const MAILBOX_REQUESTS_DESTINATION: &str = "/run/lazaret/mailbox/requests";
|
||||||
|
pub const MAILBOX_RESULTS_DESTINATION: &str = "/run/lazaret/mailbox/results";
|
||||||
|
pub const ESCALATION_CLIENT_DESTINATION: &str = "/run/lazaret/bin/lazaret-escalate";
|
||||||
|
pub const TERMINFO_ROOT_DESTINATION: &str = "/run/lazaret/terminfo";
|
||||||
|
pub const NETWORK_NAMESPACE_PATH: &str = "/proc/self/ns/net";
|
||||||
|
|
||||||
|
const OCI_VERSION: &str = "1.2.1";
|
||||||
|
const DEFAULT_PATH: &str = "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin";
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||||
|
pub enum SandboxBuildErrorKind {
|
||||||
|
InvalidProcess,
|
||||||
|
InvalidPath,
|
||||||
|
InvalidTerminal,
|
||||||
|
ChangedAuthority,
|
||||||
|
NetworkNamespaceMismatch,
|
||||||
|
OciSpecification,
|
||||||
|
JsonEncoding,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct SandboxBuildError {
|
||||||
|
kind: SandboxBuildErrorKind,
|
||||||
|
detail: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SandboxBuildError {
|
||||||
|
fn new(kind: SandboxBuildErrorKind, detail: impl Into<String>) -> Self {
|
||||||
|
Self {
|
||||||
|
kind,
|
||||||
|
detail: detail.into(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub const fn kind(&self) -> SandboxBuildErrorKind {
|
||||||
|
self.kind
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for SandboxBuildError {
|
||||||
|
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
write!(formatter, "{}: {}", error_label(self.kind), self.detail)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::error::Error for SandboxBuildError {}
|
||||||
|
|
||||||
|
const fn error_label(kind: SandboxBuildErrorKind) -> &'static str {
|
||||||
|
match kind {
|
||||||
|
SandboxBuildErrorKind::InvalidProcess => "invalid root process",
|
||||||
|
SandboxBuildErrorKind::InvalidPath => "invalid sandbox path",
|
||||||
|
SandboxBuildErrorKind::InvalidTerminal => "invalid host terminal",
|
||||||
|
SandboxBuildErrorKind::ChangedAuthority => "changed host authority",
|
||||||
|
SandboxBuildErrorKind::NetworkNamespaceMismatch => "network namespace mismatch",
|
||||||
|
SandboxBuildErrorKind::OciSpecification => "invalid OCI specification",
|
||||||
|
SandboxBuildErrorKind::JsonEncoding => "OCI JSON encoding failed",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||||
|
pub struct RootProcess {
|
||||||
|
arguments: Vec<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RootProcess {
|
||||||
|
/// Creates a direct argument vector for the terminal-neutral root process.
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
/// The program must be an absolute normalized sandbox path and no argument may contain NUL.
|
||||||
|
pub fn new(
|
||||||
|
program: impl Into<String>,
|
||||||
|
arguments: impl IntoIterator<Item = String>,
|
||||||
|
) -> Result<Self, SandboxBuildError> {
|
||||||
|
let program = program.into();
|
||||||
|
validate_sandbox_executable(&program)?;
|
||||||
|
let mut complete = vec![program];
|
||||||
|
for argument in arguments {
|
||||||
|
if argument.as_bytes().contains(&0) {
|
||||||
|
return Err(SandboxBuildError::new(
|
||||||
|
SandboxBuildErrorKind::InvalidProcess,
|
||||||
|
"argument contains NUL",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
complete.push(argument);
|
||||||
|
}
|
||||||
|
Ok(Self {
|
||||||
|
arguments: complete,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub fn arguments(&self) -> &[String] {
|
||||||
|
&self.arguments
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||||
|
struct FileIdentity {
|
||||||
|
device: u64,
|
||||||
|
inode: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FileIdentity {
|
||||||
|
fn from_metadata(metadata: &Metadata) -> Self {
|
||||||
|
Self {
|
||||||
|
device: metadata.dev(),
|
||||||
|
inode: metadata.ino(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||||
|
pub struct HostTerminal {
|
||||||
|
term: String,
|
||||||
|
color_term: Option<String>,
|
||||||
|
source: PathBuf,
|
||||||
|
identity: FileIdentity,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl HostTerminal {
|
||||||
|
/// Selects one exact compiled host terminfo entry beneath a trusted root.
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
/// Invalid names, inaccessible entries, non-files, and sources outside the roots are refused.
|
||||||
|
pub fn from_trusted_roots(
|
||||||
|
term: impl Into<String>,
|
||||||
|
color_term: Option<String>,
|
||||||
|
source: &Path,
|
||||||
|
trusted_roots: &[PathBuf],
|
||||||
|
) -> Result<Self, SandboxBuildError> {
|
||||||
|
let term = term.into();
|
||||||
|
validate_terminal_name(&term)?;
|
||||||
|
if color_term.as_deref().is_some_and(|value| {
|
||||||
|
value.is_empty() || value.bytes().any(|byte| byte < 0x20 || byte == 0x7f)
|
||||||
|
}) {
|
||||||
|
return Err(SandboxBuildError::new(
|
||||||
|
SandboxBuildErrorKind::InvalidTerminal,
|
||||||
|
"COLORTERM contains control bytes or is empty",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
let source = std::fs::canonicalize(source).map_err(|error| {
|
||||||
|
SandboxBuildError::new(
|
||||||
|
SandboxBuildErrorKind::InvalidTerminal,
|
||||||
|
format!("cannot canonicalize terminfo source: {error}"),
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
if source.file_name() != Some(OsStr::new(&term)) {
|
||||||
|
return Err(SandboxBuildError::new(
|
||||||
|
SandboxBuildErrorKind::InvalidTerminal,
|
||||||
|
"terminfo source name differs from TERM",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
let beneath_trusted_root = trusted_roots.iter().any(|root| {
|
||||||
|
std::fs::canonicalize(root)
|
||||||
|
.ok()
|
||||||
|
.is_some_and(|root| source.starts_with(&root) && source != root)
|
||||||
|
});
|
||||||
|
if !beneath_trusted_root {
|
||||||
|
return Err(SandboxBuildError::new(
|
||||||
|
SandboxBuildErrorKind::InvalidTerminal,
|
||||||
|
"terminfo source is outside every trusted root",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
let metadata = std::fs::metadata(&source).map_err(|error| {
|
||||||
|
SandboxBuildError::new(
|
||||||
|
SandboxBuildErrorKind::InvalidTerminal,
|
||||||
|
format!("cannot inspect terminfo source: {error}"),
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
if !metadata.is_file() {
|
||||||
|
return Err(SandboxBuildError::new(
|
||||||
|
SandboxBuildErrorKind::InvalidTerminal,
|
||||||
|
"terminfo source is not a regular file",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
Ok(Self {
|
||||||
|
term,
|
||||||
|
color_term,
|
||||||
|
source,
|
||||||
|
identity: FileIdentity::from_metadata(&metadata),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn revalidate(&self) -> Result<(), SandboxBuildError> {
|
||||||
|
let metadata = std::fs::metadata(&self.source).map_err(|error| {
|
||||||
|
SandboxBuildError::new(
|
||||||
|
SandboxBuildErrorKind::ChangedAuthority,
|
||||||
|
format!("cannot revalidate terminfo source: {error}"),
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
if !metadata.is_file() || FileIdentity::from_metadata(&metadata) != self.identity {
|
||||||
|
return Err(SandboxBuildError::new(
|
||||||
|
SandboxBuildErrorKind::ChangedAuthority,
|
||||||
|
"terminfo source changed identity",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn destination(&self) -> PathBuf {
|
||||||
|
let bucket = char::from(self.term.as_bytes()[0]).to_string();
|
||||||
|
Path::new(TERMINFO_ROOT_DESTINATION)
|
||||||
|
.join(bucket)
|
||||||
|
.join(&self.term)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct SandboxBuildRequest<'a> {
|
||||||
|
pub configuration: &'a ResolvedDomainConfiguration,
|
||||||
|
pub container_id: ContainerId,
|
||||||
|
pub expected_network_namespace: NamespaceIdentity,
|
||||||
|
pub root_process: RootProcess,
|
||||||
|
pub terminal: Option<HostTerminal>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||||
|
pub struct RunscCommand {
|
||||||
|
program: OsString,
|
||||||
|
arguments: Vec<OsString>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RunscCommand {
|
||||||
|
#[must_use]
|
||||||
|
pub fn program(&self) -> &OsStr {
|
||||||
|
&self.program
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub fn arguments(&self) -> &[OsString] {
|
||||||
|
&self.arguments
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||||
|
pub struct SandboxBundle {
|
||||||
|
specification: Spec,
|
||||||
|
specification_json: Vec<u8>,
|
||||||
|
command: RunscCommand,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SandboxBundle {
|
||||||
|
#[must_use]
|
||||||
|
pub const fn specification(&self) -> &Spec {
|
||||||
|
&self.specification
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub fn specification_json(&self) -> &[u8] {
|
||||||
|
&self.specification_json
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub const fn command(&self) -> &RunscCommand {
|
||||||
|
&self.command
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the complete protected destination roots used by bundle construction.
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
/// Returns an error if the fixed layout violates the configuration layout contract.
|
||||||
|
pub fn production_sandbox_layout() -> Result<SandboxLayoutV1, SandboxBuildError> {
|
||||||
|
SandboxLayoutV1::from_production_layout([
|
||||||
|
WORKSPACE_DESTINATION.to_owned(),
|
||||||
|
CONTROL_ROOT_DESTINATION.to_owned(),
|
||||||
|
"/proc".to_owned(),
|
||||||
|
"/sys".to_owned(),
|
||||||
|
"/dev".to_owned(),
|
||||||
|
])
|
||||||
|
.map_err(|error| {
|
||||||
|
SandboxBuildError::new(
|
||||||
|
SandboxBuildErrorKind::InvalidPath,
|
||||||
|
format!("production layout is invalid: {error}"),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Builds deterministic OCI bytes and the direct attached-runsc argument vector.
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
/// Refuses changed host authority, namespace mismatch, invalid paths, or encoding failures.
|
||||||
|
pub fn build_sandbox_bundle(
|
||||||
|
request: SandboxBuildRequest<'_>,
|
||||||
|
) -> Result<SandboxBundle, SandboxBuildError> {
|
||||||
|
require_network_namespace(request.expected_network_namespace)?;
|
||||||
|
request
|
||||||
|
.configuration
|
||||||
|
.workspace()
|
||||||
|
.open_authority()
|
||||||
|
.map_err(changed_configuration_authority)?;
|
||||||
|
for volume in request.configuration.volumes() {
|
||||||
|
volume
|
||||||
|
.open_source()
|
||||||
|
.map_err(changed_configuration_authority)?;
|
||||||
|
}
|
||||||
|
if let Some(terminal) = &request.terminal {
|
||||||
|
terminal.revalidate()?;
|
||||||
|
}
|
||||||
|
|
||||||
|
let paths = runtime_paths(request.configuration, request.container_id)?;
|
||||||
|
let mounts = build_mounts(
|
||||||
|
request.configuration,
|
||||||
|
request.terminal.as_ref(),
|
||||||
|
&paths.control_sources,
|
||||||
|
)?;
|
||||||
|
let process = build_process(
|
||||||
|
request.configuration,
|
||||||
|
request.root_process,
|
||||||
|
request.terminal.as_ref(),
|
||||||
|
)?;
|
||||||
|
let linux = build_linux()?;
|
||||||
|
let command = build_runsc_command(&paths);
|
||||||
|
let root = RootBuilder::default()
|
||||||
|
.path(paths.rootfs)
|
||||||
|
.readonly(false)
|
||||||
|
.build()
|
||||||
|
.map_err(oci_error)?;
|
||||||
|
let specification = SpecBuilder::default()
|
||||||
|
.version(OCI_VERSION)
|
||||||
|
.root(root)
|
||||||
|
.mounts(mounts)
|
||||||
|
.process(process)
|
||||||
|
.hostname("lazaret")
|
||||||
|
.linux(linux)
|
||||||
|
.build()
|
||||||
|
.map_err(oci_error)?;
|
||||||
|
let specification_json = serde_json::to_vec(&specification).map_err(|error| {
|
||||||
|
SandboxBuildError::new(SandboxBuildErrorKind::JsonEncoding, error.to_string())
|
||||||
|
})?;
|
||||||
|
Ok(SandboxBundle {
|
||||||
|
specification,
|
||||||
|
specification_json,
|
||||||
|
command,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
struct RuntimePaths {
|
||||||
|
container_name: String,
|
||||||
|
bundle: PathBuf,
|
||||||
|
runsc_root: PathBuf,
|
||||||
|
rootfs: PathBuf,
|
||||||
|
control_sources: PathBuf,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn runtime_paths(
|
||||||
|
configuration: &ResolvedDomainConfiguration,
|
||||||
|
container_id: ContainerId,
|
||||||
|
) -> Result<RuntimePaths, SandboxBuildError> {
|
||||||
|
let runtime_domain = host_path(configuration.runtime_domain_root().canonical_label())?;
|
||||||
|
let container_name = hex_lower(container_id.as_bytes());
|
||||||
|
Ok(RuntimePaths {
|
||||||
|
bundle: runtime_domain.join("bundles").join(&container_name),
|
||||||
|
runsc_root: runtime_domain.join("runsc"),
|
||||||
|
rootfs: runtime_domain.join("state-anchor").join("merged"),
|
||||||
|
control_sources: runtime_domain.join("sandbox-control"),
|
||||||
|
container_name,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn build_mounts(
|
||||||
|
configuration: &ResolvedDomainConfiguration,
|
||||||
|
terminal: Option<&HostTerminal>,
|
||||||
|
control_sources: &Path,
|
||||||
|
) -> Result<Vec<Mount>, SandboxBuildError> {
|
||||||
|
let mut mounts = standard_mounts()?;
|
||||||
|
mounts.push(bind_mount(
|
||||||
|
host_path(configuration.workspace().canonical_label())?,
|
||||||
|
PathBuf::from(WORKSPACE_DESTINATION),
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
)?);
|
||||||
|
for volume in configuration.volumes() {
|
||||||
|
let record = volume.record();
|
||||||
|
mounts.push(bind_mount(
|
||||||
|
host_path(&record.canonical_host_source)?,
|
||||||
|
PathBuf::from(&record.container_path),
|
||||||
|
record.access == VolumeAccess::ReadOnly,
|
||||||
|
false,
|
||||||
|
)?);
|
||||||
|
}
|
||||||
|
mounts.extend(control_mounts(control_sources)?);
|
||||||
|
if let Some(terminal) = terminal {
|
||||||
|
mounts.push(bind_mount(
|
||||||
|
terminal.source.clone(),
|
||||||
|
terminal.destination(),
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
)?);
|
||||||
|
}
|
||||||
|
Ok(mounts)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn build_linux() -> Result<oci_spec::runtime::Linux, SandboxBuildError> {
|
||||||
|
let private_namespaces = [
|
||||||
|
LinuxNamespaceType::Pid,
|
||||||
|
LinuxNamespaceType::Ipc,
|
||||||
|
LinuxNamespaceType::Uts,
|
||||||
|
LinuxNamespaceType::Mount,
|
||||||
|
LinuxNamespaceType::User,
|
||||||
|
]
|
||||||
|
.into_iter()
|
||||||
|
.map(|typ| {
|
||||||
|
LinuxNamespaceBuilder::default()
|
||||||
|
.typ(typ)
|
||||||
|
.build()
|
||||||
|
.map_err(oci_error)
|
||||||
|
})
|
||||||
|
.chain(std::iter::once(
|
||||||
|
LinuxNamespaceBuilder::default()
|
||||||
|
.typ(LinuxNamespaceType::Network)
|
||||||
|
.path(PathBuf::from(NETWORK_NAMESPACE_PATH))
|
||||||
|
.build()
|
||||||
|
.map_err(oci_error),
|
||||||
|
))
|
||||||
|
.collect::<Result<Vec<_>, _>>()?;
|
||||||
|
let identity_mapping = LinuxIdMappingBuilder::default()
|
||||||
|
.container_id(0_u32)
|
||||||
|
.host_id(0_u32)
|
||||||
|
.size(1_u32)
|
||||||
|
.build()
|
||||||
|
.map_err(oci_error)?;
|
||||||
|
LinuxBuilder::default()
|
||||||
|
.namespaces(private_namespaces)
|
||||||
|
.uid_mappings(vec![identity_mapping])
|
||||||
|
.gid_mappings(vec![identity_mapping])
|
||||||
|
.rootfs_propagation("rprivate")
|
||||||
|
.build()
|
||||||
|
.map_err(oci_error)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn build_runsc_command(paths: &RuntimePaths) -> RunscCommand {
|
||||||
|
RunscCommand {
|
||||||
|
program: OsString::from("runsc"),
|
||||||
|
arguments: vec![
|
||||||
|
OsString::from(format!(
|
||||||
|
"-root={}",
|
||||||
|
paths
|
||||||
|
.runsc_root
|
||||||
|
.to_str()
|
||||||
|
.expect("validated runtime path remains UTF-8")
|
||||||
|
)),
|
||||||
|
OsString::from("-ignore-cgroups"),
|
||||||
|
OsString::from("run"),
|
||||||
|
OsString::from(format!(
|
||||||
|
"-bundle={}",
|
||||||
|
paths
|
||||||
|
.bundle
|
||||||
|
.to_str()
|
||||||
|
.expect("validated runtime path remains UTF-8")
|
||||||
|
)),
|
||||||
|
OsString::from(&paths.container_name),
|
||||||
|
],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn build_process(
|
||||||
|
configuration: &ResolvedDomainConfiguration,
|
||||||
|
root_process: RootProcess,
|
||||||
|
terminal: Option<&HostTerminal>,
|
||||||
|
) -> Result<oci_spec::runtime::Process, SandboxBuildError> {
|
||||||
|
let mut environment = BTreeMap::from([("PATH".to_owned(), DEFAULT_PATH.to_owned())]);
|
||||||
|
for (name, value) in configuration.ephemeral_environment() {
|
||||||
|
let value = value.expose().to_str().ok_or_else(|| {
|
||||||
|
SandboxBuildError::new(
|
||||||
|
SandboxBuildErrorKind::InvalidProcess,
|
||||||
|
"resolved environment value is not UTF-8",
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
environment.insert(name.clone(), value.to_owned());
|
||||||
|
}
|
||||||
|
if let Some(terminal) = terminal {
|
||||||
|
environment.insert("TERM".to_owned(), terminal.term.clone());
|
||||||
|
if let Some(color_term) = &terminal.color_term {
|
||||||
|
environment.insert("COLORTERM".to_owned(), color_term.clone());
|
||||||
|
} else {
|
||||||
|
environment.remove("COLORTERM");
|
||||||
|
}
|
||||||
|
environment.insert(
|
||||||
|
"TERMINFO_DIRS".to_owned(),
|
||||||
|
format!("{TERMINFO_ROOT_DESTINATION}:"),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
let environment = environment
|
||||||
|
.into_iter()
|
||||||
|
.map(|(name, value)| format!("{name}={value}"))
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
let empty = Capabilities::new();
|
||||||
|
let capabilities = LinuxCapabilitiesBuilder::default()
|
||||||
|
.bounding(empty.clone())
|
||||||
|
.effective(empty.clone())
|
||||||
|
.inheritable(empty.clone())
|
||||||
|
.permitted(empty.clone())
|
||||||
|
.ambient(empty)
|
||||||
|
.build()
|
||||||
|
.map_err(oci_error)?;
|
||||||
|
let user = UserBuilder::default()
|
||||||
|
.uid(0_u32)
|
||||||
|
.gid(0_u32)
|
||||||
|
.umask(0o022_u32)
|
||||||
|
.build()
|
||||||
|
.map_err(oci_error)?;
|
||||||
|
ProcessBuilder::default()
|
||||||
|
.terminal(false)
|
||||||
|
.user(user)
|
||||||
|
.args(root_process.arguments)
|
||||||
|
.env(environment)
|
||||||
|
.cwd(PathBuf::from(WORKSPACE_DESTINATION))
|
||||||
|
.capabilities(capabilities)
|
||||||
|
.no_new_privileges(true)
|
||||||
|
.build()
|
||||||
|
.map_err(oci_error)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn standard_mounts() -> Result<Vec<Mount>, SandboxBuildError> {
|
||||||
|
[
|
||||||
|
("/proc", "proc", "proc", vec!["nosuid", "noexec", "nodev"]),
|
||||||
|
(
|
||||||
|
"/dev",
|
||||||
|
"tmpfs",
|
||||||
|
"tmpfs",
|
||||||
|
vec!["nosuid", "strictatime", "mode=755", "size=65536k"],
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"/dev/pts",
|
||||||
|
"devpts",
|
||||||
|
"devpts",
|
||||||
|
vec![
|
||||||
|
"nosuid",
|
||||||
|
"noexec",
|
||||||
|
"newinstance",
|
||||||
|
"ptmxmode=0666",
|
||||||
|
"mode=0620",
|
||||||
|
],
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"/dev/shm",
|
||||||
|
"tmpfs",
|
||||||
|
"shm",
|
||||||
|
vec!["nosuid", "noexec", "nodev", "mode=1777", "size=65536k"],
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"/dev/mqueue",
|
||||||
|
"mqueue",
|
||||||
|
"mqueue",
|
||||||
|
vec!["nosuid", "noexec", "nodev"],
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"/sys",
|
||||||
|
"sysfs",
|
||||||
|
"sysfs",
|
||||||
|
vec!["nosuid", "noexec", "nodev", "ro"],
|
||||||
|
),
|
||||||
|
]
|
||||||
|
.into_iter()
|
||||||
|
.map(|(destination, typ, source, options)| {
|
||||||
|
MountBuilder::default()
|
||||||
|
.destination(PathBuf::from(destination))
|
||||||
|
.typ(typ)
|
||||||
|
.source(PathBuf::from(source))
|
||||||
|
.options(options.into_iter().map(str::to_owned).collect::<Vec<_>>())
|
||||||
|
.build()
|
||||||
|
.map_err(oci_error)
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn control_mounts(control_sources: &Path) -> Result<Vec<Mount>, SandboxBuildError> {
|
||||||
|
[
|
||||||
|
("credentials", CREDENTIALS_DESTINATION, true, false),
|
||||||
|
(
|
||||||
|
"mailbox/requests",
|
||||||
|
MAILBOX_REQUESTS_DESTINATION,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
),
|
||||||
|
("mailbox/results", MAILBOX_RESULTS_DESTINATION, true, false),
|
||||||
|
(
|
||||||
|
"bin/lazaret-escalate",
|
||||||
|
ESCALATION_CLIENT_DESTINATION,
|
||||||
|
true,
|
||||||
|
false,
|
||||||
|
),
|
||||||
|
]
|
||||||
|
.into_iter()
|
||||||
|
.map(|(source, destination, read_only, no_exec)| {
|
||||||
|
bind_mount(
|
||||||
|
control_sources.join(source),
|
||||||
|
PathBuf::from(destination),
|
||||||
|
read_only,
|
||||||
|
no_exec,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn bind_mount(
|
||||||
|
source: PathBuf,
|
||||||
|
destination: PathBuf,
|
||||||
|
read_only: bool,
|
||||||
|
no_exec: bool,
|
||||||
|
) -> Result<Mount, SandboxBuildError> {
|
||||||
|
validate_absolute_normalized(&source)?;
|
||||||
|
validate_absolute_normalized(&destination)?;
|
||||||
|
let mut options = vec!["bind".to_owned(), "nosuid".to_owned(), "nodev".to_owned()];
|
||||||
|
options.push(if read_only { "ro" } else { "rw" }.to_owned());
|
||||||
|
if no_exec {
|
||||||
|
options.push("noexec".to_owned());
|
||||||
|
}
|
||||||
|
MountBuilder::default()
|
||||||
|
.destination(destination)
|
||||||
|
.typ("bind")
|
||||||
|
.source(source)
|
||||||
|
.options(options)
|
||||||
|
.build()
|
||||||
|
.map_err(oci_error)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn require_network_namespace(expected: NamespaceIdentity) -> Result<(), SandboxBuildError> {
|
||||||
|
let observed = lazaret_linux::filesystem::namespace_identity(Path::new(NETWORK_NAMESPACE_PATH))
|
||||||
|
.map_err(|error| {
|
||||||
|
SandboxBuildError::new(
|
||||||
|
SandboxBuildErrorKind::NetworkNamespaceMismatch,
|
||||||
|
format!("cannot inspect Runtime Supervisor network namespace: {error}"),
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
if observed != expected {
|
||||||
|
return Err(SandboxBuildError::new(
|
||||||
|
SandboxBuildErrorKind::NetworkNamespaceMismatch,
|
||||||
|
"Runtime Supervisor network namespace identity differs from readiness evidence",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn changed_configuration_authority(error: impl fmt::Display) -> SandboxBuildError {
|
||||||
|
SandboxBuildError::new(SandboxBuildErrorKind::ChangedAuthority, error.to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn oci_error(error: impl fmt::Display) -> SandboxBuildError {
|
||||||
|
SandboxBuildError::new(SandboxBuildErrorKind::OciSpecification, error.to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn validate_sandbox_executable(program: &str) -> Result<(), SandboxBuildError> {
|
||||||
|
validate_absolute_normalized(Path::new(program)).map_err(|_| {
|
||||||
|
SandboxBuildError::new(
|
||||||
|
SandboxBuildErrorKind::InvalidProcess,
|
||||||
|
"program is not a normalized absolute sandbox path",
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn validate_terminal_name(term: &str) -> Result<(), SandboxBuildError> {
|
||||||
|
let valid = !term.is_empty()
|
||||||
|
&& term.len() <= 64
|
||||||
|
&& term
|
||||||
|
.bytes()
|
||||||
|
.all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'+' | b'-' | b'.' | b'_'));
|
||||||
|
if valid {
|
||||||
|
Ok(())
|
||||||
|
} else {
|
||||||
|
Err(SandboxBuildError::new(
|
||||||
|
SandboxBuildErrorKind::InvalidTerminal,
|
||||||
|
"TERM violates the conservative terminal-name grammar",
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn validate_absolute_normalized(path: &Path) -> Result<(), SandboxBuildError> {
|
||||||
|
if !path.is_absolute()
|
||||||
|
|| path.as_os_str().as_bytes().contains(&0)
|
||||||
|
|| path
|
||||||
|
.components()
|
||||||
|
.any(|component| !matches!(component, Component::RootDir | Component::Normal(_)))
|
||||||
|
{
|
||||||
|
return Err(SandboxBuildError::new(
|
||||||
|
SandboxBuildErrorKind::InvalidPath,
|
||||||
|
"path is not normalized and absolute",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
utf8_path(path)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn utf8_path(path: &Path) -> Result<&str, SandboxBuildError> {
|
||||||
|
path.to_str().ok_or_else(|| {
|
||||||
|
SandboxBuildError::new(
|
||||||
|
SandboxBuildErrorKind::InvalidPath,
|
||||||
|
"OCI JSON path is not UTF-8",
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn host_path(path: &lazaret_core::identity::HostPath) -> Result<PathBuf, SandboxBuildError> {
|
||||||
|
let path = PathBuf::from(OsString::from_vec(path.as_bytes().to_vec()));
|
||||||
|
validate_absolute_normalized(&path)?;
|
||||||
|
Ok(path)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn hex_lower(bytes: &[u8]) -> String {
|
||||||
|
let mut output = String::with_capacity(bytes.len() * 2);
|
||||||
|
for byte in bytes {
|
||||||
|
use fmt::Write as _;
|
||||||
|
write!(&mut output, "{byte:02x}").expect("writing to String cannot fail");
|
||||||
|
}
|
||||||
|
output
|
||||||
|
}
|
||||||
433
crates/lazaret-runtime-supervisor/tests/sandbox_builder.rs
Normal file
433
crates/lazaret-runtime-supervisor/tests/sandbox_builder.rs
Normal file
|
|
@ -0,0 +1,433 @@
|
||||||
|
use std::collections::{BTreeMap, BTreeSet};
|
||||||
|
use std::ffi::OsStr;
|
||||||
|
use std::fs;
|
||||||
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
|
use lazaret_core::configuration::{
|
||||||
|
ConfigurationLoadRequest, ResolvedDomainConfiguration, load_trusted_configuration,
|
||||||
|
};
|
||||||
|
use lazaret_core::identity::{ContainerId, DeviceId, InodeId, NamespaceIdentity};
|
||||||
|
use lazaret_runtime_supervisor::sandbox::{
|
||||||
|
CREDENTIALS_DESTINATION, ESCALATION_CLIENT_DESTINATION, HostTerminal,
|
||||||
|
MAILBOX_REQUESTS_DESTINATION, MAILBOX_RESULTS_DESTINATION, NETWORK_NAMESPACE_PATH, RootProcess,
|
||||||
|
SandboxBuildErrorKind, SandboxBuildRequest, TERMINFO_ROOT_DESTINATION, WORKSPACE_DESTINATION,
|
||||||
|
build_sandbox_bundle, production_sandbox_layout,
|
||||||
|
};
|
||||||
|
use serde_json::Value;
|
||||||
|
use tempfile::TempDir;
|
||||||
|
|
||||||
|
struct Fixture {
|
||||||
|
tree: TempDir,
|
||||||
|
configuration: ResolvedDomainConfiguration,
|
||||||
|
terminfo_source: PathBuf,
|
||||||
|
terminfo_root: PathBuf,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Fixture {
|
||||||
|
fn new() -> Self {
|
||||||
|
let tree = tempfile::tempdir().unwrap();
|
||||||
|
let configuration_directory = tree.path().join("configuration");
|
||||||
|
let workspace = tree.path().join("workspace");
|
||||||
|
let runtime = tree.path().join("runtime");
|
||||||
|
fs::create_dir(&configuration_directory).unwrap();
|
||||||
|
fs::create_dir(&workspace).unwrap();
|
||||||
|
fs::create_dir(&runtime).unwrap();
|
||||||
|
let configuration_path = configuration_directory.join("lazaret.toml");
|
||||||
|
fs::write(
|
||||||
|
&configuration_path,
|
||||||
|
format!(
|
||||||
|
r#"schema = "lazaret-trusted-host-config-v1"
|
||||||
|
|
||||||
|
[storage]
|
||||||
|
data_root = "{}"
|
||||||
|
|
||||||
|
[profiles.default]
|
||||||
|
image = "example.invalid/agent:v1"
|
||||||
|
persistent_dirs = ["/root"]
|
||||||
|
env = {{ SELECTED_SECRET = "env:SELECTED_HOST_SECRET" }}
|
||||||
|
volumes = [
|
||||||
|
{{ host = "{{profile_dir}}/read-only", container = "/root/read-only", kind = "directory", access = "read-only", create = true }},
|
||||||
|
{{ host = "{{profile_dir}}/read-write", container = "/root/read-write", kind = "directory", access = "read-write", create = true }},
|
||||||
|
]
|
||||||
|
|
||||||
|
[profiles.unselected]
|
||||||
|
image = "example.invalid/unselected:v1"
|
||||||
|
env = {{ UNSELECTED_SECRET = "env:UNSELECTED_HOST_SECRET" }}
|
||||||
|
"#,
|
||||||
|
tree.path().join("data").display()
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
let environment = BTreeMap::from([
|
||||||
|
("SELECTED_HOST_SECRET".to_owned(), "selected-value".into()),
|
||||||
|
("UNSELECTED_HOST_SECRET".to_owned(), "must-not-enter".into()),
|
||||||
|
]);
|
||||||
|
let configuration = load_trusted_configuration(ConfigurationLoadRequest {
|
||||||
|
explicit_configuration_path: Some(configuration_path),
|
||||||
|
selected_profile: "default".to_owned(),
|
||||||
|
workspace,
|
||||||
|
home: tree.path().join("home"),
|
||||||
|
xdg_config_home: None,
|
||||||
|
xdg_data_home: None,
|
||||||
|
xdg_runtime_dir: runtime,
|
||||||
|
environment,
|
||||||
|
sandbox_layout: production_sandbox_layout().unwrap(),
|
||||||
|
})
|
||||||
|
.unwrap();
|
||||||
|
let terminfo_root = tree.path().join("trusted-terminfo");
|
||||||
|
let terminfo_source = terminfo_root.join("x").join("xterm-lazaret");
|
||||||
|
fs::create_dir_all(terminfo_source.parent().unwrap()).unwrap();
|
||||||
|
fs::write(&terminfo_source, b"compiled host terminfo capabilities").unwrap();
|
||||||
|
Self {
|
||||||
|
tree,
|
||||||
|
configuration,
|
||||||
|
terminfo_source,
|
||||||
|
terminfo_root,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn terminal(&self) -> HostTerminal {
|
||||||
|
HostTerminal::from_trusted_roots(
|
||||||
|
"xterm-lazaret",
|
||||||
|
Some("truecolor".to_owned()),
|
||||||
|
&self.terminfo_source,
|
||||||
|
std::slice::from_ref(&self.terminfo_root),
|
||||||
|
)
|
||||||
|
.unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn request(&self, root_process: RootProcess) -> SandboxBuildRequest<'_> {
|
||||||
|
SandboxBuildRequest {
|
||||||
|
configuration: &self.configuration,
|
||||||
|
container_id: ContainerId::new([0x11; 32]),
|
||||||
|
expected_network_namespace: current_network_namespace(),
|
||||||
|
root_process,
|
||||||
|
terminal: Some(self.terminal()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn current_network_namespace() -> NamespaceIdentity {
|
||||||
|
lazaret_linux::filesystem::namespace_identity(Path::new(NETWORK_NAMESPACE_PATH)).unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn root_process(arguments: impl IntoIterator<Item = String>) -> RootProcess {
|
||||||
|
RootProcess::new("/sbin/lazaret-sandbox-init", arguments).unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn value(bundle: &lazaret_runtime_supervisor::sandbox::SandboxBundle) -> Value {
|
||||||
|
serde_json::from_slice(bundle.specification_json()).unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn mounts(specification: &Value) -> BTreeMap<&str, &Value> {
|
||||||
|
specification["mounts"]
|
||||||
|
.as_array()
|
||||||
|
.unwrap()
|
||||||
|
.iter()
|
||||||
|
.map(|mount| (mount["destination"].as_str().unwrap(), mount))
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn options(mount: &Value) -> BTreeSet<&str> {
|
||||||
|
mount["options"]
|
||||||
|
.as_array()
|
||||||
|
.unwrap()
|
||||||
|
.iter()
|
||||||
|
.map(|option| option.as_str().unwrap())
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn deterministic_snapshot_has_only_private_namespaces_and_fixed_runtime_shape() {
|
||||||
|
let fixture = Fixture::new();
|
||||||
|
let arguments = vec!["literal;$(touch /host)".to_owned(), "two words".to_owned()];
|
||||||
|
let first = build_sandbox_bundle(fixture.request(root_process(arguments.clone()))).unwrap();
|
||||||
|
let second = build_sandbox_bundle(fixture.request(root_process(arguments.clone()))).unwrap();
|
||||||
|
assert_eq!(first, second);
|
||||||
|
|
||||||
|
let specification = value(&first);
|
||||||
|
assert_eq!(specification["ociVersion"], "1.2.1");
|
||||||
|
assert_eq!(specification["hostname"], "lazaret");
|
||||||
|
assert_eq!(specification["process"]["terminal"], false);
|
||||||
|
assert_eq!(specification["process"]["noNewPrivileges"], true);
|
||||||
|
assert_eq!(specification["process"]["cwd"], WORKSPACE_DESTINATION);
|
||||||
|
assert_eq!(
|
||||||
|
specification["process"]["args"],
|
||||||
|
serde_json::json!([
|
||||||
|
"/sbin/lazaret-sandbox-init",
|
||||||
|
"literal;$(touch /host)",
|
||||||
|
"two words"
|
||||||
|
])
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
specification["process"]["capabilities"],
|
||||||
|
serde_json::json!({
|
||||||
|
"bounding": [], "effective": [], "inheritable": [], "permitted": [], "ambient": []
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
let namespaces = specification["linux"]["namespaces"].as_array().unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
namespaces
|
||||||
|
.iter()
|
||||||
|
.map(|namespace| namespace["type"].as_str().unwrap())
|
||||||
|
.collect::<Vec<_>>(),
|
||||||
|
["pid", "ipc", "uts", "mount", "user", "network"]
|
||||||
|
);
|
||||||
|
for namespace in &namespaces[..5] {
|
||||||
|
assert!(namespace.get("path").is_none());
|
||||||
|
}
|
||||||
|
assert_eq!(namespaces[5]["path"], NETWORK_NAMESPACE_PATH);
|
||||||
|
assert_eq!(
|
||||||
|
specification["linux"]["uidMappings"],
|
||||||
|
serde_json::json!([{ "hostID": 0, "containerID": 0, "size": 1 }])
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
specification["linux"]["gidMappings"],
|
||||||
|
serde_json::json!([{ "hostID": 0, "containerID": 0, "size": 1 }])
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(first.command().program(), OsStr::new("runsc"));
|
||||||
|
let runtime_domain = String::from_utf8(
|
||||||
|
fixture
|
||||||
|
.configuration
|
||||||
|
.runtime_domain_root()
|
||||||
|
.canonical_label()
|
||||||
|
.as_bytes()
|
||||||
|
.to_vec(),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
first
|
||||||
|
.command()
|
||||||
|
.arguments()
|
||||||
|
.iter()
|
||||||
|
.map(|argument| argument.to_str().unwrap())
|
||||||
|
.collect::<Vec<_>>(),
|
||||||
|
[
|
||||||
|
format!("-root={runtime_domain}/runsc"),
|
||||||
|
"-ignore-cgroups".to_owned(),
|
||||||
|
"run".to_owned(),
|
||||||
|
format!(
|
||||||
|
"-bundle={}/bundles/{}",
|
||||||
|
String::from_utf8(
|
||||||
|
fixture
|
||||||
|
.configuration
|
||||||
|
.runtime_domain_root()
|
||||||
|
.canonical_label()
|
||||||
|
.as_bytes()
|
||||||
|
.to_vec()
|
||||||
|
)
|
||||||
|
.unwrap(),
|
||||||
|
"11".repeat(32)
|
||||||
|
),
|
||||||
|
"11".repeat(32),
|
||||||
|
]
|
||||||
|
);
|
||||||
|
assert!(!first.command().arguments().iter().any(|argument| {
|
||||||
|
matches!(
|
||||||
|
argument.to_str(),
|
||||||
|
Some("-detach" | "-console-socket" | "-c")
|
||||||
|
)
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn selected_environment_volume_modes_and_host_terminal_are_preserved() {
|
||||||
|
let fixture = Fixture::new();
|
||||||
|
let bundle = build_sandbox_bundle(fixture.request(root_process([]))).unwrap();
|
||||||
|
let specification = value(&bundle);
|
||||||
|
let environment = specification["process"]["env"]
|
||||||
|
.as_array()
|
||||||
|
.unwrap()
|
||||||
|
.iter()
|
||||||
|
.map(|entry| entry.as_str().unwrap())
|
||||||
|
.collect::<BTreeSet<_>>();
|
||||||
|
assert!(environment.contains("SELECTED_SECRET=selected-value"));
|
||||||
|
assert!(environment.contains("TERM=xterm-lazaret"));
|
||||||
|
assert!(environment.contains("COLORTERM=truecolor"));
|
||||||
|
assert!(environment.contains("TERMINFO_DIRS=/run/lazaret/terminfo:"));
|
||||||
|
|
||||||
|
let mounts = mounts(&specification);
|
||||||
|
assert!(options(mounts[WORKSPACE_DESTINATION]).contains("rw"));
|
||||||
|
assert!(options(mounts["/root/read-only"]).contains("ro"));
|
||||||
|
assert!(options(mounts["/root/read-write"]).contains("rw"));
|
||||||
|
let terminfo = &mounts["/run/lazaret/terminfo/x/xterm-lazaret"];
|
||||||
|
assert_eq!(
|
||||||
|
terminfo["source"],
|
||||||
|
fixture.terminfo_source.display().to_string()
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
options(terminfo).is_superset(&BTreeSet::from(["bind", "ro", "nodev", "nosuid", "noexec"]))
|
||||||
|
);
|
||||||
|
|
||||||
|
assert!(options(mounts[CREDENTIALS_DESTINATION]).contains("ro"));
|
||||||
|
assert!(options(mounts[MAILBOX_REQUESTS_DESTINATION]).contains("rw"));
|
||||||
|
assert!(options(mounts[MAILBOX_RESULTS_DESTINATION]).contains("ro"));
|
||||||
|
assert!(options(mounts[ESCALATION_CLIENT_DESTINATION]).contains("ro"));
|
||||||
|
assert!(!options(mounts[ESCALATION_CLIENT_DESTINATION]).contains("noexec"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn configuration_document_unselected_secret_and_unselected_host_paths_do_not_enter_bundle() {
|
||||||
|
let fixture = Fixture::new();
|
||||||
|
let bundle = build_sandbox_bundle(fixture.request(root_process([]))).unwrap();
|
||||||
|
let bytes = bundle.specification_json();
|
||||||
|
for forbidden in [
|
||||||
|
b"lazaret-trusted-host-config-v1".as_slice(),
|
||||||
|
b"UNSELECTED_SECRET".as_slice(),
|
||||||
|
b"must-not-enter".as_slice(),
|
||||||
|
b"example.invalid/unselected:v1".as_slice(),
|
||||||
|
b"configuration/lazaret.toml".as_slice(),
|
||||||
|
] {
|
||||||
|
assert!(
|
||||||
|
!bytes
|
||||||
|
.windows(forbidden.len())
|
||||||
|
.any(|window| window == forbidden)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let specification = value(&bundle);
|
||||||
|
let allowed_external_sources = BTreeSet::from([
|
||||||
|
fixture.terminfo_source.display().to_string(),
|
||||||
|
String::from_utf8(
|
||||||
|
fixture
|
||||||
|
.configuration
|
||||||
|
.workspace()
|
||||||
|
.canonical_label()
|
||||||
|
.as_bytes()
|
||||||
|
.to_vec(),
|
||||||
|
)
|
||||||
|
.unwrap(),
|
||||||
|
String::from_utf8(
|
||||||
|
fixture.configuration.volumes()[0]
|
||||||
|
.record()
|
||||||
|
.canonical_host_source
|
||||||
|
.as_bytes()
|
||||||
|
.to_vec(),
|
||||||
|
)
|
||||||
|
.unwrap(),
|
||||||
|
String::from_utf8(
|
||||||
|
fixture.configuration.volumes()[1]
|
||||||
|
.record()
|
||||||
|
.canonical_host_source
|
||||||
|
.as_bytes()
|
||||||
|
.to_vec(),
|
||||||
|
)
|
||||||
|
.unwrap(),
|
||||||
|
]);
|
||||||
|
let runtime_domain = String::from_utf8(
|
||||||
|
fixture
|
||||||
|
.configuration
|
||||||
|
.runtime_domain_root()
|
||||||
|
.canonical_label()
|
||||||
|
.as_bytes()
|
||||||
|
.to_vec(),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
for mount in specification["mounts"].as_array().unwrap() {
|
||||||
|
let Some(source) = mount.get("source").and_then(Value::as_str) else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
if source.starts_with('/') {
|
||||||
|
assert!(
|
||||||
|
allowed_external_sources.contains(source) || source.starts_with(&runtime_domain)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn hostile_terminal_names_sources_and_changed_entries_fail_closed() {
|
||||||
|
let fixture = Fixture::new();
|
||||||
|
for term in ["../xterm", "xterm/name", "xterm\nname", "", &"x".repeat(65)] {
|
||||||
|
let error = HostTerminal::from_trusted_roots(
|
||||||
|
term,
|
||||||
|
None,
|
||||||
|
&fixture.terminfo_source,
|
||||||
|
std::slice::from_ref(&fixture.terminfo_root),
|
||||||
|
)
|
||||||
|
.unwrap_err();
|
||||||
|
assert_eq!(error.kind(), SandboxBuildErrorKind::InvalidTerminal);
|
||||||
|
}
|
||||||
|
let outside = fixture.tree.path().join("outside").join("xterm-lazaret");
|
||||||
|
fs::create_dir(outside.parent().unwrap()).unwrap();
|
||||||
|
fs::write(&outside, b"outside").unwrap();
|
||||||
|
assert!(
|
||||||
|
HostTerminal::from_trusted_roots(
|
||||||
|
"xterm-lazaret",
|
||||||
|
None,
|
||||||
|
&outside,
|
||||||
|
std::slice::from_ref(&fixture.terminfo_root),
|
||||||
|
)
|
||||||
|
.is_err()
|
||||||
|
);
|
||||||
|
|
||||||
|
let terminal = fixture.terminal();
|
||||||
|
fs::remove_file(&fixture.terminfo_source).unwrap();
|
||||||
|
fs::write(&fixture.terminfo_source, b"replacement").unwrap();
|
||||||
|
let error = build_sandbox_bundle(SandboxBuildRequest {
|
||||||
|
configuration: &fixture.configuration,
|
||||||
|
container_id: ContainerId::new([0x11; 32]),
|
||||||
|
expected_network_namespace: current_network_namespace(),
|
||||||
|
root_process: root_process([]),
|
||||||
|
terminal: Some(terminal),
|
||||||
|
})
|
||||||
|
.unwrap_err();
|
||||||
|
assert_eq!(error.kind(), SandboxBuildErrorKind::ChangedAuthority);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn invalid_processes_and_wrong_network_identity_fail_closed() {
|
||||||
|
assert!(RootProcess::new("bin/agent", []).is_err());
|
||||||
|
assert!(RootProcess::new("/bin/agent", ["nul\0argument".to_owned()]).is_err());
|
||||||
|
|
||||||
|
let fixture = Fixture::new();
|
||||||
|
for literal in [
|
||||||
|
"$(touch /host)",
|
||||||
|
"`touch /host`",
|
||||||
|
"a; touch /host",
|
||||||
|
"a && touch /host",
|
||||||
|
"a | touch /host",
|
||||||
|
"*.secret",
|
||||||
|
"$HOME",
|
||||||
|
] {
|
||||||
|
let bundle =
|
||||||
|
build_sandbox_bundle(fixture.request(root_process([literal.to_owned()]))).unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
value(&bundle)["process"]["args"],
|
||||||
|
serde_json::json!(["/sbin/lazaret-sandbox-init", literal])
|
||||||
|
);
|
||||||
|
}
|
||||||
|
let error = build_sandbox_bundle(SandboxBuildRequest {
|
||||||
|
configuration: &fixture.configuration,
|
||||||
|
container_id: ContainerId::new([0x11; 32]),
|
||||||
|
expected_network_namespace: NamespaceIdentity {
|
||||||
|
device: DeviceId::new(u64::MAX),
|
||||||
|
inode: InodeId::new(u64::MAX),
|
||||||
|
},
|
||||||
|
root_process: root_process([]),
|
||||||
|
terminal: Some(fixture.terminal()),
|
||||||
|
})
|
||||||
|
.unwrap_err();
|
||||||
|
assert_eq!(
|
||||||
|
error.kind(),
|
||||||
|
SandboxBuildErrorKind::NetworkNamespaceMismatch
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn production_layout_reserves_every_mount_root() {
|
||||||
|
let layout = production_sandbox_layout().unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
layout.protected_destinations(),
|
||||||
|
&BTreeSet::from([
|
||||||
|
"/dev".to_owned(),
|
||||||
|
"/proc".to_owned(),
|
||||||
|
"/run/lazaret".to_owned(),
|
||||||
|
"/sys".to_owned(),
|
||||||
|
"/workspace".to_owned(),
|
||||||
|
])
|
||||||
|
);
|
||||||
|
assert_eq!(TERMINFO_ROOT_DESTINATION, "/run/lazaret/terminfo");
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue