diff --git a/src/main/java/com/google/devtools/build/lib/bazel/BazelRepositoryModule.java b/src/main/java/com/google/devtools/build/lib/bazel/BazelRepositoryModule.java index 50a664a9324273..56c706252fbca8 100644 --- a/src/main/java/com/google/devtools/build/lib/bazel/BazelRepositoryModule.java +++ b/src/main/java/com/google/devtools/build/lib/bazel/BazelRepositoryModule.java @@ -122,6 +122,7 @@ import com.google.devtools.build.lib.vfs.RootedPath; import com.google.devtools.common.options.OptionsBase; import com.google.devtools.common.options.OptionsParsingResult; +import java.io.FileNotFoundException; import java.io.IOException; import java.lang.reflect.InvocationTargetException; import java.time.Instant; @@ -376,6 +377,50 @@ public void beforeCommand(CommandEnvironment env) throws AbruptExitException { repositoryCache.getRepoContentsCache().setPath(toPath(repoOptions.repoContentsCache, env)); } Path repoContentsCachePath = repositoryCache.getRepoContentsCache().getPath(); + if (repoContentsCachePath != null) { + // Check that the repo contents cache directory, which is managed by a garbage collecting + // idle task, does not contain the output base. Since the specified output base path may be + // a symlink, we resolve it fully. Intermediate symlinks do not have to be checked as the + // garbage collector ignores symlinks. We also resolve the repo contents cache directory, + // where intermediate symlinks also don't matter since deletion only occurs under the fully + // resolved path. + Path resolvedOutputBase = env.getOutputBase(); + try { + resolvedOutputBase = resolvedOutputBase.resolveSymbolicLinks(); + } catch (FileNotFoundException ignored) { + // Will be created later. + } catch (IOException e) { + throw new AbruptExitException( + detailedExitCode( + "could not resolve output base: %s".formatted(e.getMessage()), + Code.BAD_REPO_CONTENTS_CACHE), + e); + } + Path resolvedRepoContentsCache = repoContentsCachePath; + try { + resolvedRepoContentsCache = resolvedRepoContentsCache.resolveSymbolicLinks(); + } catch (FileNotFoundException ignored) { + // Will be created later. + } catch (IOException e) { + throw new AbruptExitException( + detailedExitCode( + "could not resolve repo contents cache path: %s".formatted(e.getMessage()), + Code.BAD_REPO_CONTENTS_CACHE), + e); + } + if (resolvedOutputBase.startsWith(resolvedRepoContentsCache)) { + // This is dangerous as the repo contents cache GC may delete files in the output base. + throw new AbruptExitException( + detailedExitCode( + """ + The output base [%s] is inside the repo contents cache [%s]. This can cause \ + spurious failures. Disable the repo contents cache with `--repo_contents_cache=`, \ + or specify `--repo_contents_cache=`. + """ + .formatted(resolvedOutputBase, resolvedRepoContentsCache), + Code.BAD_REPO_CONTENTS_CACHE)); + } + } if (repoContentsCachePath != null && env.getWorkspace() != null && repoContentsCachePath.startsWith(env.getWorkspace())) { diff --git a/src/main/java/com/google/devtools/build/lib/remote/RemoteModule.java b/src/main/java/com/google/devtools/build/lib/remote/RemoteModule.java index a220789d8a4ae5..55b795ba6415f2 100644 --- a/src/main/java/com/google/devtools/build/lib/remote/RemoteModule.java +++ b/src/main/java/com/google/devtools/build/lib/remote/RemoteModule.java @@ -111,6 +111,7 @@ import io.netty.handler.codec.DecoderException; import io.netty.handler.codec.http.HttpResponseStatus; import io.reactivex.rxjava3.plugins.RxJavaPlugins; +import java.io.FileNotFoundException; import java.io.IOException; import java.net.URI; import java.net.URISyntaxException; @@ -344,6 +345,38 @@ public void beforeCommand(CommandEnvironment env) throws AbruptExitException { boolean enableRemoteDownloader = shouldEnableRemoteDownloader(remoteOptions); if (enableDiskCache) { + // Check that the disk cache directory, which is managed by a garbage collecting idle task, + // does not contain the output base. Since the specified output base path may be a symlink, + // we resolve it fully. Intermediate symlinks do not have to be checked as the garbage + // collector ignores symlinks. We also resolve the disk cache directory, where intermediate + // symlinks also don't matter since deletion only occurs under the fully resolved path. + Path resolvedOutputBase = env.getOutputBase(); + try { + resolvedOutputBase = resolvedOutputBase.resolveSymbolicLinks(); + } catch (FileNotFoundException ignored) { + // Will be created later. + } catch (IOException e) { + throw createOptionsExitException( + "Failed to resolve output base: %s".formatted(e.getMessage()), + FailureDetails.RemoteOptions.Code.EXECUTION_WITH_INVALID_CACHE); + } + Path resolvedDiskCache = env.getWorkingDirectory().getRelative(remoteOptions.diskCache); + try { + resolvedDiskCache = resolvedDiskCache.resolveSymbolicLinks(); + } catch (FileNotFoundException ignored) { + // Will be created later. + } catch (IOException e) { + throw createOptionsExitException( + "Failed to resolve disk cache directory: %s".formatted(e.getMessage()), + FailureDetails.RemoteOptions.Code.EXECUTION_WITH_INVALID_CACHE); + } + if (resolvedOutputBase.startsWith(resolvedDiskCache)) { + // This is dangerous as the disk cache GC may delete files in the output base. + throw createOptionsExitException( + "The output base [%s] cannot be a subdirectory of the --disk_cache directory [%s]" + .formatted(resolvedOutputBase, resolvedDiskCache), + FailureDetails.RemoteOptions.Code.EXECUTION_WITH_INVALID_CACHE); + } var gcIdleTask = DiskCacheGarbageCollectorIdleTask.create(remoteOptions, env.getWorkingDirectory()); if (gcIdleTask != null) { @@ -1223,5 +1256,4 @@ static Credentials createCredentials( Downloader getRemoteDownloader() { return remoteDownloader; } - }