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
57
fn self_hash ( ) -> & 'static str {
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 {
63
std:: fs:: read_to_string( " /proc/sys/kernel/osrelease" ) .map ( | s | s.trim ( ) .to_string ( ) ) .unwrap_or_else ( | _| " unknown" .into ( ) )
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,
ea56c56e Andy McCall 1 month ago ↶
289
} )
290
}
291
292
293
pub fn disk_dir ( scratch : & Path, n : usize ) -> PathBuf {
294
scratch.join ( format! ( " mount-{n} " ) )
295
}
296
297
298
const FAILED : & str = " \u{1e} F " ;
299
300
fn lines ( r : impl Read, stdout : bool , tx : mpsc:: Sender< ( bool , Vec< u8 > ) > ) {
301
let mut r = BufReader:: new( r) ;
302
let mut buf = Vec :: new( ) ;
303
loop {
304
buf.clear ( ) ;
305
306
match ( & mut r) .take ( 16 * 1024 ) .read_until ( b ' \n ' , & mut buf) {
307
Ok ( 0 ) | Err ( _ ) => break ,
308
Ok ( _ ) => {
309
if buf.last ( ) = = Some ( & b ' \n ' ) {
310
buf.pop ( ) ;
311
}
312
if tx.send ( ( stdout, buf.clone ( ) ) ) .is_err ( ) {
313
break ;
314
}
315
}
316
}
317
}
318
}
319
320
321
322
323
324
fn unpacked ( home : & Home, hash : & str , image : & Path) -> Result< PathBuf> {
325
let trees = home.root.join ( " trees" ) ;
326
let dir = trees.join ( hash) ;
327
if dir.is_dir ( ) {
328
return Ok ( dir) ;
329
}
330
std:: fs:: create_dir_all( & trees) ? ;
331
let nanos = std:: time:: SystemTime:: now( ) .duration_since ( std:: time:: UNIX_EPOCH ) .map ( | d | d.subsec_nanos ( ) ) .unwrap_or ( 0 ) ;
332
let tmp = trees.join ( format! ( " .{hash} .{} .{nanos} " , std:: process:: id( ) ) ) ;
333
std:: fs:: create_dir_all( & tmp) ? ;
334
let out = std:: process:: Command:: new( " debugfs" )
335
.arg ( " -R" )
336
.arg ( format! ( " rdump / \" {} \" " , tmp.display ( ) ) )
337
.arg ( image)
338
.output ( )
339
.context ( " run debugfs (e2fsprogs) to unpack the rootfs" ) ? ;
340
let empty = std:: fs:: read_dir( & tmp) .map ( | mut d | d.next ( ) .is_none ( ) ) .unwrap_or ( true ) ;
341
if ! out.status.success ( ) | | empty {
342
let _ = crate :: workspace:: remove_tree( & tmp) ;
343
bail! ( " unpacking rootfs {}: {}" , & hash[ .. 12. min ( hash.len ( ) ) ] , String :: from_utf8_lossy( & out.stderr) .trim ( ) ) ;
344
}
345
match std:: fs:: rename( & tmp, & dir) {
346
Ok ( ( ) ) => Ok ( dir) ,
347
348
Err ( _ ) if dir.is_dir ( ) => {
349
let _ = crate :: workspace:: remove_tree( & tmp) ;
350
Ok ( dir)
351
}
352
Err ( e) => Err ( e) .with_context ( | | format! ( " keep the unpacked rootfs at {} " , dir.display ( ) ) ) ,
353
}
354
}
355
356
357
358
359
360
361
362
# [ derive ( Clone ) ]
363
pub struct Cgroups {
364
base : PathBuf,
365
}
366
367
368
const MAIN_LEAF : & str = " flow-main" ;
369
370
impl Cgroups {
371
pub fn delegated ( ) -> std:: result:: Result< Cgroups, String> {
372
static ONCE : OnceLock< std:: result:: Result< Cgroups, String> > = OnceLock:: new( ) ;
373
ONCE .get_or_init ( Self :: set_up) .clone ( )
374
}
375
376
fn set_up ( ) -> std:: result:: Result< Cgroups, String> {
377
let own = std:: fs:: read_to_string( " /proc/self/cgroup" ) .map_err ( | e | format! ( " read /proc/self/cgroup: {e} " ) ) ? ;
378
let rel = own
379
.lines ( )
380
.find_map ( | l | l.strip_prefix ( " 0::" ) )
381
.ok_or ( " this machine does not use cgroup v2 (the unified hierarchy)" ) ?
382
.trim ( )
383
.to_string ( ) ;
384
let mut base = Path:: new( " /sys/fs/cgroup" ) .join ( rel.trim_start_matches ( ' /' ) ) ;
385
if base.file_name ( ) .is_some_and ( | n | n = = MAIN_LEAF ) {
386
base.pop ( ) ;
387
}
388
let controllers = std:: fs:: read_to_string( base.join ( " cgroup.controllers" ) ) .unwrap_or_default ( ) ;
389
let has = | c : & str | controllers.split_whitespace ( ) .any ( | x | x = = c ) ;
390
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 …`" ;
391
if ! has ( " cpu" ) | | ! has ( " memory" ) {
392
return Err ( format! ( " the cgroup {} has no cpu and memory controllers to give jobs; {delegate_hint} " , base.display ( ) ) ) ;
393
}
394
let writable =
395
| 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 ) ;
396
if ! writable ( & base) | | ! writable ( & base.join ( " cgroup.subtree_control" ) ) | | ! writable ( & base.join ( " cgroup.procs" ) ) {
397
return Err ( format! ( " this runner may not manage its cgroup {} ; {delegate_hint} " , base.display ( ) ) ) ;
398
}
399
400
401
let me = std:: process:: id( ) .to_string ( ) ;
402
let others = std:: fs:: read_to_string( base.join ( " cgroup.procs" ) ) .unwrap_or_default ( ) ;
403
if others.lines ( ) .any ( | p | p.trim ( ) ! = me ) {
404
return Err ( format! ( " the cgroup {} holds other processes; {delegate_hint} " , base.display ( ) ) ) ;
405
}
406
let leaf = base.join ( MAIN_LEAF ) ;
407
if ! leaf.is_dir ( ) {
408
std:: fs:: create_dir( & leaf) .map_err ( | e | format! ( " make {} : {e} " , leaf.display ( ) ) ) ? ;
409
}
410
std:: fs:: write( leaf.join ( " cgroup.procs" ) , " 0" ) .map_err ( | e | format! ( " move into {} : {e} " , leaf.display ( ) ) ) ? ;
411
let want: Vec< & str > = [ " cpu" , " memory" , " pids" ] .into_iter ( ) .filter ( | c | has ( c) ) .collect ( ) ;
412
let enable = want.iter ( ) .map ( | c | format! ( " +{c} " ) ) .collect:: < Vec< _ > > ( ) .join ( " " ) ;
413
std:: fs:: write( base.join ( " cgroup.subtree_control" ) , & enable)
414
.map_err ( | e | format! ( " enable {enable} in {} : {e} (another process may still be in it; {delegate_hint} )" , base.display ( ) ) ) ? ;
415
Ok ( Cgroups { base } )
416
}
417
418
419
pub fn job ( & self , name : & str , memory_mib : u32 , cpus : u32 ) -> Result< JobGroup> {
420
let dir = self .base.join ( name) ;
421
if dir.is_dir ( ) {
422
423
drop ( JobGroup { dir: dir.clone ( ) } ) ;
424
}
425
std:: fs:: create_dir( & dir) .with_context ( | | format! ( " make the job's cgroup {} " , dir.display ( ) ) ) ? ;
426
let group = JobGroup { dir } ;
427
let set = | file : & str , value : String| {
428
std:: fs:: write( group.dir.join ( file) , & value) .with_context ( | | format! ( " set {file} to {value} in {} " , group.dir.display ( ) ) )
429
} ;
430
set ( " memory.max" , ( u64 :: from( memory_mib) < < 20 ) .to_string ( ) ) ? ;
431
if group.dir.join ( " memory.swap.max" ) .exists ( ) {
432
set ( " memory.swap.max" , " 0" .into ( ) ) ? ;
433
}
434
set ( " cpu.max" , format! ( " {} 100000" , u64 :: from( cpus.max ( 1 ) ) * 100_000 ) ) ? ;
435
Ok ( group)
436
}
437
}
438
439
440
pub struct JobGroup {
441
dir : PathBuf,
442
}
443
444
impl JobGroup {
445
fn kill ( & self ) {
446
let _ = std:: fs:: write( self .dir.join ( " cgroup.kill" ) , " 1" ) ;
447
}
448
449
fn out_of_memory ( & self ) -> bool {
450
std:: fs:: read_to_string( self .dir.join ( " memory.events" ) )
451
.ok ( )
452
.and_then ( | s | s.lines ( ) .find_map ( | l | l.strip_prefix ( " oom_kill " ) ) .and_then ( | n | n.trim ( ) .parse:: < u64 > ( ) .ok ( ) ) )
453
.is_some_and ( | n | n > 0 )
454
}
455
}
456
457
impl Drop for JobGroup {
458
fn drop ( & mut self ) {
459
self .kill ( ) ;
460
for _ in 0 .. 100 {
461
if std:: fs:: remove_dir( & self .dir) .is_ok ( ) | | ! self .dir.exists ( ) {
462
return ;
463
}
464
std:: thread:: sleep( Duration:: from_millis( 50 ) ) ;
465
}
466
eprintln! ( " flow: could not remove the cgroup {}" , self .dir.display ( ) ) ;
467
}
468
}
469
470
471
472
473
474
pub fn sandbox_main ( spec_path : & Path) -> i32 {
475
match helper ( spec_path) {
476
Ok ( code) => code,
477
Err ( e) => {
478
eprintln! ( " {FAILED}{e}" ) ;
479
125
480
}
481
}
482
}
483
484
fn helper ( spec_path : & Path) -> std:: result:: Result< i32 , String> {
485
let spec: Spec = serde_json:: from_slice( & std:: fs:: read( spec_path) .map_err ( | e | format! ( " read the spec: {e} " ) ) ? )
486
.map_err ( | e | format! ( " parse the spec: {e} " ) ) ? ;
487
let _ = std:: fs:: remove_file( spec_path) ;
488
if spec.argv.is_empty ( ) {
489
return Err ( " empty argv" .into ( ) ) ;
490
}
491
std:: fs:: write( spec.cgroup.join ( " cgroup.procs" ) , " 0" ) .map_err ( | e | format! ( " join the job's cgroup: {e} " ) ) ? ;
492
let ( uid, gid) = unsafe { ( libc:: getuid( ) , libc:: getgid( ) ) } ;
493
enter_namespaces (
494
libc:: CLONE_NEWUSER | libc:: CLONE_NEWNS | libc:: CLONE_NEWPID | libc:: CLONE_NEWNET | libc:: CLONE_NEWIPC | libc:: CLONE_NEWUTS ,
495
uid,
496
gid,
497
) ? ;
498
499
500
let pid = unsafe { libc:: fork( ) } ;
501
if pid < 0 {
502
return Err ( format! ( " fork: {} " , std:: io:: Error:: last_os_error( ) ) ) ;
503
}
504
if pid = = 0 {
505
let code = match init ( & spec) {
506
Ok ( code) => code,
507
Err ( e) => {
508
eprintln! ( " {FAILED}{e}" ) ;
509
125
510
}
511
} ;
512
unsafe { libc:: _exit( code) } ;
513
}
514
Ok ( wait_for ( pid) )
515
}
516
517
518
519
fn wait_for ( pid : libc:: pid_t) -> i32 {
520
let mut status = 0 ;
521
loop {
522
let r = unsafe { libc:: waitpid( - 1 , & mut status, 0 ) } ;
523
if r = = pid {
524
break ;
525
}
526
if r < 0 & & std:: io:: Error:: last_os_error( ) .raw_os_error ( ) ! = Some ( libc:: EINTR ) {
527
return 125 ;
528
}
529
}
530
if libc:: WIFEXITED ( status) {
531
libc:: WEXITSTATUS ( status)
532
} else {
533
128 + libc:: WTERMSIG ( status)
534
}
535
}
536
537
538
539
fn enter_namespaces ( flags : libc:: c_int, uid : u32 , gid : u32 ) -> std:: result:: Result< ( ) , String> {
540
if unsafe { libc:: unshare( flags) } ! = 0 {
541
return Err ( format! ( " unshare: {} " , std:: io:: Error:: last_os_error( ) ) ) ;
542
}
543
let write = | file : & str , body : String| std:: fs:: write( file, body) .map_err ( | e | format! ( " write {file} : {e} " ) ) ;
544
write ( " /proc/self/setgroups" , " deny" .into ( ) ) ? ;
545
write ( " /proc/self/uid_map" , format! ( " {JOB_ID} {uid} 1\n " ) ) ? ;
546
write ( " /proc/self/gid_map" , format! ( " {JOB_ID} {gid} 1\n " ) )
547
}
548
549
550
fn init ( spec : & Spec) -> std:: result:: Result< i32 , String> {
551
552
unsafe { libc:: prctl( libc:: PR_SET_PDEATHSIG , libc:: SIGKILL ) } ;
553
let nosuid = libc:: MS_NOSUID | libc:: MS_NODEV ;
554
mount ( " none" , " /" , " " , libc:: MS_REC | libc:: MS_PRIVATE , " " ) ? ;
555
let root = spec.root.to_str ( ) .ok_or ( " the root path is not UTF-8" ) ? ;
556
let at = | p : & str | format! ( " {root} {p} " ) ;
557
bind ( spec.rootfs.to_str ( ) .ok_or ( " the rootfs path is not UTF-8" ) ? , root) ? ;
558
for d in [ " /src" , " /tmp" , " /proc" , " /dev" ] {
559
if ! Path:: new( & at ( d) ) .is_dir ( ) {
560
return Err ( format! ( " the image has no {d} directory" ) ) ;
561
}
562
}
563
remount_bind ( root, libc:: MS_RDONLY | nosuid) ? ;
564
bind ( spec.workspace.to_str ( ) .ok_or ( " the workspace path is not UTF-8" ) ? , & at ( " /src" ) ) ? ;
565
mount ( " tmpfs" , & at ( " /tmp" ) , " tmpfs" , nosuid, " mode=1777" ) ? ;
566
mount ( " proc" , & at ( " /proc" ) , " proc" , nosuid | libc:: MS_NOEXEC , " " ) ? ;
567
568
mount ( " tmpfs" , & at ( " /dev" ) , " tmpfs" , libc:: MS_NOSUID | libc:: MS_NOEXEC , " mode=755" ) ? ;
569
for d in [ " null" , " zero" , " full" , " random" , " urandom" ] {
570
let target = at ( & format! ( " /dev/{d} " ) ) ;
571
std:: fs:: File:: create( & target) .map_err ( | e | format! ( " make {target} : {e} " ) ) ? ;
572
bind ( & format! ( " /dev/{d} " ) , & target) ? ;
573
}
574
dev_links ( & at ( " /dev" ) ) ? ;
575
std:: fs:: create_dir( at ( " /dev/shm" ) ) .map_err ( | e | format! ( " make /dev/shm: {e} " ) ) ? ;
576
mount ( " tmpfs" , & at ( " /dev/shm" ) , " tmpfs" , nosuid, " mode=1777" ) ? ;
577
if Path:: new( & at ( " /run" ) ) .is_dir ( ) {
578
mount ( " tmpfs" , & at ( " /run" ) , " tmpfs" , nosuid, " mode=755" ) ? ;
579
}
580
581
for m in & spec.mounts {
582
let target = at ( & m.target) ;
583
std:: fs:: create_dir_all( & target) .map_err ( | e | format! ( " make {} : {e} " , m.target) ) ? ;
584
match & m.dir {
585
Some ( dir) => bind ( dir.to_str ( ) .ok_or ( " a mount path is not UTF-8" ) ? , & target) ? ,
586
None => mount ( " tmpfs" , & target, " tmpfs" , nosuid, & format! ( " size={} m,mode=755" , m.size_mib) ) ? ,
587
}
588
}
589
590
if ! spec.secrets.is_empty ( ) {
591
if ! Path:: new( & at ( " /run" ) ) .is_dir ( ) {
592
return Err ( " the image has no /run, where secrets are mounted" .into ( ) ) ;
593
}
594
let dir = at ( " /run/secrets" ) ;
595
std:: fs:: create_dir_all( & dir) .map_err ( | e | format! ( " mkdir {dir} : {e} " ) ) ? ;
596
mount ( " tmpfs" , & dir, " tmpfs" , nosuid | libc:: MS_NOEXEC , " size=1m,mode=700" ) ? ;
597
for ( path, value) in & spec.secrets {
598
let Some ( rel) = path.strip_prefix ( " /run/secrets/" ) else {
599
return Err ( format! ( " secret path {path:?} is not under /run/secrets/" ) ) ;
600
} ;
601
let full = format! ( " {dir} /{rel} " ) ;
602
if let Some ( parent) = Path:: new( & full) .parent ( ) {
603
std:: fs:: create_dir_all( parent) .map_err ( | e | format! ( " mkdir for {path} : {e} " ) ) ? ;
604
}
605
std:: fs:: write( & full, value.as_bytes ( ) ) .map_err ( | e | format! ( " write {path} : {e} " ) ) ? ;
606
std:: fs:: set_permissions( & full, std:: os:: unix:: fs:: PermissionsExt:: from_mode( 0o400 ) )
607
.map_err ( | e | format! ( " chmod {path} : {e} " ) ) ? ;
608
}
609
}
610
let host = b " flow" ;
611
unsafe { libc:: sethostname( host.as_ptr ( ) as * const libc:: c_char , host.len ( ) ) } ;
612
if spec.loopback {
613
loopback_up ( ) ? ;
614
}
615
616
617
std:: env:: set_current_dir( root) .map_err ( | e | format! ( " cd {root} : {e} " ) ) ? ;
618
let dot = c" ." ;
619
if unsafe { libc:: syscall( libc:: SYS_pivot_root, dot.as_ptr ( ) , dot.as_ptr ( ) ) } ! = 0 {
620
return Err ( format! ( " pivot_root: {} " , std:: io:: Error:: last_os_error( ) ) ) ;
621
}
622
if unsafe { libc:: umount2( dot.as_ptr ( ) , libc:: MNT_DETACH ) } ! = 0 {
623
return Err ( format! ( " detach the old root: {} " , std:: io:: Error:: last_os_error( ) ) ) ;
624
}
625
std:: env:: set_current_dir( " /src" ) .map_err ( | e | format! ( " cd /src: {e} " ) ) ? ;
626
627
let vars = job_env ( std:: fs:: read_to_string( " /etc/flow/env" ) .ok ( ) .as_deref ( ) , & spec.env) ;
628
let path = vars.get ( " PATH" ) .map ( String :: as_str) .unwrap_or ( PATH ) ;
629
let program =
630
cstr ( & resolve_program ( & spec.argv[ 0 ] , path) .ok_or_else ( | | format! ( " cannot run {:?} : not found in {path} " , spec.argv[ 0 ] ) ) ? ) ? ;
631
let argv: Vec< CString> = spec.argv.iter ( ) .map ( | a | cstr ( a) ) .collect:: < Result< _ , _ > > ( ) ? ;
632
let env: Vec< CString> = vars.iter ( ) .map ( | ( k , v ) | cstr ( & format! ( " {k} ={v} " ) ) ) .collect:: < Result< _ , _ > > ( ) ? ;
633
let mut writable: Vec< String> = [ " /src" , " /tmp" , " /dev" ] .into_iter ( ) .map ( String :: from) .collect ( ) ;
634
writable.extend ( spec.mounts.iter ( ) .map ( | m | m.target.clone ( ) ) ) ;
635
let pid = unsafe { libc:: fork( ) } ;
636
if pid < 0 {
637
return Err ( format! ( " fork: {} " , std:: io:: Error:: last_os_error( ) ) ) ;
638
}
639
if pid = = 0 {
640
641
let confined = ( | | {
642
unsafe {
643
let null = libc:: open( c" /dev/null" .as_ptr ( ) , libc:: O_RDONLY ) ;
644
libc:: dup2( null, 0 ) ;
645
libc:: setsid( ) ;
646
}
647
no_new_privs ( ) ? ;
648
landlock ( & writable) ? ;
649
seccomp ( DENIED )
650
} ) ( ) ;
651
unsafe {
652
if let Err ( e) = confined {
653
let msg = format! ( " {FAILED} {e} \n " ) ;
654
libc:: write( 2 , msg.as_ptr ( ) as * const libc:: c_void , msg.len ( ) ) ;
655
libc:: _exit( 125 ) ;
656
}
657
let mut a: Vec< * const libc:: c_char > = argv.iter ( ) .map ( | c | c.as_ptr ( ) ) .collect ( ) ;
658
a.push ( std:: ptr:: null( ) ) ;
659
let mut e: Vec< * const libc:: c_char > = env.iter ( ) .map ( | c | c.as_ptr ( ) ) .collect ( ) ;
660
e.push ( std:: ptr:: null( ) ) ;
661
libc:: execve( program.as_ptr ( ) , a.as_ptr ( ) , e.as_ptr ( ) ) ;
662
let msg = format! ( " flow: cannot run {:?} : {} \n " , spec.argv[ 0 ] , std:: io:: Error:: last_os_error( ) ) ;
663
libc:: write( 2 , msg.as_ptr ( ) as * const libc:: c_void , msg.len ( ) ) ;
664
libc:: _exit( 127 ) ;
665
}
666
}
667
Ok ( wait_for ( pid) )
668
}
669
670
fn cstr ( s : & str ) -> std:: result:: Result< CString, String> {
671
CString:: new( s) .map_err ( | _| format! ( " {s:?} contains NUL" ) )
672
}
673
674
fn mount ( src : & str , target : & str , fstype : & str , flags : libc:: c_ulong, data : & str ) -> std:: result:: Result< ( ) , String> {
675
let ( s, t, f, d) = ( cstr ( src) ? , cstr ( target) ? , cstr ( fstype) ? , cstr ( data) ? ) ;
676
let fstype = if fstype.is_empty ( ) { std:: ptr:: null( ) } else { f.as_ptr ( ) } ;
677
let data = if data.is_empty ( ) { std:: ptr:: null( ) } else { d.as_ptr ( ) as * const libc:: c_void } ;
678
if unsafe { libc:: mount( s.as_ptr ( ) , t.as_ptr ( ) , fstype, flags, data) } ! = 0 {
679
return Err ( format! ( " mount {src} on {target} : {} " , std:: io:: Error:: last_os_error( ) ) ) ;
680
}
681
Ok ( ( ) )
682
}
683
684
fn bind ( src : & str , target : & str ) -> std:: result:: Result< ( ) , String> {
685
mount ( src, target, " " , libc:: MS_BIND | libc:: MS_REC , " " )
686
}
687
688
689
690
fn remount_bind ( target : & str , flags : libc:: c_ulong) -> std:: result:: Result< ( ) , String> {
691
let t = cstr ( target) ? ;
692
let mut st: libc:: statvfs = unsafe { std:: mem:: zeroed( ) } ;
693
if unsafe { libc:: statvfs( t.as_ptr ( ) , & mut st) } ! = 0 {
694
return Err ( format! ( " statvfs {target} : {} " , std:: io:: Error:: last_os_error( ) ) ) ;
695
}
696
let kept = [
697
( libc:: ST_NOSUID , libc:: MS_NOSUID ) ,
698
( libc:: ST_NODEV , libc:: MS_NODEV ) ,
699
( libc:: ST_NOEXEC , libc:: MS_NOEXEC ) ,
700
( libc:: ST_NOATIME , libc:: MS_NOATIME ) ,
701
( libc:: ST_NODIRATIME , libc:: MS_NODIRATIME ) ,
702
( libc:: ST_RELATIME , libc:: MS_RELATIME ) ,
703
]
704
.into_iter ( )
705
.filter ( | ( st_flag , _) | st.f_flag & * st_flag ! = 0 )
706
.fold ( 0 , | acc , ( _, ms ) | acc | ms ) ;
707
mount ( " none" , target, " " , libc:: MS_BIND | libc:: MS_REMOUNT | flags | kept, " " )
708
}
709
710
fn no_new_privs ( ) -> std:: result:: Result< ( ) , String> {
711
if unsafe { libc:: prctl( libc:: PR_SET_NO_NEW_PRIVS , 1 , 0 , 0 , 0 ) } ! = 0 {
712
return Err ( format! ( " no_new_privs: {} " , std:: io:: Error:: last_os_error( ) ) ) ;
713
}
714
Ok ( ( ) )
715
}
716
717
718
719
const LANDLOCK_CREATE_RULESET_VERSION : libc:: c_uint = 1 ;
720
const LANDLOCK_RULE_PATH_BENEATH : libc:: c_int = 1 ;
721
const ACCESS_EXECUTE : u64 = 1 ;
722
const ACCESS_READ_FILE : u64 = 1 < < 2 ;
723
const ACCESS_READ_DIR : u64 = 1 < < 3 ;
724
725
const ACCESS_ABI1 : u64 = ( 1 < < 13 ) - 1 ;
726
const ACCESS_REFER : u64 = 1 < < 13 ;
727
const ACCESS_TRUNCATE : u64 = 1 < < 14 ;
728
729
# [ repr ( C ) ]
730
struct RulesetAttr {
731
handled_access_fs : u64 ,
732
}
733
734
# [ repr ( C, packed ) ]
735
struct PathBeneath {
736
allowed_access : u64 ,
737
parent_fd : i32 ,
738
}
739
740
741
fn landlock_abi ( ) -> i64 {
742
unsafe { libc:: syscall( libc:: SYS_landlock_create_ruleset, std:: ptr:: null:: < RulesetAttr> ( ) , 0 usize , LANDLOCK_CREATE_RULESET_VERSION ) }
743
}
744
745
746
fn landlock ( writable : & [String]) -> std:: result:: Result< ( ) , String> {
747
let abi = landlock_abi ( ) ;
748
if abi < 1 {
749
return Err ( " Landlock is not available" .into ( ) ) ;
750
}
751
let mut all = ACCESS_ABI1 ;
752
if abi > = 2 {
753
all | = ACCESS_REFER ;
754
}
755
if abi > = 3 {
756
all | = ACCESS_TRUNCATE ;
757
}
758
let attr = RulesetAttr { handled_access_fs: all } ;
759
let ruleset = unsafe { libc:: syscall( libc:: SYS_landlock_create_ruleset, & attr, std:: mem:: size_of:: < RulesetAttr> ( ) , 0 ) } ;
760
if ruleset < 0 {
761
return Err ( format! ( " landlock ruleset: {} " , std:: io:: Error:: last_os_error( ) ) ) ;
762
}
763
let ruleset = ruleset as libc:: c_int ;
764
let rule = | path : & str , access : u64 | -> std:: result:: Result< ( ) , String> {
765
let p = cstr ( path) ? ;
766
let fd = unsafe { libc:: open( p.as_ptr ( ) , libc:: O_PATH | libc:: O_CLOEXEC ) } ;
767
if fd < 0 {
768
return Err ( format! ( " open {path} for Landlock: {} " , std:: io:: Error:: last_os_error( ) ) ) ;
769
}
770
let beneath = PathBeneath { allowed_access: access, parent_fd: fd } ;
771
let rc = unsafe { libc:: syscall( libc:: SYS_landlock_add_rule, ruleset, LANDLOCK_RULE_PATH_BENEATH , & beneath, 0 ) } ;
772
unsafe { libc:: close( fd) } ;
773
if rc ! = 0 {
774
return Err ( format! ( " landlock rule for {path} : {} " , std:: io:: Error:: last_os_error( ) ) ) ;
775
}
776
Ok ( ( ) )
777
} ;
778
rule ( " /" , ACCESS_EXECUTE | ACCESS_READ_FILE | ACCESS_READ_DIR ) ? ;
779
for w in writable {
780
rule ( w, all) ? ;
781
}
782
if unsafe { libc:: syscall( libc:: SYS_landlock_restrict_self, ruleset, 0 ) } ! = 0 {
783
return Err ( format! ( " landlock restrict: {} " , std:: io:: Error:: last_os_error( ) ) ) ;
784
}
785
unsafe { libc:: close( ruleset) } ;
786
Ok ( ( ) )
787
}
788
789
790
791
# [ cfg ( target_arch = " x86_64" ) ]
792
const AUDIT_ARCH : u32 = 0xC000_003E ;
793
# [ cfg ( target_arch = " aarch64" ) ]
794
const AUDIT_ARCH : u32 = 0xC000_00B7 ;
795
# [ cfg ( target_arch = " riscv64" ) ]
796
const AUDIT_ARCH : u32 = 0xC000_00F3 ;
797
798
799
800
801
const DENIED : & [ libc:: c_long ] = & [
802
libc:: SYS_ptrace,
803
libc:: SYS_process_vm_readv,
804
libc:: SYS_process_vm_writev,
805
libc:: SYS_mount,
806
libc:: SYS_umount2,
807
libc:: SYS_pivot_root,
808
libc:: SYS_open_tree,
809
libc:: SYS_move_mount,
810
libc:: SYS_fsopen,
811
libc:: SYS_fsconfig,
812
libc:: SYS_fsmount,
813
libc:: SYS_fspick,
814
libc:: SYS_bpf,
815
libc:: SYS_perf_event_open,
816
libc:: SYS_keyctl,
817
libc:: SYS_add_key,
818
libc:: SYS_request_key,
819
libc:: SYS_init_module,
820
libc:: SYS_finit_module,
821
libc:: SYS_delete_module,
822
libc:: SYS_kexec_load,
823
libc:: SYS_swapon,
824
libc:: SYS_swapoff,
825
libc:: SYS_reboot,
826
libc:: SYS_io_uring_setup,
827
libc:: SYS_io_uring_enter,
828
libc:: SYS_io_uring_register,
829
] ;
830
831
832
833
834
fn seccomp ( denied : & [libc:: c_long]) -> std:: result:: Result< ( ) , String> {
835
const LD : u16 = ( libc:: BPF_LD | libc:: BPF_W | libc:: BPF_ABS ) as u16 ;
836
const JEQ : u16 = ( libc:: BPF_JMP | libc:: BPF_JEQ | libc:: BPF_K ) as u16 ;
837
const RET : u16 = ( libc:: BPF_RET | libc:: BPF_K ) as u16 ;
838
let op = | code : u16 , k : u32 , jt : u8 , jf : u8 | libc:: sock_filter { code, jt, jf, k } ;
839
let errno = | e : libc:: c_int| libc:: SECCOMP_RET_ERRNO | ( e as u32 & libc:: SECCOMP_RET_DATA ) ;
840
841
842
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 ) ] ;
843
# [ cfg ( target_arch = " x86_64" ) ]
844
{
845
846
const JGE : u16 = ( libc:: BPF_JMP | libc:: BPF_JGE | libc:: BPF_K ) as u16 ;
847
f.push ( op ( JGE , 0x4000_0000 , 0 , 1 ) ) ;
848
f.push ( op ( RET , errno ( libc:: EPERM ) , 0 , 0 ) ) ;
849
}
850
for nr in denied {
851
f.push ( op ( JEQ , * nr as u32 , 0 , 1 ) ) ;
852
f.push ( op ( RET , errno ( libc:: EPERM ) , 0 , 0 ) ) ;
853
}
854
f.push ( op ( JEQ , libc:: SYS_socket as u32 , 0 , 3 ) ) ;
855
f.push ( op ( LD , 16 , 0 , 0 ) ) ;
856
f.push ( op ( JEQ , libc:: AF_UNIX as u32 , 1 , 0 ) ) ;
857
f.push ( op ( RET , errno ( libc:: EAFNOSUPPORT ) , 0 , 0 ) ) ;
858
f.push ( op ( RET , libc:: SECCOMP_RET_ALLOW , 0 , 0 ) ) ;
859
let prog = libc:: sock_fprog { len: f.len ( ) as u16 , filter: f.as_mut_ptr ( ) } ;
860
if unsafe { libc:: syscall( libc:: SYS_seccomp, libc:: SECCOMP_SET_MODE_FILTER , 0 , & prog) } ! = 0 {
861
return Err ( format! ( " seccomp: {} " , std:: io:: Error:: last_os_error( ) ) ) ;
862
}
863
Ok ( ( ) )
864
}