-
Notifications
You must be signed in to change notification settings - Fork 31
/
UserNSRunner.jl
484 lines (413 loc) · 15.7 KB
/
UserNSRunner.jl
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
import Base: show
"""
UserNSRunner
A `UserNSRunner` represents an "execution context", an object that bundles all
necessary information to run commands within the container that contains
our crossbuild environment. Use `run()` to actually run commands within the
`UserNSRunner`, and [`runshell()`](@ref) as a quick way to get an interactive shell
within the crossbuild environment.
"""
mutable struct UserNSRunner <: Runner
sandbox_cmd::Cmd
env::Dict{String, String}
platform::Platform
shards::Vector{CompilerShard}
workspace_root::String
end
function UserNSRunner(workspace_root::String;
cwd = nothing,
workspaces::Vector = Pair[],
mappings::Vector = Pair[],
platform::Platform = platform_key_abi(),
extra_env=Dict{String, String}(),
verbose::Bool = false,
compiler_wrapper_path::String = mktempdir(),
src_name::AbstractString = "",
shards = nothing,
kwargs...)
global use_ccache, runner_override
# Check that our kernel is new enough to use this runner
kernel_version_check()
# Check to make sure we're not going to try and bindmount within an
# encrypted directory, as that triggers kernel bugs
check_encryption(workspace_root; verbose=verbose)
# Construct environment variables we'll use from here on out
envs = merge(platform_envs(platform, src_name; verbose=verbose), extra_env)
# JIT out some compiler wrappers, add it to our mounts
generate_compiler_wrappers!(platform; bin_path=compiler_wrapper_path, extract_kwargs(kwargs, (:compilers,:allow_unsafe_flags,:lock_microarchitecture))...)
push!(workspaces, compiler_wrapper_path => "/opt/bin")
# the workspace_root is always a workspace, and we always mount it first
insert!(workspaces, 1, workspace_root => "/workspace")
# If we're enabling ccache, then mount in a read-writeable volume at /root/.ccache
if use_ccache
if !isdir(ccache_dir())
mkpath(ccache_dir())
end
push!(workspaces, ccache_dir() => "/root/.ccache")
end
if isnothing(shards)
# Choose the shards we're going to mount
shards = choose_shards(platform; extract_kwargs(kwargs, (:preferred_gcc_version,:preferred_llvm_version,:bootstrap_list,:compilers))...)
end
# Construct sandbox command to look at the location it'll be mounted under
mpath = mount_path(shards[1], workspace_root)
sandbox_cmd = `$(mpath)/sandbox`
if verbose
sandbox_cmd = `$sandbox_cmd --verbose`
end
sandbox_cmd = `$sandbox_cmd --rootfs $(mpath)`
if cwd != nothing
sandbox_cmd = `$sandbox_cmd --cd $cwd`
end
# Add in read-only mappings and read-write workspaces
for (outside, inside) in workspaces
sandbox_cmd = `$sandbox_cmd --workspace $outside:$inside`
end
# Mount in compiler shards (excluding the rootfs shard)
for shard in shards[2:end]
mpath = mount_path(shard, workspace_root)
sandbox_cmd = `$sandbox_cmd --map $(mpath):$(map_target(shard))`
end
# Mount in externally-defined read-only mappings
for (outside, inside) in mappings
sandbox_cmd = `$sandbox_cmd --map $(outside):$(inside)`
end
# If runner_override is not yet set, let's probe to see if we can use
# unprivileged containers, and if we can't, switch over to privileged.
if runner_override == ""
if !probe_unprivileged_containers()
msg = strip("""
Unable to run unprivileged containers on this system!
This may be because your kernel does not support mounting overlay
filesystems within user namespaces. To work around this, we will
switch to using privileged containers. This requires the use of
sudo. To choose this automatically, set the BINARYBUILDER_RUNNER
environment variable to "privileged" before starting Julia.
""")
@warn(replace(msg, "\n" => " "))
runner_override = "privileged"
else
runner_override = "userns"
end
end
# Check to see if we need to run privileged containers.
if runner_override == "privileged"
# Next, prefer `sudo`, but allow fallback to `su`. Also, force-set
# our environmental mappings with sudo, because it is typically
# lost and forgotten. :(
if sudo_cmd() == `sudo`
sudo_envs = vcat([["-E", "$k=$(envs[k])"] for k in keys(envs)]...)
sandbox_cmd = `$(sudo_cmd()) $(Cmd(sudo_envs)) $(sandbox_cmd)`
else
sandbox_cmd = `$(sudo_cmd()) "$sandbox_cmd"`
end
end
# Finally, return the UserNSRunner in all its glory
return UserNSRunner(sandbox_cmd, envs, platform, shards, workspace_root)
end
function show(io::IO, x::UserNSRunner)
p = x.platform
# Displays as, e.g., Linux x86_64 (glibc) UserNSRunner
write(io, "$(typeof(p).name.name)", " ", arch(p), " ",
Sys.islinux(p) ? "($(p.libc)) " : "",
"UserNSRunner")
end
prompted_userns_run_privileged = false
function warn_priviledged()
global prompted_userns_run_privileged
if runner_override == "privileged" && !prompted_userns_run_privileged
@info("Running privileged container via `sudo`, may ask for your password:")
prompted_userns_run_privileged = true
end
end
function Base.run(ur::UserNSRunner, cmd, logger::IO = stdout; verbose::Bool = false, tee_stream=stdout)
warn_priviledged()
did_succeed = false
try
mount_shards(ur; verbose=verbose)
sandbox_cmd = `$(ur.sandbox_cmd) -- $(cmd)`
if cmd.ignorestatus
sandbox_cmd = ignorestatus(sandbox_cmd)
end
oc = OutputCollector(setenv(sandbox_cmd, ur.env); verbose=verbose, tee_stream=tee_stream)
did_succeed = wait(oc)
# First print out the command, then the output
println(logger, cmd)
print(logger, merge(oc))
finally
unmount_shards(ur; verbose=verbose)
end
# Return whether we succeeded or not
return did_succeed
end
function Base.read(ur::UserNSRunner, cmd; verbose=false)
warn_priviledged()
local oc
did_succeed = false
try
mount_shards(ur; verbose=verbose)
oc = OutputCollector(setenv(`$(ur.sandbox_cmd) -- $(cmd)`, ur.env))
did_succeed = wait(oc)
finally
unmount_shards(ur; verbose=verbose)
end
if !did_succeed
print(stderr, collect_stderr(oc))
return nothing
end
return collect_stdout(oc)
end
const AnyRedirectable = Union{Base.AbstractCmd, Base.TTY, IOStream}
function run_interactive(ur::UserNSRunner, user_cmd::Cmd; stdin = nothing, stdout = nothing, stderr = nothing, verbose::Bool = false)
warn_priviledged()
cmd = setenv(`$(ur.sandbox_cmd) -- $(user_cmd.exec)`, ur.env)
if user_cmd.ignorestatus
cmd = ignorestatus(cmd)
end
@debug("About to run: $(cmd)")
if stdin isa AnyRedirectable
cmd = pipeline(cmd, stdin=stdin)
end
if stdout isa AnyRedirectable
cmd = pipeline(cmd, stdout=stdout)
end
if stderr isa AnyRedirectable
cmd = pipeline(cmd, stderr=stderr)
end
try
mount_shards(ur; verbose=verbose)
if stdout isa IOBuffer
if !(stdin isa IOBuffer)
stdin = devnull
end
process = open(cmd, "r", stdin)
@async begin
while !eof(process)
write(stdout, read(process))
end
end
wait(process)
return success(process)
else
return success(run(cmd))
end
finally
unmount_shards(ur)
end
end
"""
uname()
On Linux systems, return the strings returned by the `uname()` function in libc
"""
function uname()
# Get libc and handle to uname
libcs = filter(x -> occursin("libc.so", x), dllist())
if isempty(libcs)
error("Could not find libc, unable to call uname()")
end
libc = dlopen(first(libcs))
uname_hdl = dlsym(libc, :uname)
# The uname struct can have wildly differing layouts; we take advantage
# of the fact that it is just a bunch of NULL-terminated strings laid out
# one after the other, and that it is (as best as I can tell) at maximum
# around 1.5KB long. We bump up to 2KB to be safe.
uname_struct = zeros(UInt8, 2048)
ccall(uname_hdl, Cint, (Ptr{UInt8},), uname_struct)
# Parse out all the strings embedded within this struct
strings = String[]
idx = 1
while idx < length(uname_struct)
# Extract string
new_string = unsafe_string(pointer(uname_struct, idx))
push!(strings, new_string)
idx += length(new_string) + 1
# Skip trailing zeros
while uname_struct[idx] == 0 && idx < length(uname_struct)
idx += 1
end
end
return strings
end
function kernel_version_check(;verbose::Bool = false)
# If we're not on Linux, just say everything is okay.
if !Sys.islinux()
return
end
uname_strings = try
uname()
catch e
if isa(e, InterruptException)
rethrow(e)
end
@warn("Unable to run `uname()` to check version number; assuming kernel version >= 3.18")
return
end
# Otherwise, get the strings, convert to VersionNumber
kernel_version = nothing
# Some distributions tack extra stuff onto the version number. We walk backwards
# from the end, searching for the longest string that we can extract a VersionNumber
# out of. We choose a minimum length of 5, as all kernel version numbers will be at
# least `X.Y.Z`.
for end_idx in length(uname_strings[3]):-1:5
try
kernel_version = VersionNumber(uname_strings[3][1:end_idx])
break
catch e
if isa(e, InterruptException)
rethrow(e)
end
end
end
# If we were unable to parse any part of the version number, then warn and exit.
if kernel_version === nothing
@warn("Unable to check version number; assuming kernel version >= 3.18")
return
end
# Otherwise, we have a kernel version and if it's too old, we should freak out.
if kernel_version < v"3.18"
error("Kernel version too old: detected $(kernel_version), need at least 3.18!")
end
if verbose
@info("Parsed kernel version \"$(kernel_version)\"")
end
end
function probe_unprivileged_containers(;verbose::Bool=false)
# Ensure we're not about to make fools of ourselves by trying to mount an
# encrypted directory, which triggers kernel bugs. :(
check_encryption(tempdir())
function test_sandbox(; verbose=verbose, workspace_tmpdir=false, map_shard=false)
# Choose and prepare our shards
shards = choose_shards(Linux(:x86_64; libc=:musl))
root_shard = first(shards)
other_shard = last(shards)
mktempdir() do tmpdir
mpath = mount(root_shard, tmpdir; verbose=verbose)
workspace_flag = []
if workspace_tmpdir
mkdir(joinpath(tmpdir, "workspace"))
touch(joinpath(tmpdir, "workspace", "foo"))
workspace_flag = `--workspace $(tmpdir)/workspace:/workspace`
end
map_flag = []
if map_shard
shard_mpath = mount(other_shard, tmpdir; verbose=verbose)
map_flag = `--map $(shard_mpath):$(map_target(other_shard))`
end
try
cmd = `$(mpath)/sandbox --rootfs $(mpath) $(workspace_flag) $(map_flag) -- /bin/sh -c "echo hello julia"`
oc = OutputCollector(cmd; tail_error=verbose)
return wait(oc) && merge(oc) == "hello julia\n"
finally
if map_shard
unmount(other_shard, tmpdir)
end
unmount(root_shard, tmpdir)
end
end
end
if verbose
@info("Probing for unprivileged container capability...")
end
flags = []
if !test_sandbox(;verbose=verbose)
if verbose
@warn("Unable to run simple unprivileged container test")
end
return false
else
if verbose
@info(" * Bare-bones test passed")
end
end
if !test_sandbox(;verbose=verbose, workspace_tmpdir=true)
if verbose
@warn("Unable to mount workspaces!")
end
return false
else
if verbose
@info(" * Workspacing test passed")
end
end
if !test_sandbox(;verbose=verbose, map_shard=true)
if verbose
@warn("Unable to map in a compiler shard!")
end
return false
else
if verbose
@info(" * Shard mapping test passed")
end
end
return true
end
"""
is_ecryptfs(path::AbstractString; verbose::Bool=false)
Checks to see if the given `path` (or any parent directory) is placed upon an
`ecryptfs` mount. This is known not to work on current kernels, see this bug
for more details: https://bugzilla.kernel.org/show_bug.cgi?id=197603
This method returns whether it is encrypted or not, and what mountpoint it used
to make that decision.
"""
function is_ecryptfs(path::AbstractString; verbose::Bool=false)
# Canonicalize `path` immediately, and if it's a directory, add a "/" so
# as to be consistent with the rest of this function
path = abspath(path)
if isdir(path)
path = abspath(path * "/")
end
if verbose
@info("Checking to see if $path is encrypted...")
end
# Get a listing of the current mounts. If we can't do this, just give up
if !isfile("/proc/mounts")
if verbose
@info("Couldn't open /proc/mounts, returning...")
end
return false, path
end
mounts = String(read("/proc/mounts"))
# Grab the fstype and the mountpoints
mounts = [split(m)[2:3] for m in split(mounts, "\n") if !isempty(m)]
# Canonicalize mountpoints now so as to dodge symlink difficulties
mounts = [(abspath(m[1]*"/"), m[2]) for m in mounts]
# Fast-path asking for a mountpoint directly (e.g. not a subdirectory)
direct_path = [m[1] == path for m in mounts]
parent = if any(direct_path)
mounts[findfirst(direct_path)]
else
# Find the longest prefix mount:
parent_mounts = [m for m in mounts if startswith(path, m[1])]
parent_mounts[argmax(map(m->length(m[1]), parent_mounts))]
end
# Return true if this mountpoint is an ecryptfs mount
return parent[2] == "ecryptfs", parent[1]
end
function check_encryption(workspace_root::AbstractString;
verbose::Bool = false)
# If we've explicitly allowed ecryptfs, just quit out immediately
global allow_ecryptfs
if allow_ecryptfs
return
end
msg = []
is_encrypted, mountpoint = is_ecryptfs(workspace_root; verbose=verbose)
if is_encrypted
push!(msg, replace(strip("""
Will not launch a user namespace runner within $(workspace_root), it
has been encrypted! Change your working directory to one outside of
$(mountpoint) and try again.
"""), "\n" => " "))
end
is_encrypted, mountpoint = is_ecryptfs(storage_dir(); verbose=verbose)
if is_encrypted
push!(msg, replace(strip("""
Cannot mount rootfs at $(storage_dir()), it has been encrypted! Change
your rootfs cache directory to one outside of $(mountpoint) by setting
the BINARYBUILDER_ROOTFS_DIR environment variable and try again.
"""), "\n" => " "))
end
if !isempty(msg)
error(join(msg, "\n"))
end
end