Skip to content

Commit eb762d4

Browse files
alexjskicopybara-github
authored andcommitted
Fix racy write of temporary files while staging virtual inputs for the sandbox.
When staging virtual inputs without delayed materialization, `SandboxHelpers` performs atomic writes by first staging virtual inputs into a temporary file and later moving it to the target destination. This is achieved by performing the initial writes into a file with a uniquifying suffix. This suffix happens to currently always be hardcoded to `.sandbox`, which means that staging the same virtual inputs for 2 actions may race on that. Fix the race condition by providing a truly unique suffix for the temporary file. PiperOrigin-RevId: 351613739
1 parent a607d9d commit eb762d4

File tree

3 files changed

+86
-1
lines changed

3 files changed

+86
-1
lines changed

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

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
import java.util.Map;
3535
import java.util.Set;
3636
import java.util.TreeMap;
37+
import java.util.concurrent.atomic.AtomicInteger;
3738

3839
/**
3940
* Helper methods that are shared by the different sandboxing strategies.
@@ -102,6 +103,10 @@ public static void atomicallyWriteVirtualInput(
102103

103104
/** Wrapper class for the inputs of a sandbox. */
104105
public static final class SandboxInputs {
106+
107+
private static final AtomicInteger tempFileUniquifierForVirtualInputWrites =
108+
new AtomicInteger();
109+
105110
private final Map<PathFragment, Path> files;
106111
private final Set<VirtualActionInput> virtualInputs;
107112
private final Map<PathFragment, PathFragment> symlinks;
@@ -145,7 +150,12 @@ private static void materializeVirtualInput(
145150

146151
Path outputPath = execroot.getRelative(input.getExecPath());
147152
if (isExecRootSandboxed) {
148-
atomicallyWriteVirtualInput(input, outputPath, ".sandbox");
153+
atomicallyWriteVirtualInput(
154+
input,
155+
outputPath,
156+
// When 2 actions try to atomically create the same virtual input, they need to have a
157+
// different suffix for the temporary file in order to avoid racy write to the same one.
158+
".sandbox" + tempFileUniquifierForVirtualInputWrites.incrementAndGet());
149159
return;
150160
}
151161

src/test/java/com/google/devtools/build/lib/sandbox/BUILD

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ java_test(
6767
"//src/test/java/com/google/devtools/build/lib/testutil",
6868
"//src/test/java/com/google/devtools/build/lib/testutil:TestUtils",
6969
"//third_party:guava",
70+
"//third_party:jsr305",
7071
"//third_party:junit4",
7172
"//third_party:truth",
7273
],

src/test/java/com/google/devtools/build/lib/sandbox/SandboxHelpersTest.java

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
import static com.google.common.collect.ImmutableMap.toImmutableMap;
1818
import static com.google.common.truth.Truth.assertThat;
1919
import static java.nio.charset.StandardCharsets.UTF_8;
20+
import static java.util.concurrent.TimeUnit.SECONDS;
2021

2122
import com.google.common.collect.ImmutableList;
2223
import com.google.common.collect.ImmutableMap;
@@ -31,14 +32,26 @@
3132
import com.google.devtools.build.lib.exec.util.SpawnBuilder;
3233
import com.google.devtools.build.lib.sandbox.SandboxHelpers.SandboxInputs;
3334
import com.google.devtools.build.lib.testutil.Scratch;
35+
import com.google.devtools.build.lib.testutil.TestUtils;
36+
import com.google.devtools.build.lib.vfs.DigestHashFunction;
3437
import com.google.devtools.build.lib.vfs.Dirent;
38+
import com.google.devtools.build.lib.vfs.FileSystem;
3539
import com.google.devtools.build.lib.vfs.FileSystemUtils;
3640
import com.google.devtools.build.lib.vfs.Path;
3741
import com.google.devtools.build.lib.vfs.PathFragment;
3842
import com.google.devtools.build.lib.vfs.Symlinks;
43+
import com.google.devtools.build.lib.vfs.inmemoryfs.InMemoryFileSystem;
3944
import java.io.IOException;
4045
import java.util.Arrays;
46+
import java.util.concurrent.BrokenBarrierException;
47+
import java.util.concurrent.CyclicBarrier;
48+
import java.util.concurrent.ExecutorService;
49+
import java.util.concurrent.Executors;
50+
import java.util.concurrent.Future;
51+
import java.util.concurrent.Semaphore;
4152
import java.util.function.Function;
53+
import javax.annotation.Nullable;
54+
import org.junit.After;
4255
import org.junit.Before;
4356
import org.junit.Test;
4457
import org.junit.runner.RunWith;
@@ -53,12 +66,23 @@ public class SandboxHelpersTest {
5366

5467
private final Scratch scratch = new Scratch();
5568
private Path execRoot;
69+
@Nullable private ExecutorService executorToCleanup;
5670

5771
@Before
5872
public void createExecRoot() throws IOException {
5973
execRoot = scratch.dir("/execRoot");
6074
}
6175

76+
@After
77+
public void shutdownExecutor() throws InterruptedException {
78+
if (executorToCleanup == null) {
79+
return;
80+
}
81+
82+
executorToCleanup.shutdown();
83+
executorToCleanup.awaitTermination(TestUtils.WAIT_TIMEOUT_SECONDS, SECONDS);
84+
}
85+
6286
@Test
6387
public void processInputFiles_materializesParamFile() throws Exception {
6488
SandboxHelpers sandboxHelpers = new SandboxHelpers(/*delayVirtualInputMaterialization=*/ false);
@@ -150,6 +174,56 @@ public void sandboxInputMaterializeVirtualInputs_delayMaterialization_writesCorr
150174
assertThat(sandboxToolFile.isExecutable()).isTrue();
151175
}
152176

177+
/**
178+
* Test simulating a scenario when 2 parallel writes of the same virtual input both complete write
179+
* of the temp file and then proceed with post-processing steps one-by-one.
180+
*/
181+
@Test
182+
public void sandboxInputMaterializeVirtualInput_parallelWritesForSameInput_writesCorrectFile()
183+
throws Exception {
184+
VirtualActionInput input = ActionsTestUtil.createVirtualActionInput("file", "hello");
185+
executorToCleanup = Executors.newSingleThreadExecutor();
186+
CyclicBarrier bothWroteTempFile = new CyclicBarrier(2);
187+
Semaphore finishProcessingSemaphore = new Semaphore(1);
188+
FileSystem customFs =
189+
new InMemoryFileSystem(DigestHashFunction.SHA1) {
190+
@Override
191+
protected void setExecutable(Path path, boolean executable) throws IOException {
192+
try {
193+
bothWroteTempFile.await();
194+
finishProcessingSemaphore.acquire();
195+
} catch (BrokenBarrierException | InterruptedException e) {
196+
throw new IllegalArgumentException(e);
197+
}
198+
super.setExecutable(path, executable);
199+
}
200+
};
201+
Scratch customScratch = new Scratch(customFs);
202+
Path customExecRoot = customScratch.dir("/execroot");
203+
SandboxHelpers sandboxHelpers = new SandboxHelpers(/*delayVirtualInputMaterialization=*/ false);
204+
205+
Future<?> future =
206+
executorToCleanup.submit(
207+
() -> {
208+
try {
209+
sandboxHelpers.processInputFiles(
210+
inputMap(input), SPAWN, EMPTY_EXPANDER, customExecRoot);
211+
finishProcessingSemaphore.release();
212+
} catch (IOException e) {
213+
throw new IllegalArgumentException(e);
214+
}
215+
});
216+
sandboxHelpers.processInputFiles(inputMap(input), SPAWN, EMPTY_EXPANDER, customExecRoot);
217+
finishProcessingSemaphore.release();
218+
future.get();
219+
220+
assertThat(customExecRoot.readdir(Symlinks.NOFOLLOW))
221+
.containsExactly(new Dirent("file", Dirent.Type.FILE));
222+
Path outputFile = customExecRoot.getChild("file");
223+
assertThat(FileSystemUtils.readLines(outputFile, UTF_8)).containsExactly("hello");
224+
assertThat(outputFile.isExecutable()).isTrue();
225+
}
226+
153227
private static ImmutableMap<PathFragment, ActionInput> inputMap(ActionInput... inputs) {
154228
return Arrays.stream(inputs)
155229
.collect(toImmutableMap(ActionInput::getExecPath, Function.identity()));

0 commit comments

Comments
 (0)