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