Skip to content

Commit fb19a28

Browse files
larsrc-googlecopybara-github
authored andcommitted
Add support for multiplex worker sandboxing. This sandboxing requires worker support.
Design doc: https://docs.google.com/document/d/1ncLW0hz6uDhNvci1dpzfEoifwTiNTqiBEm1vi-bIIRM/edit The multiplex worker process will run in a subdir "<mnemonic>-multiplex-workdir/<workspacename>" under the global execroot. Inside that directory, each request gets its own subdir "sandbox/<workerid>/<workspacename>". RELNOTES: Adds `--experimental_worker_multiplex_sandboxing` flag that controls whether to sandbox multiplex workers that support it. PiperOrigin-RevId: 419591728
1 parent 715c9fa commit fb19a28

28 files changed

Lines changed: 801 additions & 135 deletions

‎site/docs/creating-workers.md‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,9 @@ values indicate more verbose output. Passing the `--worker_verbose` flag to
5959
Bazel sets the `verbosity` field to 10, but smaller or larger values can be
6060
used manually for different amounts of output.
6161

62+
The optional `sandbox_dir` field is used only by workers that support
63+
[multiplex sandboxing](multiplex-worker.md).
64+
6265
### Work responses
6366

6467
A `WorkResponse` contains a request id, a zero or nonzero exit

‎site/docs/multiplex-worker.md‎

Lines changed: 28 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -66,14 +66,33 @@ A ruleset is encouraged to use multiplex workers if possible, to reduce memory
6666
pressure and improve performance. However, multiplex workers are not currently
6767
compatible with [dynamic execution](/dynamic-execution.html).
6868

69-
### Warning about rare bug
69+
## Multiplex sandboxing
7070

71-
Due to a rare bug, multiplex workers are currently not enabled by default.
72-
Occasionally, Bazel hangs indefinitely at the execution phase. If you see this
73-
behavior, stop the Bazel server and rerun. This delay is probably caused by
71+
Multiplex workers can be sandboxed by adding explicit support for it in the
72+
worker implementations. While singleplex worker sandboxing can be done by
73+
running each worker process in its own sandbox, multiplex workers share the
74+
process working directory between multiple parallel requests. To allow
75+
sandboxing of multiplex workers, the worker must support reading from and
76+
writing to a subdirectory specified in each request, instead of directly in
77+
its working directory.
7478

