Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Remote: Add --experimental_capture_corrupted_outputs flag. #13568

Closed
Closed
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -73,22 +73,37 @@ public static RemoteActionContextProvider createForPlaceholder(
env, /*cache=*/ null, /*executor=*/ null, retryScheduler, digestUtil, /*logDir=*/ null);
}

private static void maybeSetCaptureCorruptedOutputsDir(
RemoteOptions remoteOptions, RemoteCache remoteCache, Path workingDirectory) {
if (remoteOptions.remoteCaptureCorruptedOutputs != null
&& !remoteOptions.remoteCaptureCorruptedOutputs.isEmpty()) {
remoteCache.setCaptureCorruptedOutputsDir(
workingDirectory.getRelative(remoteOptions.remoteCaptureCorruptedOutputs));
}
}

public static RemoteActionContextProvider createForRemoteCaching(
CommandEnvironment env,
RemoteOptions options,
RemoteCache cache,
ListeningScheduledExecutorService retryScheduler,
DigestUtil digestUtil) {
maybeSetCaptureCorruptedOutputsDir(options, cache, env.getWorkingDirectory());

return new RemoteActionContextProvider(
env, cache, /*executor=*/ null, retryScheduler, digestUtil, /*logDir=*/ null);
}

public static RemoteActionContextProvider createForRemoteExecution(
CommandEnvironment env,
RemoteOptions options,
RemoteExecutionCache cache,
RemoteExecutionClient executor,
ListeningScheduledExecutorService retryScheduler,
DigestUtil digestUtil,
Path logDir) {
maybeSetCaptureCorruptedOutputsDir(options, cache, env.getWorkingDirectory());

return new RemoteActionContextProvider(
env, cache, executor, retryScheduler, digestUtil, logDir);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
import com.google.common.util.concurrent.FutureCallback;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.MoreExecutors;
import com.google.common.util.concurrent.SettableFuture;
import com.google.devtools.build.lib.actions.ActionInput;
import com.google.devtools.build.lib.actions.Artifact;
Expand All @@ -55,6 +56,7 @@
import com.google.devtools.build.lib.remote.RemoteCache.ActionResultMetadata.DirectoryMetadata;
import com.google.devtools.build.lib.remote.RemoteCache.ActionResultMetadata.FileMetadata;
import com.google.devtools.build.lib.remote.RemoteCache.ActionResultMetadata.SymlinkMetadata;
import com.google.devtools.build.lib.remote.common.OutputDigestMismatchException;
import com.google.devtools.build.lib.remote.common.RemoteActionExecutionContext;
import com.google.devtools.build.lib.remote.common.RemoteActionFileArtifactValue;
import com.google.devtools.build.lib.remote.common.RemoteCacheClient;
Expand Down Expand Up @@ -110,13 +112,19 @@ interface OutputFilesLocker {
protected final RemoteOptions options;
protected final DigestUtil digestUtil;

private Path captureCorruptedOutputsDir;

public RemoteCache(
RemoteCacheClient cacheProtocol, RemoteOptions options, DigestUtil digestUtil) {
this.cacheProtocol = cacheProtocol;
this.options = options;
this.digestUtil = digestUtil;
}

public void setCaptureCorruptedOutputsDir(Path captureCorruptedOutputsDir) {
this.captureCorruptedOutputsDir = captureCorruptedOutputsDir;
}

public ActionResult downloadActionResult(
RemoteActionExecutionContext context, ActionKey actionKey, boolean inlineOutErr)
throws IOException, InterruptedException {
Expand Down Expand Up @@ -334,7 +342,10 @@ public void download(
(file) -> {
try {
ListenableFuture<Void> download =
downloadFile(context, toTmpDownloadPath(file.path()), file.digest());
downloadFile(
context,
remotePathResolver,
file);
return Futures.transform(download, (d) -> file, directExecutor());
} catch (IOException e) {
return Futures.<FileMetadata>immediateFailedFuture(e);
Expand All @@ -355,6 +366,25 @@ public void download(
try {
waitForBulkTransfer(downloads, /* cancelRemainingOnInterrupt=*/ true);
} catch (Exception e) {
if (captureCorruptedOutputsDir != null) {
if (e instanceof BulkTransferException) {
for (Throwable suppressed : e.getSuppressed()) {
if (suppressed instanceof OutputDigestMismatchException) {
// Capture corrupted outputs
try {
String outputPath = ((OutputDigestMismatchException) suppressed).getOutputPath();
Path localPath = ((OutputDigestMismatchException) suppressed).getLocalPath();
Path dst = captureCorruptedOutputsDir.getRelative(outputPath);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it guaranteed (even with broken or malicious backends) that the output path will never contain .. references or start with a /? Should we check that the resulting path dst is still below the captureCorruptedOutputsDir just to make sure? 🤔

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We are safe here since outputPath is what we obtained from skyframe and send to remote execution backend (something like bazel-out/darwin-fastbuild/bin/...).

It is a path relative to the working directory (or input root depends on the flags) when the remote backend executing the action.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

However, I added the check in case the invariance changed in the future. Thanks for the catch!

dst.createDirectoryAndParents();
FileSystemUtils.copyFile(localPath, dst);
} catch (IOException ioEx) {
suppressed.addSuppressed(ioEx);
}
}
}
}
}

try {
// Delete any (partially) downloaded output files.
for (OutputFile file : result.getOutputFilesList()) {
Expand Down Expand Up @@ -461,6 +491,37 @@ private void createSymlinks(Iterable<SymlinkMetadata> symlinks) throws IOExcepti
}
}

public ListenableFuture<Void> downloadFile(
RemoteActionExecutionContext context,
RemotePathResolver remotePathResolver,
FileMetadata file)
throws IOException {
SettableFuture<Void> outerF = SettableFuture.create();
Path localPath = toTmpDownloadPath(file.path());
ListenableFuture<Void> f = downloadFile(context, localPath, file.digest());
Futures.addCallback(
f,
new FutureCallback<Void>() {
@Override
public void onSuccess(Void unused) {
outerF.set(null);
}

@Override
public void onFailure(Throwable throwable) {
if (throwable instanceof OutputDigestMismatchException) {
OutputDigestMismatchException e = ((OutputDigestMismatchException) throwable);
e.setOutputPath(remotePathResolver.localPathToOutputPath(file.path()));
e.setLocalPath(localPath);
}
outerF.setException(throwable);
}
},
MoreExecutors.directExecutor());

return outerF;
}

/** Downloads a file (that is not a directory). The content is fetched from the digest. */
public ListenableFuture<Void> downloadFile(
RemoteActionExecutionContext context, Path path, Digest digest) throws IOException {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -223,7 +223,7 @@ private void initHttpAndDiskCache(
RemoteCache remoteCache = new RemoteCache(cacheClient, remoteOptions, digestUtil);
actionContextProvider =
RemoteActionContextProvider.createForRemoteCaching(
env, remoteCache, /* retryScheduler= */ null, digestUtil);
env, remoteOptions, remoteCache, /* retryScheduler= */ null, digestUtil);
}

@Override
Expand Down Expand Up @@ -530,7 +530,7 @@ public void beforeCommand(CommandEnvironment env) throws AbruptExitException {
new RemoteExecutionCache(cacheClient, remoteOptions, digestUtil);
actionContextProvider =
RemoteActionContextProvider.createForRemoteExecution(
env, remoteCache, remoteExecutor, retryScheduler, digestUtil, logDir);
env, remoteOptions, remoteCache, remoteExecutor, retryScheduler, digestUtil, logDir);
repositoryRemoteExecutorFactoryDelegate.init(
new RemoteRepositoryRemoteExecutorFactory(
remoteCache,
Expand Down Expand Up @@ -560,7 +560,7 @@ public void beforeCommand(CommandEnvironment env) throws AbruptExitException {
RemoteCache remoteCache = new RemoteCache(cacheClient, remoteOptions, digestUtil);
actionContextProvider =
RemoteActionContextProvider.createForRemoteCaching(
env, remoteCache, retryScheduler, digestUtil);
env, remoteOptions, remoteCache, retryScheduler, digestUtil);
}

if (enableRemoteDownloader) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
// Copyright 2021 The Bazel Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package com.google.devtools.build.lib.remote.common;

import build.bazel.remote.execution.v2.Digest;
import com.google.devtools.build.lib.vfs.Path;
import java.io.IOException;

/** An exception to indicate the digest of downloaded output does not match the expected value. */
public class OutputDigestMismatchException extends IOException {
private final Digest expected;
private final Digest actual;

private Path localPath;
private String outputPath;

public OutputDigestMismatchException(Digest expected, Digest actual) {
this.expected = expected;
this.actual = actual;
}

public void setOutputPath(String outputPath) {
this.outputPath = outputPath;
}

public String getOutputPath() {
return outputPath;
}

public Path getLocalPath() {
return localPath;
}

public void setLocalPath(Path localPath) {
this.localPath = localPath;
}

@Override
public String getMessage() {
return String.format(
"Output %s download failed: Expected digest '%s/%d' does not match "
+ "received digest '%s/%d'.",
outputPath,
expected.getHash(),
expected.getSizeBytes(),
actual.getHash(),
actual.getSizeBytes());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,15 @@ public final class RemoteOptions extends OptionsBase {
help = "Whether to use keepalive for remote execution calls.")
public boolean remoteExecutionKeepalive;

@Option(
name = "experimental_remote_capture_corrupted_outputs",
defaultValue = "null",
documentationCategory = OptionDocumentationCategory.REMOTE,
effectTags = {OptionEffectTag.UNKNOWN},
converter = OptionsUtils.PathFragmentConverter.class,
help = "A path to a directory where the corrupted outputs will be captured to.")
public PathFragment remoteCaptureCorruptedOutputs;

@Option(
name = "remote_cache",
oldName = "remote_http_cache",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
import com.google.devtools.build.lib.authandtls.CallCredentialsProvider;
import com.google.devtools.build.lib.remote.ExecutionStatusException;
import com.google.devtools.build.lib.remote.common.CacheNotFoundException;
import com.google.devtools.build.lib.remote.common.OutputDigestMismatchException;
import com.google.devtools.build.lib.remote.common.RemoteCacheClient.ActionKey;
import com.google.devtools.build.lib.remote.options.RemoteOutputsMode;
import com.google.devtools.build.lib.server.FailureDetails;
Expand Down Expand Up @@ -390,12 +391,7 @@ public static ListenableFuture<ActionResult> downloadAsActionResult(

public static void verifyBlobContents(Digest expected, Digest actual) throws IOException {
if (!expected.equals(actual)) {
String msg =
String.format(
"Output download failed: Expected digest '%s/%d' does not match "
+ "received digest '%s/%d'.",
expected.getHash(), expected.getSizeBytes(), actual.getHash(), actual.getSizeBytes());
throw new IOException(msg);
throw new OutputDigestMismatchException(expected, actual);
}
}

Expand Down