/home/runner/work/klsn/klsn/_build/test/cover/eunit/klsn_bwrap.html

1 -module(klsn_bwrap).
2
3 -export([
4 run/2
5 , open/2
6 , send/2
7 , send_eof/1
8 , stop/1
9 ]).
10
11 -export_type([
12 command/0
13 , opts/0
14 , open_opts/0
15 , bwrap_opt/0
16 , result/0
17 , stream/0
18 ]).
19
20 %% argv-style command where each element is a single argument.
21 -type command() :: [klsn:binstr()].
22
23 -type bwrap_opt() ::
24 help
25 | version
26 | {args, non_neg_integer()}
27 | unshare_all
28 | share_net
29 | unshare_user
30 | unshare_user_try
31 | unshare_ipc
32 | unshare_pid
33 | unshare_net
34 | unshare_uts
35 | unshare_cgroup
36 | unshare_cgroup_try
37 | {userns, non_neg_integer()}
38 | {userns2, non_neg_integer()}
39 | {pidns, non_neg_integer()}
40 | {uid, non_neg_integer()}
41 | {gid, non_neg_integer()}
42 | {hostname, klsn:binstr()}
43 | {chdir, klsn:binstr()}
44 | clearenv
45 | {setenv, klsn:binstr(), klsn:binstr()}
46 | {unsetenv, klsn:binstr()}
47 | {lock_file, klsn:binstr()}
48 | {sync_fd, non_neg_integer()}
49 | {bind, klsn:binstr(), klsn:binstr()}
50 | {bind_try, klsn:binstr(), klsn:binstr()}
51 | {dev_bind, klsn:binstr(), klsn:binstr()}
52 | {dev_bind_try, klsn:binstr(), klsn:binstr()}
53 | {ro_bind, klsn:binstr(), klsn:binstr()}
54 | {ro_bind_try, klsn:binstr(), klsn:binstr()}
55 | {bind_fd, non_neg_integer(), klsn:binstr()}
56 | {ro_bind_fd, non_neg_integer(), klsn:binstr()}
57 | {remount_ro, klsn:binstr()}
58 | {exec_label, klsn:binstr()}
59 | {file_label, klsn:binstr()}
60 | {proc, klsn:binstr()}
61 | {dev, klsn:binstr()}
62 | {tmpfs, klsn:binstr()}
63 | {mqueue, klsn:binstr()}
64 | {dir, klsn:binstr()}
65 | {file, non_neg_integer(), klsn:binstr()}
66 | {bind_data, non_neg_integer(), klsn:binstr()}
67 | {ro_bind_data, non_neg_integer(), klsn:binstr()}
68 | {symlink, klsn:binstr(), klsn:binstr()}
69 | {seccomp, non_neg_integer()}
70 | {add_seccomp, non_neg_integer()}
71 | {block_fd, non_neg_integer()}
72 | {userns_block_fd, non_neg_integer()}
73 | {info_fd, non_neg_integer()}
74 | {json_status_fd, non_neg_integer()}
75 | new_session
76 | die_with_parent
77 | as_pid_1
78 | {cap_add, klsn:binstr()}
79 | {cap_drop, klsn:binstr()}
80 | {perms, non_neg_integer()}
81 | {chmod, non_neg_integer(), klsn:binstr()}
82 .
83
84 %% Options for run/2.
85 %%
86 %% - bwrap: required list of bubblewrap options
87 %% - stdin: binary to write then close.
88 %% - timeout: timeout in milliseconds or infinity
89 -type opts() :: #{
90 bwrap := [bwrap_opt()]
91 , stdin => binary() % Unspecified: No stdin
92 , timeout => timeout() % Default: infinity
93 }.
94
95 %% Options for open/2.
96 %%
97 %% - bwrap: required list of bubblewrap options
98 %% - stdin: binary to write without closing stdin
99 -type open_opts() :: #{
100 bwrap := [bwrap_opt()]
101 , stdin => binary() % Unspecified: No stdin
102 }.
103
104 %% Result of run/2.
105 -type result() :: #{
106 exit_code := non_neg_integer()
107 , stdout := binary()
108 , stderr := binary()
109 }.
110
111 %% Stream handle returned from open/2.
112 -type stream() :: #{
113 os_pid := integer()
114 , exec_pid := pid()
115 }.
116
117 -spec run(command(), opts()) -> result().
118 run(Command, Opts) when is_list(Command), is_map(Opts) ->
119
:-(
ensure_erlexec_started(),
120
121
:-(
BwrapOpts = maps:get(bwrap, Opts),
122
:-(
Timeout = maps:get(timeout, Opts, infinity),
123
124
:-(
Argv0 = [
125 bwrap_executable()
126 | bwrap_opts_to_argv(BwrapOpts)
127 ],
128
:-(
Argv = Argv0 ++ [<<"--">>] ++ Command,
129
130
:-(
ExecOpts0 = [stdout, stderr, monitor],
131
:-(
{ExecOpts, MaybeStdin} = case klsn_map:lookup([stdin], Opts) of
132 none ->
133
:-(
{ExecOpts0, none};
134 {value, StdinBinary0} when is_binary(StdinBinary0) ->
135
:-(
{[stdin | ExecOpts0], {value, StdinBinary0}}
136 ; {value, _} ->
137
:-(
erlang:error(badarg, [Command, Opts])
138 end,
139
140
:-(
case exec:run(Argv, ExecOpts) of
141 {ok, _Pid, OsPid} ->
142
:-(
case MaybeStdin of
143 {value, StdinBinary1} ->
144
:-(
ok = send_stdin_chunked(OsPid, StdinBinary1),
145
:-(
ok = exec:send(OsPid, eof);
146 none ->
147
:-(
ok
148 end,
149
:-(
wait_result(OsPid, timeout_deadline(Timeout), [], []);
150 {error, Reason} ->
151
:-(
erlang:error(Reason, [Command, Opts])
152 end;
153 run(Command, Opts) ->
154
:-(
erlang:error(badarg, [Command, Opts]).
155
156 %% @doc
157 %% Start a bubblewrap sandbox and keep stdin/stdout open for streaming.
158 -spec open(command(), open_opts()) -> stream().
159 open(Command, Opts) when is_list(Command), is_map(Opts) ->
160
:-(
ensure_erlexec_started(),
161
162
:-(
BwrapOpts = maps:get(bwrap, Opts),
163
164
:-(
Argv0 = [
165 bwrap_executable()
166 | bwrap_opts_to_argv(BwrapOpts)
167 ],
168
:-(
Argv = Argv0 ++ [<<"--">>] ++ Command,
169
170
:-(
ExecOpts = [stdout, stderr, monitor, stdin],
171
:-(
MaybeStdin = case klsn_map:lookup([stdin], Opts) of
172 none ->
173
:-(
none;
174 {value, StdinBinary0} when is_binary(StdinBinary0) ->
175
:-(
{value, StdinBinary0};
176 {value, _} ->
177
:-(
erlang:error(badarg, [Command, Opts])
178 end,
179
180
:-(
case exec:run(Argv, ExecOpts) of
181 {ok, Pid, OsPid} ->
182
:-(
case MaybeStdin of
183 {value, StdinBinary1} ->
184
:-(
ok = send_stdin_chunked(OsPid, StdinBinary1);
185 none ->
186
:-(
ok
187 end,
188
:-(
#{
189 os_pid => OsPid
190 , exec_pid => Pid
191 };
192 {error, Reason} ->
193
:-(
erlang:error(Reason, [Command, Opts])
194 end;
195 open(Command, Opts) ->
196
:-(
erlang:error(badarg, [Command, Opts]).
197
198 %% @doc
199 %% Send a binary chunk to a streaming sandbox.
200 -spec send(stream(), binary()) -> ok.
201 send(#{os_pid := OsPid}, Data) when is_integer(OsPid), is_binary(Data) ->
202
:-(
ok = send_stdin_chunked(OsPid, Data);
203 send(Handle, Data) ->
204
:-(
erlang:error(badarg, [Handle, Data]).
205
206 %% @doc
207 %% Close stdin for a streaming sandbox.
208 -spec send_eof(stream()) -> ok.
209 send_eof(#{os_pid := OsPid}) when is_integer(OsPid) ->
210
:-(
ok = exec:send(OsPid, eof);
211 send_eof(Handle) ->
212
:-(
erlang:error(badarg, [Handle]).
213
214 %% @doc
215 %% Stop a streaming sandbox.
216 -spec stop(stream()) -> ok.
217 stop(#{os_pid := OsPid}) when is_integer(OsPid) ->
218
:-(
ok = exec:stop(OsPid);
219 stop(Handle) ->
220
:-(
erlang:error(badarg, [Handle]).
221
222 ensure_erlexec_started() ->
223
:-(
case whereis(exec) of
224 Pid when is_pid(Pid) ->
225
:-(
ok;
226 undefined ->
227
:-(
case os:getenv("SHELL") of
228 false ->
229
:-(
os:putenv("SHELL", find_shell());
230 _ ->
231
:-(
ok
232 end,
233
:-(
{ok, _} = application:ensure_all_started(erlexec)
234 end.
235
236 find_shell() ->
237
:-(
case os:find_executable("bash") of
238 false ->
239
:-(
case os:find_executable("sh") of
240 false ->
241
:-(
case filelib:is_file("/bin/bash") of
242 true ->
243
:-(
"/bin/bash";
244 false ->
245
:-(
"/bin/sh"
246 end;
247 Sh ->
248
:-(
Sh
249 end;
250 Bash ->
251
:-(
Bash
252 end.
253
254 send_stdin_chunked(_OsPid, <<>>) ->
255
:-(
ok;
256 send_stdin_chunked(OsPid, StdinBinary) when is_integer(OsPid), is_binary(StdinBinary) ->
257
:-(
ChunkSize = 60000,
258
:-(
send_stdin_chunked(OsPid, StdinBinary, ChunkSize, 0).
259
260 send_stdin_chunked(_OsPid, StdinBinary, _ChunkSize, Pos) when Pos >= byte_size(StdinBinary) ->
261
:-(
ok;
262 send_stdin_chunked(OsPid, StdinBinary, ChunkSize, Pos) ->
263
:-(
Remaining = byte_size(StdinBinary) - Pos,
264
:-(
Len = erlang:min(ChunkSize, Remaining),
265
:-(
Chunk = binary:part(StdinBinary, Pos, Len),
266
:-(
ok = exec:send(OsPid, Chunk),
267
:-(
send_stdin_chunked(OsPid, StdinBinary, ChunkSize, Pos + Len).
268
269 wait_result(OsPid, Deadline, StdoutAcc, StderrAcc) ->
270
:-(
Timeout = timeout_remaining(Deadline),
271
:-(
receive
272 {stdout, OsPid, Data} when is_binary(Data) ->
273
:-(
wait_result(OsPid, Deadline, [StdoutAcc, Data], StderrAcc);
274 {stderr, OsPid, Data} when is_binary(Data) ->
275
:-(
wait_result(OsPid, Deadline, StdoutAcc, [StderrAcc, Data]);
276 {'DOWN', OsPid, process, _Pid, {exit_status, ExitStatus}} when is_integer(ExitStatus) ->
277
:-(
ExitCode = exit_code(exec:status(ExitStatus)),
278
:-(
{StdoutAcc1, StderrAcc1} = drain_output(OsPid, StdoutAcc, StderrAcc),
279
:-(
#{
280 exit_code => ExitCode
281 , stdout => iolist_to_binary(StdoutAcc1)
282 , stderr => iolist_to_binary(StderrAcc1)
283 };
284 {'DOWN', OsPid, process, _Pid, normal} ->
285
:-(
{StdoutAcc1, StderrAcc1} = drain_output(OsPid, StdoutAcc, StderrAcc),
286
:-(
#{
287 exit_code => 0
288 , stdout => iolist_to_binary(StdoutAcc1)
289 , stderr => iolist_to_binary(StderrAcc1)
290 };
291 {'DOWN', OsPid, process, _Pid, Reason} ->
292
:-(
erlang:error(Reason, [OsPid])
293 after Timeout ->
294
:-(
exec:stop(OsPid),
295
:-(
drain_down(OsPid),
296
:-(
erlang:error(timeout, [OsPid])
297 end.
298
299 drain_output(OsPid, StdoutAcc, StderrAcc) ->
300
:-(
receive
301 {stdout, OsPid, Data} when is_binary(Data) ->
302
:-(
drain_output(OsPid, [StdoutAcc, Data], StderrAcc);
303 {stderr, OsPid, Data} when is_binary(Data) ->
304
:-(
drain_output(OsPid, StdoutAcc, [StderrAcc, Data])
305 after 0 ->
306
:-(
{StdoutAcc, StderrAcc}
307 end.
308
309 drain_down(OsPid) ->
310
:-(
receive
311 {stdout, OsPid, _} ->
312
:-(
drain_down(OsPid);
313 {stderr, OsPid, _} ->
314
:-(
drain_down(OsPid);
315 {'DOWN', OsPid, process, _Pid, _Reason} ->
316
:-(
ok
317 after 5000 ->
318
:-(
ok
319 end.
320
321 bwrap_executable() ->
322
:-(
case os:find_executable("bwrap") of
323 false ->
324
:-(
erlang:error(not_found, [bwrap]);
325 Path ->
326
:-(
unicode:characters_to_binary(Path)
327 end.
328
329 bwrap_opts_to_argv(Opts) when is_list(Opts) ->
330
:-(
lists:append(lists:map(fun bwrap_opt_to_argv/1, Opts));
331 bwrap_opts_to_argv(Other) ->
332
:-(
erlang:error(badarg, [Other]).
333
334 %% Strict option parsing: reject unknown atoms/tuples (catch typos).
335
:-(
bwrap_opt_to_argv(help) -> [flag(<<"help">>)];
336
:-(
bwrap_opt_to_argv(version) -> [flag(<<"version">>)];
337
:-(
bwrap_opt_to_argv({args, N}) when is_integer(N), N >= 0 -> [flag(<<"args">>), integer_to_binary(N)];
338
339
:-(
bwrap_opt_to_argv(unshare_all) -> [flag(<<"unshare-all">>)];
340
:-(
bwrap_opt_to_argv(share_net) -> [flag(<<"share-net">>)];
341
:-(
bwrap_opt_to_argv(unshare_user) -> [flag(<<"unshare-user">>)];
342
:-(
bwrap_opt_to_argv(unshare_user_try) -> [flag(<<"unshare-user-try">>)];
343
:-(
bwrap_opt_to_argv(unshare_ipc) -> [flag(<<"unshare-ipc">>)];
344
:-(
bwrap_opt_to_argv(unshare_pid) -> [flag(<<"unshare-pid">>)];
345
:-(
bwrap_opt_to_argv(unshare_net) -> [flag(<<"unshare-net">>)];
346
:-(
bwrap_opt_to_argv(unshare_uts) -> [flag(<<"unshare-uts">>)];
347
:-(
bwrap_opt_to_argv(unshare_cgroup) -> [flag(<<"unshare-cgroup">>)];
348
:-(
bwrap_opt_to_argv(unshare_cgroup_try) -> [flag(<<"unshare-cgroup-try">>)];
349
350
:-(
bwrap_opt_to_argv({userns, Fd}) when is_integer(Fd), Fd >= 0 -> [flag(<<"userns">>), integer_to_binary(Fd)];
351
:-(
bwrap_opt_to_argv({userns2, Fd}) when is_integer(Fd), Fd >= 0 -> [flag(<<"userns2">>), integer_to_binary(Fd)];
352
:-(
bwrap_opt_to_argv({pidns, Fd}) when is_integer(Fd), Fd >= 0 -> [flag(<<"pidns">>), integer_to_binary(Fd)];
353
:-(
bwrap_opt_to_argv({uid, Uid}) when is_integer(Uid), Uid >= 0 -> [flag(<<"uid">>), integer_to_binary(Uid)];
354
:-(
bwrap_opt_to_argv({gid, Gid}) when is_integer(Gid), Gid >= 0 -> [flag(<<"gid">>), integer_to_binary(Gid)];
355
356 bwrap_opt_to_argv({hostname, Hostname}) when is_binary(Hostname) ->
357
:-(
[flag(<<"hostname">>), Hostname];
358 bwrap_opt_to_argv({chdir, Path}) when is_binary(Path) ->
359
:-(
[flag(<<"chdir">>), Path];
360
361
:-(
bwrap_opt_to_argv(clearenv) -> [flag(<<"clearenv">>)];
362 bwrap_opt_to_argv({setenv, Key, Val}) when is_binary(Key), is_binary(Val) ->
363
:-(
[flag(<<"setenv">>), Key, Val];
364 bwrap_opt_to_argv({unsetenv, Key}) when is_binary(Key) ->
365
:-(
[flag(<<"unsetenv">>), Key];
366 bwrap_opt_to_argv({lock_file, Path}) when is_binary(Path) ->
367
:-(
[flag(<<"lock-file">>), Path];
368 bwrap_opt_to_argv({sync_fd, Fd}) when is_integer(Fd), Fd >= 0 ->
369
:-(
[flag(<<"sync-fd">>), integer_to_binary(Fd)];
370
371 bwrap_opt_to_argv({bind, Src, Dst}) when is_binary(Src), is_binary(Dst) ->
372
:-(
[flag(<<"bind">>), Src, Dst];
373 bwrap_opt_to_argv({bind_try, Src, Dst}) when is_binary(Src), is_binary(Dst) ->
374
:-(
[flag(<<"bind-try">>), Src, Dst];
375 bwrap_opt_to_argv({dev_bind, Src, Dst}) when is_binary(Src), is_binary(Dst) ->
376
:-(
[flag(<<"dev-bind">>), Src, Dst];
377 bwrap_opt_to_argv({dev_bind_try, Src, Dst}) when is_binary(Src), is_binary(Dst) ->
378
:-(
[flag(<<"dev-bind-try">>), Src, Dst];
379 bwrap_opt_to_argv({ro_bind, Src, Dst}) when is_binary(Src), is_binary(Dst) ->
380
:-(
[flag(<<"ro-bind">>), Src, Dst];
381 bwrap_opt_to_argv({ro_bind_try, Src, Dst}) when is_binary(Src), is_binary(Dst) ->
382
:-(
[flag(<<"ro-bind-try">>), Src, Dst];
383
384 bwrap_opt_to_argv({bind_fd, Fd, Path}) when is_integer(Fd), Fd >= 0, is_binary(Path) ->
385
:-(
[flag(<<"bind-fd">>), integer_to_binary(Fd), Path];
386 bwrap_opt_to_argv({ro_bind_fd, Fd, Path}) when is_integer(Fd), Fd >= 0, is_binary(Path) ->
387
:-(
[flag(<<"ro-bind-fd">>), integer_to_binary(Fd), Path];
388 bwrap_opt_to_argv({remount_ro, Path}) when is_binary(Path) ->
389
:-(
[flag(<<"remount-ro">>), Path];
390
391 bwrap_opt_to_argv({exec_label, Label}) when is_binary(Label) ->
392
:-(
[flag(<<"exec-label">>), Label];
393 bwrap_opt_to_argv({file_label, Label}) when is_binary(Label) ->
394
:-(
[flag(<<"file-label">>), Label];
395
396 bwrap_opt_to_argv({proc, Path}) when is_binary(Path) ->
397
:-(
[flag(<<"proc">>), Path];
398 bwrap_opt_to_argv({dev, Path}) when is_binary(Path) ->
399
:-(
[flag(<<"dev">>), Path];
400 bwrap_opt_to_argv({tmpfs, Path}) when is_binary(Path) ->
401
:-(
[flag(<<"tmpfs">>), Path];
402 bwrap_opt_to_argv({mqueue, Path}) when is_binary(Path) ->
403
:-(
[flag(<<"mqueue">>), Path];
404 bwrap_opt_to_argv({dir, Path}) when is_binary(Path) ->
405
:-(
[flag(<<"dir">>), Path];
406
407 bwrap_opt_to_argv({file, Fd, Path}) when is_integer(Fd), Fd >= 0, is_binary(Path) ->
408
:-(
[flag(<<"file">>), integer_to_binary(Fd), Path];
409 bwrap_opt_to_argv({bind_data, Fd, Path}) when is_integer(Fd), Fd >= 0, is_binary(Path) ->
410
:-(
[flag(<<"bind-data">>), integer_to_binary(Fd), Path];
411 bwrap_opt_to_argv({ro_bind_data, Fd, Path}) when is_integer(Fd), Fd >= 0, is_binary(Path) ->
412
:-(
[flag(<<"ro-bind-data">>), integer_to_binary(Fd), Path];
413 bwrap_opt_to_argv({symlink, Src, Dst}) when is_binary(Src), is_binary(Dst) ->
414
:-(
[flag(<<"symlink">>), Src, Dst];
415
416 bwrap_opt_to_argv({seccomp, Fd}) when is_integer(Fd), Fd >= 0 ->
417
:-(
[flag(<<"seccomp">>), integer_to_binary(Fd)];
418 bwrap_opt_to_argv({add_seccomp, Fd}) when is_integer(Fd), Fd >= 0 ->
419
:-(
[flag(<<"add-seccomp">>), integer_to_binary(Fd)];
420 bwrap_opt_to_argv({block_fd, Fd}) when is_integer(Fd), Fd >= 0 ->
421
:-(
[flag(<<"block-fd">>), integer_to_binary(Fd)];
422 bwrap_opt_to_argv({userns_block_fd, Fd}) when is_integer(Fd), Fd >= 0 ->
423
:-(
[flag(<<"userns-block-fd">>), integer_to_binary(Fd)];
424 bwrap_opt_to_argv({info_fd, Fd}) when is_integer(Fd), Fd >= 0 ->
425
:-(
[flag(<<"info-fd">>), integer_to_binary(Fd)];
426 bwrap_opt_to_argv({json_status_fd, Fd}) when is_integer(Fd), Fd >= 0 ->
427
:-(
[flag(<<"json-status-fd">>), integer_to_binary(Fd)];
428
429
:-(
bwrap_opt_to_argv(new_session) -> [flag(<<"new-session">>)];
430
:-(
bwrap_opt_to_argv(die_with_parent) -> [flag(<<"die-with-parent">>)];
431
:-(
bwrap_opt_to_argv(as_pid_1) -> [flag(<<"as-pid-1">>)];
432
433 bwrap_opt_to_argv({cap_add, Cap}) when is_binary(Cap) ->
434
:-(
[flag(<<"cap-add">>), Cap];
435 bwrap_opt_to_argv({cap_drop, Cap}) when is_binary(Cap) ->
436
:-(
[flag(<<"cap-drop">>), Cap];
437 bwrap_opt_to_argv({perms, Mode}) when is_integer(Mode), Mode >= 0 ->
438
:-(
[flag(<<"perms">>), integer_to_binary(Mode)];
439 bwrap_opt_to_argv({chmod, Mode, Path}) when is_integer(Mode), Mode >= 0, is_binary(Path) ->
440
:-(
[flag(<<"chmod">>), integer_to_binary(Mode), Path];
441 bwrap_opt_to_argv(Other) ->
442
:-(
erlang:error(badarg, [Other]).
443
444 flag(Name) when is_binary(Name) ->
445
:-(
<<"--", Name/binary>>.
446
447 timeout_deadline(infinity) ->
448
:-(
infinity;
449 timeout_deadline(Timeout) when is_integer(Timeout), Timeout >= 0 ->
450
:-(
monotonic_ms() + Timeout;
451 timeout_deadline(Other) ->
452
:-(
erlang:error(badarg, [Other]).
453
454 timeout_remaining(infinity) ->
455
:-(
infinity;
456 timeout_remaining(DeadlineMs) when is_integer(DeadlineMs) ->
457
:-(
Remaining = DeadlineMs - monotonic_ms(),
458
:-(
case Remaining > 0 of
459
:-(
true -> Remaining;
460
:-(
false -> 0
461 end.
462
463 monotonic_ms() ->
464
:-(
erlang:convert_time_unit(erlang:monotonic_time(), native, millisecond).
465
466 exit_code({status, Code}) when is_integer(Code), Code >= 0 ->
467
:-(
Code;
468 exit_code({signal, Signal, _CoreDump}) ->
469
:-(
128 + signal_to_int(Signal).
470
471 signal_to_int(Signal) when is_atom(Signal) ->
472
:-(
exec:signal_to_int(Signal);
473 signal_to_int(Signal) when is_integer(Signal) ->
474
:-(
Signal.
Line Hits Source