75-
* Multiplex workers waiting for responses from the worker process that never
76-
comes.
77-
* Incorrectly configured ruleset worker implementation where a thread dies or
78-
a race condition occurs. To counteract this, ensure the worker process
79-
returns responses in all circumstances.
79+
To support multiplex sandboxing, the worker must use the `sandbox_dir` field
80+
from the `WorkRequest` and use that as a prefix for all file reads and writes.
81+
While the `arguments` and `inputs` fields remain unchanged from an unsandboxed
82+
request, the actual inputs are relative to the `sandbox_dir`. The worker must
83+
translate file paths found in `arguments` and `inputs` to read from this
84+
modified path, and must also write all outputs relative to the `sandbox_dir`.
85+
This includes paths such as '.', as well as paths found in files specified
86+
in the arguments (e.g. ["argfile"](https://docs.oracle.com/javase/7/docs/technotes/tools/windows/javac.html#commandlineargfile) arguments).
87+
88+
Once a worker supports multiplex sandboxing, the ruleset can declare this
89+
support by adding `supports-multiplex-sandboxing` to the
90+
`execution_requirements` of an action. Bazel will then use multiplex sandboxing
91+
if the `--experimental_worker_multiplex_sandboxing` flag is passed, or if
92+
the worker is used with dynamic execution.
93+
94+
The worker files of a sandboxed multiplex worker are still relative to the
95+
working directory of the worker process. Thus, if a file is
96+
used both for running the worker and as an input, it must be specified both as
97+
an input in the flagfile argument as well as in `tools`, `executable`, or
98+
`runfiles`.

‎site/docs/persistent-workers.md‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -250,6 +250,11 @@ Even when using the input digests to guard against unwanted caching, sandboxed
250250
workers offer less strict sandboxing than a pure sandbox, because the tool may
251251
keep other internal state that has been affected by previous requests.
252252

253+
Multiplex workers can only be sandboxed if the worker implementation support it,
254+
and this sandboxing must be separately enabled with the
255+
`--experimental_worker_multiplex_sandboxing` flag. See more details in
256+
[the design doc](https://docs.google.com/document/d/1ncLW0hz6uDhNvci1dpzfEoifwTiNTqiBEm1vi-bIIRM/edit))
257+
253258
## Further reading <a name="further-reading"></a>
254259

255260
For more information on persistent workers, see:

‎src/main/java/com/google/devtools/build/lib/actions/ExecutionRequirements.java‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,8 @@ public String parseIfMatches(String tag) throws ValidationException {
162162

163163
public static final String SUPPORTS_WORKER_CANCELLATION = "supports-worker-cancellation";
164164

165+
public static final String SUPPORTS_MULTIPLEX_SANDBOXING = "supports-multiplex-sandboxing";
166+
165167
/** Denotes what the type of worker protocol the worker uses. */
166168
public enum WorkerProtocolFormat {
167169
JSON,

‎src/main/java/com/google/devtools/build/lib/actions/Spawns.java‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,15 @@ public static boolean supportsWorkerCancellation(Spawn spawn) {
101101
.equals(spawn.getExecutionInfo().get(ExecutionRequirements.SUPPORTS_WORKER_CANCELLATION));
102102
}
103103

104+
/**
105+
* Returns whether the {@link Spawn} supports sandboxing for multiplex workers through the {@code
106+
* WorkRequest.sandbox_dir} field.
107+
*/
108+
public static boolean supportsMultiplexSandboxing(Spawn spawn) {
109+
return "1"
110+
.equals(spawn.getExecutionInfo().get(ExecutionRequirements.SUPPORTS_MULTIPLEX_SANDBOXING));
111+
}
112+
104113
/**
105114
* Returns which worker protocol format a Spawn claims a persistent worker uses. Defaults to proto
106115
* if the protocol format is not specified.

‎src/main/java/com/google/devtools/build/lib/sandbox/AbstractContainerizingSandboxedSpawn.java‎

Lines changed: 1 addition & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@
1515
package com.google.devtools.build.lib.sandbox;
1616

1717

18-
import com.google.common.base.Preconditions;
1918
import com.google.common.collect.ImmutableList;
2019
import com.google.common.collect.ImmutableMap;
2120
import com.google.common.collect.ImmutableSet;
@@ -27,7 +26,6 @@
2726
import com.google.devtools.build.lib.vfs.Path;
2827
import com.google.devtools.build.lib.vfs.PathFragment;
2928
import java.io.IOException;
30-
import java.util.HashSet;
3129
import java.util.LinkedHashSet;
3230
import java.util.Set;
3331
import java.util.stream.Collectors;
@@ -115,7 +113,7 @@ public void createFileSystem() throws IOException {
115113
filterInputsAndDirsToCreate(inputsToCreate, dirsToCreate);
116114

117115
// Finally create what needs creating.
118-
createDirectories(dirsToCreate);
116+
SandboxHelpers.createDirectories(dirsToCreate, sandboxExecRoot, /* strict=*/ true);
119117
createInputs(inputsToCreate, inputs);
120118
inputs.materializeVirtualInputs(sandboxExecRoot);
121119
}
@@ -124,45 +122,6 @@ protected void filterInputsAndDirsToCreate(
124122
Set<PathFragment> inputsToCreate, LinkedHashSet<PathFragment> dirsToCreate)
125123
throws IOException {}
126124

127-
/**
128-
* Creates all directories needed for the sandbox.
129-
*
130-
* <p>No input can be a child of another input, because otherwise we might try to create a symlink
131-
* below another symlink we created earlier - which means we'd actually end up writing somewhere
132-
* in the workspace.
133-
*
134-
* <p>If all inputs were regular files, this situation could naturally not happen - but
135-
* unfortunately, we might get the occasional action that has directories in its inputs.
136-
*
137-
* <p>Creating all parent directories first ensures that we can safely create symlinks to
138-
* directories, too, because we'll get an IOException with EEXIST if inputs happen to be nested
139-
* once we start creating the symlinks for all inputs.
140-
*/
141-
void createDirectories(Iterable<PathFragment> dirsToCreate) throws IOException {
142-
Set<Path> knownDirectories = new HashSet<>();
143-
// Add sandboxExecRoot and it's parent -- all paths must fall under the parent of
144-
// sandboxExecRoot and we know that sandboxExecRoot exists. This stops the recursion in
145-
// createDirectoryAndParentsInSandboxRoot.
146-
knownDirectories.add(sandboxExecRoot);
147-
knownDirectories.add(sandboxExecRoot.getParentDirectory());
148-
149-
for (PathFragment path : dirsToCreate) {
150-
Preconditions.checkArgument(!path.isAbsolute(), path);
151-
if (path.containsUplevelReferences() && path.isMultiSegment()) {
152-
// Allow a single up-level reference to allow inputs from the siblings of the main
153-
// repository in the sandbox execution root, but forbid multiple up-level references.
154-
// PathFragment is normalized, so up-level references are guaranteed to be at the beginning.
155-
Preconditions.checkArgument(
156-
!PathFragment.containsUplevelReferences(path.getSegment(1)),
157-
"%s escapes the sandbox exec root.",
158-
path);
159-
}
160-
161-
SandboxHelpers.createDirectoryAndParentsInSandboxRoot(
162-
sandboxExecRoot.getRelative(path), knownDirectories, sandboxExecRoot);
163-
}
164-
}
165-
166125
/**
167126
* Creates all inputs needed for this spawn's sandbox.
168127
*

‎src/main/java/com/google/devtools/build/lib/sandbox/SandboxHelpers.java‎

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,11 @@
1919
import static com.google.devtools.build.lib.vfs.Dirent.Type.SYMLINK;
2020

2121
import com.google.auto.value.AutoValue;
22+
import com.google.common.base.Preconditions;
2223
import com.google.common.collect.ImmutableMap;
2324
import com.google.common.collect.ImmutableSet;
2425
import com.google.common.collect.Iterables;
26+
import com.google.common.collect.Maps;
2527
import com.google.common.flogger.GoogleLogger;
2628
import com.google.devtools.build.lib.actions.ActionInput;
2729
import com.google.devtools.build.lib.actions.Artifact;
@@ -309,6 +311,51 @@ public static void createDirectoryAndParentsInSandboxRoot(
309311
knownDirectories.add(path);
310312
}
311313

314+
/**
315+
* Creates all directories needed for the sandbox.
316+
*
317+
* <p>No input can be a child of another input, because otherwise we might try to create a symlink
318+
* below another symlink we created earlier - which means we'd actually end up writing somewhere
319+
* in the workspace.
320+
*
321+
* <p>If all inputs were regular files, this situation could naturally not happen - but
322+
* unfortunately, we might get the occasional action that has directories in its inputs.
323+
*
324+
* <p>Creating all parent directories first ensures that we can safely create symlinks to
325+
* directories, too, because we'll get an IOException with EEXIST if inputs happen to be nested
326+
* once we start creating the symlinks for all inputs.
327+
*
328+
* @param strict If true, absolute directories or directories with multiple up-level references
329+
* are disallowed, for stricter sandboxing.
330+
*/
331+
public static void createDirectories(
332+
Iterable<PathFragment> dirsToCreate, Path dir, boolean strict) throws IOException {
333+
Set<Path> knownDirectories = new HashSet<>();
334+
// Add sandboxExecRoot and it's parent -- all paths must fall under the parent of
335+
// sandboxExecRoot and we know that sandboxExecRoot exists. This stops the recursion in
336+
// createDirectoryAndParentsInSandboxRoot.
337+
knownDirectories.add(dir);
338+
knownDirectories.add(dir.getParentDirectory());
339+
340+
for (PathFragment path : dirsToCreate) {
341+
if (strict) {
342+
Preconditions.checkArgument(!path.isAbsolute(), path);
343+
if (path.containsUplevelReferences() && path.isMultiSegment()) {
344+
// Allow a single up-level reference to allow inputs from the siblings of the main
345+
// repository in the sandbox execution root, but forbid multiple up-level references.
346+
// PathFragment is normalized, so up-level references are guaranteed to be at the
347+
// beginning.
348+
Preconditions.checkArgument(
349+
!PathFragment.containsUplevelReferences(path.getSegment(1)),
350+
"%s escapes the sandbox exec root.",
351+
path);
352+
}
353+
}
354+
355+
createDirectoryAndParentsInSandboxRoot(dir.getRelative(path), knownDirectories, dir);
356+
}
357+
}
358+
312359
/** Wrapper class for the inputs of a sandbox. */
313360
public static final class SandboxInputs {
314361

@@ -397,6 +444,17 @@ public void materializeVirtualInputs(Path sandboxExecRoot) throws IOException {
397444
}
398445
}
399446

447+
/**
448+
* Returns a new SandboxInputs instance with only the inputs/symlinks listed in {@code allowed}
449+
* included.
450+
*/
451+
public SandboxInputs limitedCopy(Set<PathFragment> allowed) {
452+
return new SandboxInputs(
453+
Maps.filterKeys(files, allowed::contains),
454+
ImmutableSet.of(),
455+
Maps.filterKeys(symlinks, allowed::contains));
456+
}
457+
400458
@Override
401459
public String toString() {
402460
return "Files: " + files + "\nVirtualInputs: " + virtualInputs + "\nSymlinks: " + symlinks;
Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
// Copyright 2021 The Bazel Authors. All rights reserved.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package com.google.devtools.build.lib.worker;
16+
17+
import com.google.common.base.Joiner;
18+
import com.google.common.collect.ImmutableSet;
19+
import com.google.common.collect.Iterables;
20+
import com.google.common.flogger.GoogleLogger;
21+
import com.google.devtools.build.lib.sandbox.SandboxHelpers;
22+
import com.google.devtools.build.lib.sandbox.SandboxHelpers.SandboxInputs;
23+
import com.google.devtools.build.lib.sandbox.SandboxHelpers.SandboxOutputs;
24+
import com.google.devtools.build.lib.vfs.Path;
25+
import com.google.devtools.build.lib.vfs.PathFragment;
26+
import com.google.devtools.build.lib.worker.WorkerProtocol.WorkRequest;
27+
import java.io.IOException;
28+
import java.util.HashSet;
29+
import java.util.LinkedHashSet;
30+
import java.util.Set;
31+
32+
/**
33+
* A multiplex worker proxy with sandboxing. The multiplexer process runs in {@code workDir}, while
34+
* each proxy has a fixed subdir where it sets up its files. The subdir is then passed to the worker
35+
* in {@link WorkRequest#sandbox_dir}. The worker implementation is responsible for reading from and
36+
* writing to that subdir only.
37+
*/
38+
public class SandboxedWorkerProxy extends WorkerProxy {
39+
private static final GoogleLogger logger = GoogleLogger.forEnclosingClass();
40+
/** The sandbox directory for the current request, inside {@code workDir}. */
41+
private final Path sandboxDir;
42+
43+
private final PathFragment sandboxName;
44+
45+
SandboxedWorkerProxy(
46+
WorkerKey workerKey,
47+
int workerId,
48+
Path logFile,
49+
WorkerMultiplexer workerMultiplexer,
50+
Path workDir) {
51+
super(workerKey, workerId, logFile, workerMultiplexer, workDir);
52+
sandboxName =
53+
PathFragment.create(
54+
Joiner.on(PathFragment.SEPARATOR_CHAR)
55+
.join(
56+
"__sandbox",
57+
Integer.toString(workerId),
58+
workerKey.getExecRoot().getBaseName()));
59+
sandboxDir = this.workDir.getRelative(sandboxName);
60+
}
61+
62+
@Override
63+
public boolean isSandboxed() {
64+
return true;
65+
}
66+
67+
@Override
68+
public void prepareExecution(
69+
SandboxInputs inputFiles, SandboxOutputs outputs, Set<PathFragment> workerFiles)
70+
throws IOException {
71+
workerMultiplexer.createSandboxedProcess(workDir, workerFiles, inputFiles);
72+
73+
sandboxDir.createDirectoryAndParents();
74+
LinkedHashSet<PathFragment> dirsToCreate = new LinkedHashSet<>();
75+
Set<PathFragment> inputsToCreate = new HashSet<>();
76+
77+
SandboxHelpers.populateInputsAndDirsToCreate(
78+
ImmutableSet.of(),
79+
inputsToCreate,
80+
dirsToCreate,
81+
Iterables.concat(inputFiles.getFiles().keySet(), inputFiles.getSymlinks().keySet()),
82+
outputs.files(),
83+
outputs.dirs());
84+
SandboxHelpers.cleanExisting(
85+
sandboxDir.getParentDirectory(), inputFiles, inputsToCreate, dirsToCreate, sandboxDir);
86+
// Finally, create anything that is still missing. This is non-strict only for historical
87+
// reasons, we haven't seen what would break if we make it strict.
88+
SandboxHelpers.createDirectories(dirsToCreate, sandboxDir, /* strict=*/ false);
89+
WorkerExecRoot.createInputs(inputsToCreate, inputFiles, sandboxDir);
90+
}
91+
92+
/** Send the WorkRequest to multiplexer. */
93+
@Override
94+
protected void putRequest(WorkRequest request) throws IOException {
95+
// Modifying the request on the way out is not great. The alternatives are having the
96+
// spawn runner ask the worker for the dir or making the spawn runner understand the sandbox,
97+
// dir structure, neither of which are nice either.
98+
workerMultiplexer.putRequest(
99+
request.toBuilder().setSandboxDir(sandboxName.getPathString()).build());
100+
}
101+
102+
@Override
103+
public void finishExecution(Path execRoot, SandboxOutputs outputs) throws IOException {
104+
SandboxHelpers.moveOutputs(outputs, sandboxDir, execRoot);
105+
}
106+
107+
@Override
108+
synchronized void destroy() {
109+
super.destroy();
110+
try {
111+
workDir.deleteTree();
112+
} catch (IOException e) {
113+
logger.atWarning().withCause(e).log("Caught IOException while deleting workdir.");
114+
}
115+
}
116+
}

‎src/main/java/com/google/devtools/build/lib/worker/Worker.java‎

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,12 @@ public abstract void prepareExecution(
103103
*/
104104
abstract WorkResponse getResponse(int requestId) throws IOException, InterruptedException;
105105

106-
/** Does whatever cleanup may be required after execution is done. */
106+
/**
107+
* Does whatever cleanup may be required after execution is done.
108+
*
109+
* @param execRoot The global execRoot, where outputs must go.
110+
* @param outputs The expected outputs.
111+
*/
107112
public abstract void finishExecution(Path execRoot, SandboxOutputs outputs) throws IOException;
108113

109114
/**

0 commit comments

Comments
 (0)