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