ea56c56e Andy McCall 1 month ago ↶
1
2
3
4
5
cc462b56 Andy McCall 10 days ago ↶
6
7
ea56c56e Andy McCall 1 month ago ↶
8
9
10
11
12
13
14
15
16
17
18
19
20
use crate :: guest:: { dev_links, job_env, loopback_up, resolve_program, PATH } ;
21
use crate :: home:: Home;
22
use crate :: microvm:: { Outcome, Sink} ;
3554afef Andy McCall 10 days ago ↶
23
pub use crate :: tier:: disk_dir;
24
pub ( crate ) use crate :: tier:: { lines, self_hash, FAILED } ;
ea56c56e Andy McCall 1 month ago ↶
25
use anyhow:: { bail, Context, Result } ;
26
use flow_core:: api:: { Record, SecretInput} ;
27
use flow_core:: { Environment, Job, MountSource, Network, Sandbox} ;
28
use serde:: { Deserialize, Serialize} ;
29
use std:: ffi:: CString;
30
use std:: path:: { Path, PathBuf} ;
31
use std:: sync:: { mpsc, OnceLock} ;
32
use std:: time:: { Duration, Instant} ;
33
34
35
36
const JOB_ID : u32 = 1000 ;
37
38
39
40
41
pub fn environment ( sandbox : & Sandbox, job : & Job) -> Environment {
42
Environment {
43
runtime: " syscall" .into ( ) ,
44
parts: vec! [
45
( " flow" .into ( ) , self_hash ( ) .to_string ( ) ) ,
46
( " kernel" .into ( ) , kernel_release ( ) ) ,
47
( " memory_mib" .into ( ) , sandbox.memory_mib.to_string ( ) ) ,
48
( " cpus" .into ( ) , sandbox.cpus.to_string ( ) ) ,
49
]
50
.into_iter ( )
51
.chain ( job.mounts.iter ( ) .map ( | m | ( format! ( " mount {} " , m.target) , format! ( " {} {} MiB" , m.source.as_str ( ) , m.size_mib) ) ) )
52
.collect ( ) ,
53
rootfs: sandbox.rootfs.clone ( ) ,
54
argv: job.exec.clone ( ) ,
55
env: job.env.iter ( ) .map ( | ( k , v ) | ( k.clone ( ) , v.clone ( ) ) ) .collect ( ) ,
56
}
57
}
58
59
fn kernel_release ( ) -> String {
337afb8f Andy McCall 10 days ago ↶
60
crate :: facts:: kernel( ) .unwrap_or_else ( | | " unknown" .into ( ) )
ea56c56e Andy McCall 1 month ago ↶
61
}
62
63
64
65
66
67
pub fn available ( ) -> std:: result:: Result< ( ) , String> {
68
Cgroups:: delegated( ) ? ;
69
let out = std:: process:: Command:: new( " /proc/self/exe" )
70
.arg ( " __sandbox-probe" )
71
.stdin ( std:: process:: Stdio:: null( ) )
72
.output ( )
73
.map_err ( | e | format! ( " cannot start the probe: {e} " ) ) ? ;
74
if out.status.success ( ) {
75
Ok ( ( ) )
76
} else {
77
Err ( String :: from_utf8_lossy( & out.stderr) .trim ( ) .to_string ( ) )
78
}
79
}
80
81
82
83
pub fn probe_main ( ) -> i32 {
84
let r = ( | | -> std:: result:: Result< ( ) , String> {
85
let ( uid, gid) = unsafe { ( libc:: getuid( ) , libc:: getgid( ) ) } ;
86
enter_namespaces ( libc:: CLONE_NEWUSER | libc:: CLONE_NEWNS , uid, gid)
87
.map_err ( | e | format! ( " user namespaces are not allowed for this program ({e} ); on Ubuntu, kernel.apparmor_restrict_unprivileged_userns=1 denies them" ) ) ? ;
88
mount ( " none" , " /" , " " , libc:: MS_REC | libc:: MS_PRIVATE , " " )
89
.and_then ( | _| mount ( " tmpfs" , " /tmp" , " tmpfs" , libc:: MS_NOSUID | libc:: MS_NODEV , " " ) )
90
.map_err ( | e | {
91
format! (
92
" user namespaces give no mount rights here ({e} ); on Ubuntu, kernel.apparmor_restrict_unprivileged_userns=1 does this"
93
)
94
} ) ? ;
95
let abi = landlock_abi ( ) ;
96
if abi < 1 {
97
return Err ( " Landlock is not enabled in this kernel (it must be in the lsm= list)" .into ( ) ) ;
98
}
99
no_new_privs ( ) ? ;
cc462b56 Andy McCall 10 days ago ↶
100
seccomp ( & [ ] , families ( false ) ) .map_err ( | e | format! ( " seccomp filters are not available: {e} " ) )
ea56c56e Andy McCall 1 month ago ↶
101
} ) ( ) ;
102
match r {
103
Ok ( ( ) ) => 0 ,
104
Err ( e) => {
105
eprintln! ( " {e}" ) ;
106
1
107
}
108
}
109
}
110
111
112
113
114
# [ derive ( Serialize, Deserialize ) ]
115
struct Spec {
116
117
rootfs : PathBuf,
118
119
root : PathBuf,
120
workspace : PathBuf,
121
122
cgroup : PathBuf,
123
argv : Vec< String> ,
124
env : Vec< ( String, String) > ,
125
mounts : Vec< MountSpec> ,
126
loopback : bool ,
127
secrets : Vec< ( String, String) > ,
128
}
129
130
# [ derive ( Serialize, Deserialize ) ]
131
struct MountSpec {
132
target : String,
133
size_mib : u32 ,
134
135
136
dir : Option< PathBuf> ,
137
}
138
139
140
141
# [ allow ( clippy::too_many_arguments ) ]
142
pub fn run (
143
home : & Home,
144
sandbox : & Sandbox,
145
job : & Job,
146
secrets : & [SecretInput],
147
workspace : & Path,
148
scratch : & Path,
149
timeout : Duration,
150
sink : & mut dyn Sink,
151
name : & str ,
152
) -> Result< Outcome> {
153
let cgroups = Cgroups:: delegated( ) .map_err ( | e | anyhow:: anyhow! ( " the syscall tier cannot run here: {e}" ) ) ? ;
154
let image = home.rootfs ( & sandbox.rootfs) ;
155
if ! image.is_file ( ) {
156
bail! ( " rootfs {} is not on this runner (import it with `flow rootfs import`)" , sandbox.rootfs) ;
157
}
158
let image = home.verified ( & image, & sandbox.rootfs) ? ;
159
let rootfs = unpacked ( home, & sandbox.rootfs, & image) ? ;
160
let environment = environment ( sandbox, job) ;
161
162
let mut mounts = Vec :: new( ) ;
163
for ( i, m) in job.mounts.iter ( ) .enumerate ( ) {
164
let dir = match m.source {
165
MountSource:: Tmpfs => None ,
166
MountSource:: Disk => {
167
let d = disk_dir ( scratch, job.mounts.iter ( ) .take ( i) .filter ( | m | m.source = = MountSource:: Disk ) .count ( ) ) ;
168
std:: fs:: create_dir_all( & d) ? ;
169
Some ( d)
170
}
171
} ;
172
mounts.push ( MountSpec { target: m.target.clone ( ) , size_mib: m.size_mib, dir } ) ;
173
}
174
let root = scratch.join ( " root" ) ;
175
std:: fs:: create_dir_all( & root) ? ;
176
let group = cgroups.job ( name, sandbox.memory_mib, sandbox.cpus) ? ;
177
let spec = Spec {
178
rootfs,
179
root,
180
workspace: workspace.to_path_buf ( ) ,
181
cgroup: group.dir.clone ( ) ,
182
argv: job.exec.clone ( ) ,
183
env: job.env.iter ( ) .map ( | ( k , v ) | ( k.clone ( ) , v.clone ( ) ) ) .collect ( ) ,
184
mounts,
185
loopback: sandbox.network = = Network:: Loopback,
186
secrets: secrets.iter ( ) .map ( | s | ( s.path.clone ( ) , s.value.clone ( ) ) ) .collect ( ) ,
187
} ;
188
let spec_path = scratch.join ( " sandbox.json" ) ;
189
{
190
use std:: io:: Write;
191
use std:: os:: unix:: fs:: OpenOptionsExt;
192
let mut f = std:: fs:: OpenOptions:: new( ) .write ( true ) .create_new ( true ) .mode ( 0o600 ) .open ( & spec_path) ? ;
193
f.write_all ( & serde_json:: to_vec( & spec) ? ) ? ;
194
}
195
196
let started = Instant:: now( ) ;
197
let mut child = std:: process:: Command:: new( " /proc/self/exe" )
198
.arg ( " __sandbox" )
199
.arg ( & spec_path)
200
.env_clear ( )
201
.current_dir ( scratch)
202
.stdin ( std:: process:: Stdio:: null( ) )
203
.stdout ( std:: process:: Stdio:: piped( ) )
204
.stderr ( std:: process:: Stdio:: piped( ) )
205
.spawn ( )
206
.context ( " start the sandbox" ) ? ;
207
let ( tx, rx) = mpsc:: channel:: < ( bool , Vec< u8 > ) > ( ) ;
208
let out = child.stdout.take ( ) .unwrap ( ) ;
209
let err = child.stderr.take ( ) .unwrap ( ) ;
210
let tx2 = tx.clone ( ) ;
211
let readers = [ std:: thread:: spawn( move | | lines ( out, true , tx) ) , std:: thread:: spawn( move | | lines ( err, false , tx2) ) ] ;
212
let mut failed_to_start = false ;
213
let mut timed_out = false ;
214
let mut cancelled = false ;
215
let deadline = started + timeout;
216
let t = | s : & Instant| s.elapsed ( ) .as_millis ( ) as u64 ;
217
let stop = | child : & mut std:: process:: Child| {
218
let _ = child.kill ( ) ;
219
group.kill ( ) ;
220
} ;
8f211e20 Andy McCall 24 days ago ↶
221
222
223
let mut peak_memory_mib = None ;
ea56c56e Andy McCall 1 month ago ↶
224
loop {
8f211e20 Andy McCall 24 days ago ↶
225
if let Some ( m) = crate :: facts:: peak_rss_mib( child.id ( ) ) {
226
peak_memory_mib = Some ( m) ;
227
}
ea56c56e Andy McCall 1 month ago ↶
228
match rx.recv_timeout ( Duration:: from_millis( 200 ) ) {
229
Ok ( ( stdout, line) ) => {
230
let text = String :: from_utf8_lossy( & line) .into_owned ( ) ;
231
let rec = | stream : & str , text : & str | Record { t: t ( & started) , stream: stream.into ( ) , text: text.into ( ) } ;
232
match text.strip_prefix ( FAILED ) {
233
Some ( why) if ! stdout => {
234
failed_to_start = true ;
235
sink.record ( rec ( " flow" , & format! ( " the sandbox failed to start the job: {why} " ) ) )
236
}
237
_ => sink.record ( rec ( if stdout { " out" } else { " err" } , & text) ) ,
238
}
239
}
240
Err ( mpsc:: RecvTimeoutError:: Timeout) => { }
241
Err ( mpsc:: RecvTimeoutError:: Disconnected) => break ,
242
}
243
if ! timed_out & & ! cancelled & & sink.cancelled ( ) {
244
cancelled = true ;
245
stop ( & mut child) ;
246
sink.record ( Record { t: t ( & started) , stream: " flow" .into ( ) , text: " stopped: the run was cancelled" .into ( ) } ) ;
247
}
248
if Instant:: now( ) > = deadline & & ! timed_out & & ! cancelled {
249
timed_out = true ;
250
stop ( & mut child) ;
251
sink.record ( Record {
252
t: t ( & started) ,
253
stream: " flow" .into ( ) ,
254
text: format! ( " stopped at the time limit of {} s" , timeout.as_secs ( ) ) ,
255
} ) ;
256
}
257
}
258
let status = child.wait ( ) ? ;
259
for r in readers {
260
let _ = r.join ( ) ;
261
}
262
if group.out_of_memory ( ) {
263
sink.record ( Record {
264
t: t ( & started) ,
265
stream: " flow" .into ( ) ,
266
text: format! ( " the job ran out of memory: the sandbox allows {} MiB" , sandbox.memory_mib) ,
267
} ) ;
268
}
269
let exit_code = status.code ( ) ;
270
if exit_code.is_none ( ) & & ! timed_out & & ! cancelled & & ! failed_to_start {
271
sink.record ( Record {
272
t: t ( & started) ,
273
stream: " flow" .into ( ) ,
274
text: format! ( " the sandbox ended without the job's exit code ({status} )" ) ,
275
} ) ;
276
}
277
drop ( group) ;
278
let _ = std:: fs:: remove_file( & spec_path) ;
279
Ok ( Outcome {
280
exit_code: if timed_out | | cancelled { None } else { exit_code } ,
281
duration_ms: t ( & started) ,
282
environment,
283
timed_out,
284
cancelled,
8f211e20 Andy McCall 24 days ago ↶
285
peak_memory_mib,
077a6524 Andy McCall 16 days ago ↶
286
pruned: Vec :: new( ) ,
ea56c56e Andy McCall 1 month ago ↶
287
} )
288
}
289
290
291
292
293
294
fn unpacked ( home : & Home, hash : & str , image : & Path) -> Result< PathBuf> {
295
let trees = home.root.join ( " trees" ) ;
296
let dir = trees.join ( hash) ;
297
if dir.is_dir ( ) {
298
return Ok ( dir) ;
299
}
300
std:: fs:: create_dir_all( & trees) ? ;
301
let nanos = std:: time:: SystemTime:: now( ) .duration_since ( std:: time:: UNIX_EPOCH ) .map ( | d | d.subsec_nanos ( ) ) .unwrap_or ( 0 ) ;
302
let tmp = trees.join ( format! ( " .{hash} .{} .{nanos} " , std:: process:: id( ) ) ) ;
303
std:: fs:: create_dir_all( & tmp) ? ;
304
let out = std:: process:: Command:: new( " debugfs" )
305
.arg ( " -R" )
306
.arg ( format! ( " rdump / \" {} \" " , tmp.display ( ) ) )
307
.arg ( image)
308
.output ( )
309
.context ( " run debugfs (e2fsprogs) to unpack the rootfs" ) ? ;
310
let empty = std:: fs:: read_dir( & tmp) .map ( | mut d | d.next ( ) .is_none ( ) ) .unwrap_or ( true ) ;
311
if ! out.status.success ( ) | | empty {
312
let _ = crate :: workspace:: remove_tree( & tmp) ;
313
bail! ( " unpacking rootfs {}: {}" , & hash[ .. 12. min ( hash.len ( ) ) ] , String :: from_utf8_lossy( & out.stderr) .trim ( ) ) ;
314
}
315
match std:: fs:: rename( & tmp, & dir) {
316
Ok ( ( ) ) => Ok ( dir) ,
317
318
Err ( _ ) if dir.is_dir ( ) => {
319
let _ = crate :: workspace:: remove_tree( & tmp) ;
320
Ok ( dir)
321
}
322
Err ( e) => Err ( e) .with_context ( | | format! ( " keep the unpacked rootfs at {} " , dir.display ( ) ) ) ,
323
}
324
}
325
326
327
328
329
330
331
332
# [ derive ( Clone ) ]
333
pub struct Cgroups {
334
base : PathBuf,
335
}
336
337
338
const MAIN_LEAF : & str = " flow-main" ;
339
340
impl Cgroups {
341
pub fn delegated ( ) -> std:: result:: Result< Cgroups, String> {
342
static ONCE : OnceLock< std:: result:: Result< Cgroups, String> > = OnceLock:: new( ) ;
343
ONCE .get_or_init ( Self :: set_up) .clone ( )
344
}
345
346
fn set_up ( ) -> std:: result:: Result< Cgroups, String> {
347
let own = std:: fs:: read_to_string( " /proc/self/cgroup" ) .map_err ( | e | format! ( " read /proc/self/cgroup: {e} " ) ) ? ;
348
let rel = own
349
.lines ( )
350
.find_map ( | l | l.strip_prefix ( " 0::" ) )
351
.ok_or ( " this machine does not use cgroup v2 (the unified hierarchy)" ) ?
352
.trim ( )
353
.to_string ( ) ;
354
let mut base = Path:: new( " /sys/fs/cgroup" ) .join ( rel.trim_start_matches ( ' /' ) ) ;
355
if base.file_name ( ) .is_some_and ( | n | n = = MAIN_LEAF ) {
356
base.pop ( ) ;
357
}
358
let controllers = std:: fs:: read_to_string( base.join ( " cgroup.controllers" ) ) .unwrap_or_default ( ) ;
359
let has = | c : & str | controllers.split_whitespace ( ) .any ( | x | x = = c ) ;
360
let delegate_hint = " run the runner as a systemd service with Delegate=yes, or as a user service; for `flow run`, use `systemd-run --user --scope -p Delegate=yes flow run …`" ;
361
if ! has ( " cpu" ) | | ! has ( " memory" ) {
362
return Err ( format! ( " the cgroup {} has no cpu and memory controllers to give jobs; {delegate_hint} " , base.display ( ) ) ) ;
363
}
364
let writable =
365
| p : & Path| CString:: new( p.as_os_str ( ) .as_encoded_bytes ( ) ) .is_ok_and ( | c | unsafe { libc:: access( c.as_ptr ( ) , libc:: W_OK ) } = = 0 ) ;
366
if ! writable ( & base) | | ! writable ( & base.join ( " cgroup.subtree_control" ) ) | | ! writable ( & base.join ( " cgroup.procs" ) ) {
367
return Err ( format! ( " this runner may not manage its cgroup {} ; {delegate_hint} " , base.display ( ) ) ) ;
368
}
369
370
371
let me = std:: process:: id( ) .to_string ( ) ;
372
let others = std:: fs:: read_to_string( base.join ( " cgroup.procs" ) ) .unwrap_or_default ( ) ;
373
if others.lines ( ) .any ( | p | p.trim ( ) ! = me ) {
374
return Err ( format! ( " the cgroup {} holds other processes; {delegate_hint} " , base.display ( ) ) ) ;
375
}
376
let leaf = base.join ( MAIN_LEAF ) ;
377
if ! leaf.is_dir ( ) {
378
std:: fs:: create_dir( & leaf) .map_err ( | e | format! ( " make {} : {e} " , leaf.display ( ) ) ) ? ;
379
}
380
std:: fs:: write( leaf.join ( " cgroup.procs" ) , " 0" ) .map_err ( | e | format! ( " move into {} : {e} " , leaf.display ( ) ) ) ? ;
381
let want: Vec< & str > = [ " cpu" , " memory" , " pids" ] .into_iter ( ) .filter ( | c | has ( c) ) .collect ( ) ;
382
let enable = want.iter ( ) .map ( | c | format! ( " +{c} " ) ) .collect:: < Vec< _ > > ( ) .join ( " " ) ;
383
std:: fs:: write( base.join ( " cgroup.subtree_control" ) , & enable)
384
.map_err ( | e | format! ( " enable {enable} in {} : {e} (another process may still be in it; {delegate_hint} )" , base.display ( ) ) ) ? ;
385
Ok ( Cgroups { base } )
386
}
387
388
389
pub fn job ( & self , name : & str , memory_mib : u32 , cpus : u32 ) -> Result< JobGroup> {
390
let dir = self .base.join ( name) ;
391
if dir.is_dir ( ) {
392
393
drop ( JobGroup { dir: dir.clone ( ) } ) ;
394
}
395
std:: fs:: create_dir( & dir) .with_context ( | | format! ( " make the job's cgroup {} " , dir.display ( ) ) ) ? ;
396
let group = JobGroup { dir } ;
397
let set = | file : & str , value : String| {
398
std:: fs:: write( group.dir.join ( file) , & value) .with_context ( | | format! ( " set {file} to {value} in {} " , group.dir.display ( ) ) )
399
} ;
400
set ( " memory.max" , ( u64 :: from( memory_mib) < < 20 ) .to_string ( ) ) ? ;
401
if group.dir.join ( " memory.swap.max" ) .exists ( ) {
402
set ( " memory.swap.max" , " 0" .into ( ) ) ? ;
403
}
404
set ( " cpu.max" , format! ( " {} 100000" , u64 :: from( cpus.max ( 1 ) ) * 100_000 ) ) ? ;
405
Ok ( group)
406
}
407
}
408
409
410
pub struct JobGroup {
411
dir : PathBuf,
412
}
413
414
impl JobGroup {
415
fn kill ( & self ) {
416
let _ = std:: fs:: write( self .dir.join ( " cgroup.kill" ) , " 1" ) ;
417
}
418
419
fn out_of_memory ( & self ) -> bool {
420
std:: fs:: read_to_string( self .dir.join ( " memory.events" ) )
421
.ok ( )
422
.and_then ( | s | s.lines ( ) .find_map ( | l | l.strip_prefix ( " oom_kill " ) ) .and_then ( | n | n.trim ( ) .parse:: < u64 > ( ) .ok ( ) ) )
423
.is_some_and ( | n | n > 0 )
424
}
425
}
426
427
impl Drop for JobGroup {
428
fn drop ( & mut self ) {
429
self .kill ( ) ;
430
for _ in 0 .. 100 {
431
if std:: fs:: remove_dir( & self .dir) .is_ok ( ) | | ! self .dir.exists ( ) {
432
return ;
433
}
434
std:: thread:: sleep( Duration:: from_millis( 50 ) ) ;
435
}
436
eprintln! ( " flow: could not remove the cgroup {}" , self .dir.display ( ) ) ;
437
}
438
}
439
440
441
442
443
444
pub fn sandbox_main ( spec_path : & Path) -> i32 {
445
match helper ( spec_path) {
446
Ok ( code) => code,
447
Err ( e) => {
448
eprintln! ( " {FAILED}{e}" ) ;
449
125
450
}
451
}
452
}
453
454
fn helper ( spec_path : & Path) -> std:: result:: Result< i32 , String> {
455
let spec: Spec = serde_json:: from_slice( & std:: fs:: read( spec_path) .map_err ( | e | format! ( " read the spec: {e} " ) ) ? )
456
.map_err ( | e | format! ( " parse the spec: {e} " ) ) ? ;
457
let _ = std:: fs:: remove_file( spec_path) ;
458
if spec.argv.is_empty ( ) {
459
return Err ( " empty argv" .into ( ) ) ;
460
}
461
std:: fs:: write( spec.cgroup.join ( " cgroup.procs" ) , " 0" ) .map_err ( | e | format! ( " join the job's cgroup: {e} " ) ) ? ;
462
let ( uid, gid) = unsafe { ( libc:: getuid( ) , libc:: getgid( ) ) } ;
463
enter_namespaces (
464
libc:: CLONE_NEWUSER | libc:: CLONE_NEWNS | libc:: CLONE_NEWPID | libc:: CLONE_NEWNET | libc:: CLONE_NEWIPC | libc:: CLONE_NEWUTS ,
465
uid,
466
gid,
467
) ? ;
468
469
470
let pid = unsafe { libc:: fork( ) } ;
471
if pid < 0 {
472
return Err ( format! ( " fork: {} " , std:: io:: Error:: last_os_error( ) ) ) ;
473
}
474
if pid = = 0 {
475
let code = match init ( & spec) {
476
Ok ( code) => code,
477
Err ( e) => {
478
eprintln! ( " {FAILED}{e}" ) ;
479
125
480
}
481
} ;
482
unsafe { libc:: _exit( code) } ;
483
}
484
Ok ( wait_for ( pid) )
485
}
486
487
488
489
fn wait_for ( pid : libc:: pid_t) -> i32 {
490
let mut status = 0 ;
491
loop {
492
let r = unsafe { libc:: waitpid( - 1 , & mut status, 0 ) } ;
493
if r = = pid {
494
break ;
495
}
496
if r < 0 & & std:: io:: Error:: last_os_error( ) .raw_os_error ( ) ! = Some ( libc:: EINTR ) {
497
return 125 ;
498
}
499
}
500
if libc:: WIFEXITED ( status) {
501
libc:: WEXITSTATUS ( status)
502
} else {
503
128 + libc:: WTERMSIG ( status)
504
}
505
}
506
507
508
509
fn enter_namespaces ( flags : libc:: c_int, uid : u32 , gid : u32 ) -> std:: result:: Result< ( ) , String> {
510
if unsafe { libc:: unshare( flags) } ! = 0 {
511
return Err ( format! ( " unshare: {} " , std:: io:: Error:: last_os_error( ) ) ) ;
512
}
513
let write = | file : & str , body : String| std:: fs:: write( file, body) .map_err ( | e | format! ( " write {file} : {e} " ) ) ;
514
write ( " /proc/self/setgroups" , " deny" .into ( ) ) ? ;
515
write ( " /proc/self/uid_map" , format! ( " {JOB_ID} {uid} 1\n " ) ) ? ;
516
write ( " /proc/self/gid_map" , format! ( " {JOB_ID} {gid} 1\n " ) )
517
}
518
519
520
fn init ( spec : & Spec) -> std:: result:: Result< i32 , String> {
521
522
unsafe { libc:: prctl( libc:: PR_SET_PDEATHSIG , libc:: SIGKILL ) } ;
523
let nosuid = libc:: MS_NOSUID | libc:: MS_NODEV ;
524
mount ( " none" , " /" , " " , libc:: MS_REC | libc:: MS_PRIVATE , " " ) ? ;
525
let root = spec.root.to_str ( ) .ok_or ( " the root path is not UTF-8" ) ? ;
526
let at = | p : & str | format! ( " {root} {p} " ) ;
527
bind ( spec.rootfs.to_str ( ) .ok_or ( " the rootfs path is not UTF-8" ) ? , root) ? ;
528
for d in [ " /src" , " /tmp" , " /proc" , " /dev" ] {
529
if ! Path:: new( & at ( d) ) .is_dir ( ) {
530
return Err ( format! ( " the image has no {d} directory" ) ) ;
531
}
532
}
533
remount_bind ( root, libc:: MS_RDONLY | nosuid) ? ;
534
bind ( spec.workspace.to_str ( ) .ok_or ( " the workspace path is not UTF-8" ) ? , & at ( " /src" ) ) ? ;
535
mount ( " tmpfs" , & at ( " /tmp" ) , " tmpfs" , nosuid, " mode=1777" ) ? ;
536
mount ( " proc" , & at ( " /proc" ) , " proc" , nosuid | libc:: MS_NOEXEC , " " ) ? ;
537
538
mount ( " tmpfs" , & at ( " /dev" ) , " tmpfs" , libc:: MS_NOSUID | libc:: MS_NOEXEC , " mode=755" ) ? ;
539
for d in [ " null" , " zero" , " full" , " random" , " urandom" ] {
540
let target = at ( & format! ( " /dev/{d} " ) ) ;
541
std:: fs:: File:: create( & target) .map_err ( | e | format! ( " make {target} : {e} " ) ) ? ;
542
bind ( & format! ( " /dev/{d} " ) , & target) ? ;
543
}
544
dev_links ( & at ( " /dev" ) ) ? ;
545
std:: fs:: create_dir( at ( " /dev/shm" ) ) .map_err ( | e | format! ( " make /dev/shm: {e} " ) ) ? ;
546
mount ( " tmpfs" , & at ( " /dev/shm" ) , " tmpfs" , nosuid, " mode=1777" ) ? ;
547
if Path:: new( & at ( " /run" ) ) .is_dir ( ) {
548
mount ( " tmpfs" , & at ( " /run" ) , " tmpfs" , nosuid, " mode=755" ) ? ;
549
}
550
551
for m in & spec.mounts {
552
let target = at ( & m.target) ;
553
std:: fs:: create_dir_all( & target) .map_err ( | e | format! ( " make {} : {e} " , m.target) ) ? ;
554
match & m.dir {
555
Some ( dir) => bind ( dir.to_str ( ) .ok_or ( " a mount path is not UTF-8" ) ? , & target) ? ,
556
None => mount ( " tmpfs" , & target, " tmpfs" , nosuid, & format! ( " size={} m,mode=755" , m.size_mib) ) ? ,
557
}
558
}
559
560
if ! spec.secrets.is_empty ( ) {
561
if ! Path:: new( & at ( " /run" ) ) .is_dir ( ) {
562
return Err ( " the image has no /run, where secrets are mounted" .into ( ) ) ;
563
}
564
let dir = at ( " /run/secrets" ) ;
565
std:: fs:: create_dir_all( & dir) .map_err ( | e | format! ( " mkdir {dir} : {e} " ) ) ? ;
566
mount ( " tmpfs" , & dir, " tmpfs" , nosuid | libc:: MS_NOEXEC , " size=1m,mode=700" ) ? ;
567
for ( path, value) in & spec.secrets {
568
let Some ( rel) = path.strip_prefix ( " /run/secrets/" ) else {
569
return Err ( format! ( " secret path {path:?} is not under /run/secrets/" ) ) ;
570
} ;
571
let full = format! ( " {dir} /{rel} " ) ;
572
if let Some ( parent) = Path:: new( & full) .parent ( ) {
573
std:: fs:: create_dir_all( parent) .map_err ( | e | format! ( " mkdir for {path} : {e} " ) ) ? ;
574
}
575
std:: fs:: write( & full, value.as_bytes ( ) ) .map_err ( | e | format! ( " write {path} : {e} " ) ) ? ;
576
std:: fs:: set_permissions( & full, std:: os:: unix:: fs:: PermissionsExt:: from_mode( 0o400 ) )
577
.map_err ( | e | format! ( " chmod {path} : {e} " ) ) ? ;
578
}
579
}
580
let host = b " flow" ;
581
unsafe { libc:: sethostname( host.as_ptr ( ) as * const libc:: c_char , host.len ( ) ) } ;
582
if spec.loopback {
583
loopback_up ( ) ? ;
584
}
585
586
587
std:: env:: set_current_dir( root) .map_err ( | e | format! ( " cd {root} : {e} " ) ) ? ;
588
let dot = c" ." ;
589
if unsafe { libc:: syscall( libc:: SYS_pivot_root, dot.as_ptr ( ) , dot.as_ptr ( ) ) } ! = 0 {
590
return Err ( format! ( " pivot_root: {} " , std:: io:: Error:: last_os_error( ) ) ) ;
591
}
592
if unsafe { libc:: umount2( dot.as_ptr ( ) , libc:: MNT_DETACH ) } ! = 0 {
593
return Err ( format! ( " detach the old root: {} " , std:: io:: Error:: last_os_error( ) ) ) ;
594
}
595
std:: env:: set_current_dir( " /src" ) .map_err ( | e | format! ( " cd /src: {e} " ) ) ? ;
596
597
let vars = job_env ( std:: fs:: read_to_string( " /etc/flow/env" ) .ok ( ) .as_deref ( ) , & spec.env) ;
598
let path = vars.get ( " PATH" ) .map ( String :: as_str) .unwrap_or ( PATH ) ;
599
let program =
600
cstr ( & resolve_program ( & spec.argv[ 0 ] , path) .ok_or_else ( | | format! ( " cannot run {:?} : not found in {path} " , spec.argv[ 0 ] ) ) ? ) ? ;
601
let argv: Vec< CString> = spec.argv.iter ( ) .map ( | a | cstr ( a) ) .collect:: < Result< _ , _ > > ( ) ? ;
602
let env: Vec< CString> = vars.iter ( ) .map ( | ( k , v ) | cstr ( & format! ( " {k} ={v} " ) ) ) .collect:: < Result< _ , _ > > ( ) ? ;
603
let mut writable: Vec< String> = [ " /src" , " /tmp" , " /dev" ] .into_iter ( ) .map ( String :: from) .collect ( ) ;
604
writable.extend ( spec.mounts.iter ( ) .map ( | m | m.target.clone ( ) ) ) ;
605
let pid = unsafe { libc:: fork( ) } ;
606
if pid < 0 {
607
return Err ( format! ( " fork: {} " , std:: io:: Error:: last_os_error( ) ) ) ;
608
}
609
if pid = = 0 {
610
611
let confined = ( | | {
612
unsafe {
613
let null = libc:: open( c" /dev/null" .as_ptr ( ) , libc:: O_RDONLY ) ;
614
libc:: dup2( null, 0 ) ;
615
libc:: setsid( ) ;
616
}
617
no_new_privs ( ) ? ;
618
landlock ( & writable) ? ;
cc462b56 Andy McCall 10 days ago ↶
619
seccomp ( DENIED , families ( spec.loopback) )
ea56c56e Andy McCall 1 month ago ↶
620
} ) ( ) ;
621
unsafe {
622
if let Err ( e) = confined {
623
let msg = format! ( " {FAILED} {e} \n " ) ;
624
libc:: write( 2 , msg.as_ptr ( ) as * const libc:: c_void , msg.len ( ) ) ;
625
libc:: _exit( 125 ) ;
626
}
627
let mut a: Vec< * const libc:: c_char > = argv.iter ( ) .map ( | c | c.as_ptr ( ) ) .collect ( ) ;
628
a.push ( std:: ptr:: null( ) ) ;
629
let mut e: Vec< * const libc:: c_char > = env.iter ( ) .map ( | c | c.as_ptr ( ) ) .collect ( ) ;
630
e.push ( std:: ptr:: null( ) ) ;
631
libc:: execve( program.as_ptr ( ) , a.as_ptr ( ) , e.as_ptr ( ) ) ;
632
let msg = format! ( " flow: cannot run {:?} : {} \n " , spec.argv[ 0 ] , std:: io:: Error:: last_os_error( ) ) ;
633
libc:: write( 2 , msg.as_ptr ( ) as * const libc:: c_void , msg.len ( ) ) ;
634
libc:: _exit( 127 ) ;
635
}
636
}
637
Ok ( wait_for ( pid) )
638
}
639
640
fn cstr ( s : & str ) -> std:: result:: Result< CString, String> {
641
CString:: new( s) .map_err ( | _| format! ( " {s:?} contains NUL" ) )
642
}
643
644
fn mount ( src : & str , target : & str , fstype : & str , flags : libc:: c_ulong, data : & str ) -> std:: result:: Result< ( ) , String> {
645
let ( s, t, f, d) = ( cstr ( src) ? , cstr ( target) ? , cstr ( fstype) ? , cstr ( data) ? ) ;
646
let fstype = if fstype.is_empty ( ) { std:: ptr:: null( ) } else { f.as_ptr ( ) } ;
647
let data = if data.is_empty ( ) { std:: ptr:: null( ) } else { d.as_ptr ( ) as * const libc:: c_void } ;
648
if unsafe { libc:: mount( s.as_ptr ( ) , t.as_ptr ( ) , fstype, flags, data) } ! = 0 {
649
return Err ( format! ( " mount {src} on {target} : {} " , std:: io:: Error:: last_os_error( ) ) ) ;
650
}
651
Ok ( ( ) )
652
}
653
654
fn bind ( src : & str , target : & str ) -> std:: result:: Result< ( ) , String> {
655
mount ( src, target, " " , libc:: MS_BIND | libc:: MS_REC , " " )
656
}
657
658
659
660
fn remount_bind ( target : & str , flags : libc:: c_ulong) -> std:: result:: Result< ( ) , String> {
661
let t = cstr ( target) ? ;
662
let mut st: libc:: statvfs = unsafe { std:: mem:: zeroed( ) } ;
663
if unsafe { libc:: statvfs( t.as_ptr ( ) , & mut st) } ! = 0 {
664
return Err ( format! ( " statvfs {target} : {} " , std:: io:: Error:: last_os_error( ) ) ) ;
665
}
666
let kept = [
667
( libc:: ST_NOSUID , libc:: MS_NOSUID ) ,
668
( libc:: ST_NODEV , libc:: MS_NODEV ) ,
669
( libc:: ST_NOEXEC , libc:: MS_NOEXEC ) ,
670
( libc:: ST_NOATIME , libc:: MS_NOATIME ) ,
671
( libc:: ST_NODIRATIME , libc:: MS_NODIRATIME ) ,
672
( libc:: ST_RELATIME , libc:: MS_RELATIME ) ,
673
]
674
.into_iter ( )
675
.filter ( | ( st_flag , _) | st.f_flag & * st_flag ! = 0 )
676
.fold ( 0 , | acc , ( _, ms ) | acc | ms ) ;
677
mount ( " none" , target, " " , libc:: MS_BIND | libc:: MS_REMOUNT | flags | kept, " " )
678
}
679
680
fn no_new_privs ( ) -> std:: result:: Result< ( ) , String> {
681
if unsafe { libc:: prctl( libc:: PR_SET_NO_NEW_PRIVS , 1 , 0 , 0 , 0 ) } ! = 0 {
682
return Err ( format! ( " no_new_privs: {} " , std:: io:: Error:: last_os_error( ) ) ) ;
683
}
684
Ok ( ( ) )
685
}
686
687
688
689
const LANDLOCK_CREATE_RULESET_VERSION : libc:: c_uint = 1 ;
690
const LANDLOCK_RULE_PATH_BENEATH : libc:: c_int = 1 ;
691
const ACCESS_EXECUTE : u64 = 1 ;
692
const ACCESS_READ_FILE : u64 = 1 < < 2 ;
693
const ACCESS_READ_DIR : u64 = 1 < < 3 ;
694
695
const ACCESS_ABI1 : u64 = ( 1 < < 13 ) - 1 ;
696
const ACCESS_REFER : u64 = 1 < < 13 ;
697
const ACCESS_TRUNCATE : u64 = 1 < < 14 ;
698
699
# [ repr ( C ) ]
700
struct RulesetAttr {
701
handled_access_fs : u64 ,
702
}
703
704
# [ repr ( C, packed ) ]
705
struct PathBeneath {
706
allowed_access : u64 ,
707
parent_fd : i32 ,
708
}
709
710
711
fn landlock_abi ( ) -> i64 {
712
unsafe { libc:: syscall( libc:: SYS_landlock_create_ruleset, std:: ptr:: null:: < RulesetAttr> ( ) , 0 usize , LANDLOCK_CREATE_RULESET_VERSION ) }
713
}
714
715
716
fn landlock ( writable : & [String]) -> std:: result:: Result< ( ) , String> {
717
let abi = landlock_abi ( ) ;
718
if abi < 1 {
719
return Err ( " Landlock is not available" .into ( ) ) ;
720
}
721
let mut all = ACCESS_ABI1 ;
722
if abi > = 2 {
723
all | = ACCESS_REFER ;
724
}
725
if abi > = 3 {
726
all | = ACCESS_TRUNCATE ;
727
}
728
let attr = RulesetAttr { handled_access_fs: all } ;
729
let ruleset = unsafe { libc:: syscall( libc:: SYS_landlock_create_ruleset, & attr, std:: mem:: size_of:: < RulesetAttr> ( ) , 0 ) } ;
730
if ruleset < 0 {
731
return Err ( format! ( " landlock ruleset: {} " , std:: io:: Error:: last_os_error( ) ) ) ;
732
}
733
let ruleset = ruleset as libc:: c_int ;
734
let rule = | path : & str , access : u64 | -> std:: result:: Result< ( ) , String> {
735
let p = cstr ( path) ? ;
736
let fd = unsafe { libc:: open( p.as_ptr ( ) , libc:: O_PATH | libc:: O_CLOEXEC ) } ;
737
if fd < 0 {
738
return Err ( format! ( " open {path} for Landlock: {} " , std:: io:: Error:: last_os_error( ) ) ) ;
739
}
740
let beneath = PathBeneath { allowed_access: access, parent_fd: fd } ;
741
let rc = unsafe { libc:: syscall( libc:: SYS_landlock_add_rule, ruleset, LANDLOCK_RULE_PATH_BENEATH , & beneath, 0 ) } ;
742
unsafe { libc:: close( fd) } ;
743
if rc ! = 0 {
744
return Err ( format! ( " landlock rule for {path} : {} " , std:: io:: Error:: last_os_error( ) ) ) ;
745
}
746
Ok ( ( ) )
747
} ;
748
rule ( " /" , ACCESS_EXECUTE | ACCESS_READ_FILE | ACCESS_READ_DIR ) ? ;
749
for w in writable {
750
rule ( w, all) ? ;
751
}
752
if unsafe { libc:: syscall( libc:: SYS_landlock_restrict_self, ruleset, 0 ) } ! = 0 {
753
return Err ( format! ( " landlock restrict: {} " , std:: io:: Error:: last_os_error( ) ) ) ;
754
}
755
unsafe { libc:: close( ruleset) } ;
756
Ok ( ( ) )
757
}
758
759
760
761
# [ cfg ( target_arch = " x86_64" ) ]
762
const AUDIT_ARCH : u32 = 0xC000_003E ;
763
# [ cfg ( target_arch = " aarch64" ) ]
764
const AUDIT_ARCH : u32 = 0xC000_00B7 ;
765
# [ cfg ( target_arch = " riscv64" ) ]
766
const AUDIT_ARCH : u32 = 0xC000_00F3 ;
767
768
769
770
771
const DENIED : & [ libc:: c_long ] = & [
772
libc:: SYS_ptrace,
773
libc:: SYS_process_vm_readv,
774
libc:: SYS_process_vm_writev,
775
libc:: SYS_mount,
776
libc:: SYS_umount2,
777
libc:: SYS_pivot_root,
778
libc:: SYS_open_tree,
779
libc:: SYS_move_mount,
780
libc:: SYS_fsopen,
781
libc:: SYS_fsconfig,
782
libc:: SYS_fsmount,
783
libc:: SYS_fspick,
784
libc:: SYS_bpf,
785
libc:: SYS_perf_event_open,
786
libc:: SYS_keyctl,
787
libc:: SYS_add_key,
788
libc:: SYS_request_key,
789
libc:: SYS_init_module,
790
libc:: SYS_finit_module,
791
libc:: SYS_delete_module,
792
libc:: SYS_kexec_load,
793
libc:: SYS_swapon,
794
libc:: SYS_swapoff,
795
libc:: SYS_reboot,
796
libc:: SYS_io_uring_setup,
797
libc:: SYS_io_uring_enter,
798
libc:: SYS_io_uring_register,
799
] ;
800
5cf1e8ae Andy McCall 11 days ago ↶
801
802
803
804
const BPF_LD : u16 = 0x00 ;
805
const BPF_W : u16 = 0x00 ;
806
const BPF_ABS : u16 = 0x20 ;
807
const BPF_JMP : u16 = 0x05 ;
808
const BPF_JEQ : u16 = 0x10 ;
809
const BPF_K : u16 = 0x00 ;
810
const BPF_RET : u16 = 0x06 ;
811
cc462b56 Andy McCall 10 days ago ↶
812
813
814
815
fn families ( loopback : bool ) -> & 'static [ libc:: c_int ] {
816
if loopback {
817
& [ libc:: AF_UNIX , libc:: AF_INET , libc:: AF_INET6 ]
818
} else {
819
& [ libc:: AF_UNIX ]
820
}
821
}
822
ea56c56e Andy McCall 1 month ago ↶
823
cc462b56 Andy McCall 10 days ago ↶
824
ea56c56e Andy McCall 1 month ago ↶
825
cc462b56 Andy McCall 10 days ago ↶
826
fn seccomp ( denied : & [libc:: c_long], allowed : & [libc:: c_int]) -> std:: result:: Result< ( ) , String> {
5cf1e8ae Andy McCall 11 days ago ↶
827
const LD : u16 = BPF_LD | BPF_W | BPF_ABS ;
828
const JEQ : u16 = BPF_JMP | BPF_JEQ | BPF_K ;
829
const RET : u16 = BPF_RET | BPF_K ;
ea56c56e Andy McCall 1 month ago ↶
830
let op = | code : u16 , k : u32 , jt : u8 , jf : u8 | libc:: sock_filter { code, jt, jf, k } ;
831
let errno = | e : libc:: c_int| libc:: SECCOMP_RET_ERRNO | ( e as u32 & libc:: SECCOMP_RET_DATA ) ;
832
833
834
let mut f = vec! [ op ( LD , 4 , 0 , 0 ) , op ( JEQ , AUDIT_ARCH , 1 , 0 ) , op ( RET , libc:: SECCOMP_RET_KILL_PROCESS , 0 , 0 ) , op ( LD , 0 , 0 , 0 ) ] ;
835
# [ cfg ( target_arch = " x86_64" ) ]
836
{
837
838
const JGE : u16 = ( libc:: BPF_JMP | libc:: BPF_JGE | libc:: BPF_K ) as u16 ;
839
f.push ( op ( JGE , 0x4000_0000 , 0 , 1 ) ) ;
840
f.push ( op ( RET , errno ( libc:: EPERM ) , 0 , 0 ) ) ;
841
}
842
for nr in denied {
843
f.push ( op ( JEQ , * nr as u32 , 0 , 1 ) ) ;
844
f.push ( op ( RET , errno ( libc:: EPERM ) , 0 , 0 ) ) ;
845
}
cc462b56 Andy McCall 10 days ago ↶
846
847
848
let n = allowed.len ( ) as u8 ;
849
f.push ( op ( JEQ , libc:: SYS_socket as u32 , 0 , n + 2 ) ) ;
ea56c56e Andy McCall 1 month ago ↶
850
f.push ( op ( LD , 16 , 0 , 0 ) ) ;
cc462b56 Andy McCall 10 days ago ↶
851
for ( i, fam) in allowed.iter ( ) .enumerate ( ) {
852
f.push ( op ( JEQ , * fam as u32 , n - i as u8 , 0 ) ) ;
853
}
ea56c56e Andy McCall 1 month ago ↶
854
f.push ( op ( RET , errno ( libc:: EAFNOSUPPORT ) , 0 , 0 ) ) ;
855
f.push ( op ( RET , libc:: SECCOMP_RET_ALLOW , 0 , 0 ) ) ;
856
let prog = libc:: sock_fprog { len: f.len ( ) as u16 , filter: f.as_mut_ptr ( ) } ;
857
if unsafe { libc:: syscall( libc:: SYS_seccomp, libc:: SECCOMP_SET_MODE_FILTER , 0 , & prog) } ! = 0 {
858
return Err ( format! ( " seccomp: {} " , std:: io:: Error:: last_os_error( ) ) ) ;
859
}
860
Ok ( ( ) )
5cf1e8ae Andy McCall 11 days ago ↶
861
}
862
863
# [ cfg ( all ( test, not ( target_os = " android" ) ) ) ]
864
mod tests {
865
866
# [ test ]
867
fn bpf_opcodes_are_the_kernels ( ) {
868
use super :: * ;
869
assert_eq! (
870
[ BPF_LD , BPF_W , BPF_ABS , BPF_JMP , BPF_JEQ , BPF_K , BPF_RET ] ,
871
[ libc:: BPF_LD , libc:: BPF_W , libc:: BPF_ABS , libc:: BPF_JMP , libc:: BPF_JEQ , libc:: BPF_K , libc:: BPF_RET ] .map ( | v | v as u16 )
872
) ;
873
}
ea56c56e Andy McCall 1 month ago ↶
874
}