Check whether the remote cache accepts absolute symlinks before uploading them.

Closes #16354.

PiperOrigin-RevId: 477812051
Change-Id: I12419094b2e9fab1d4d66c5f94f331ebaaf20695
diff --git a/src/main/java/com/google/devtools/build/lib/remote/RemoteCache.java b/src/main/java/com/google/devtools/build/lib/remote/RemoteCache.java
index 1fc369b..60ebc08 100644
--- a/src/main/java/com/google/devtools/build/lib/remote/RemoteCache.java
+++ b/src/main/java/com/google/devtools/build/lib/remote/RemoteCache.java
@@ -20,6 +20,7 @@
 import static com.google.devtools.build.lib.remote.util.Utils.getFromFuture;
 
 import build.bazel.remote.execution.v2.ActionResult;
+import build.bazel.remote.execution.v2.CacheCapabilities;
 import build.bazel.remote.execution.v2.Digest;
 import com.google.common.base.Preconditions;
 import com.google.common.collect.ImmutableSet;
@@ -83,17 +84,26 @@
   private final CountDownLatch closeCountDownLatch = new CountDownLatch(1);
   protected final AsyncTaskCache.NoResult<Digest> casUploadCache = AsyncTaskCache.NoResult.create();
 
+  protected final CacheCapabilities cacheCapabilities;
   protected final RemoteCacheClient cacheProtocol;
   protected final RemoteOptions options;
   protected final DigestUtil digestUtil;
 
   public RemoteCache(
-      RemoteCacheClient cacheProtocol, RemoteOptions options, DigestUtil digestUtil) {
+      CacheCapabilities cacheCapabilities,
+      RemoteCacheClient cacheProtocol,
+      RemoteOptions options,
+      DigestUtil digestUtil) {
+    this.cacheCapabilities = cacheCapabilities;
     this.cacheProtocol = cacheProtocol;
     this.options = options;
     this.digestUtil = digestUtil;
   }
 
+  public CacheCapabilities getCacheCapabilities() {
+    return cacheCapabilities;
+  }
+
   public CachedActionResult downloadActionResult(
       RemoteActionExecutionContext context, ActionKey actionKey, boolean inlineOutErr)
       throws IOException, InterruptedException {
diff --git a/src/main/java/com/google/devtools/build/lib/remote/RemoteExecutionCache.java b/src/main/java/com/google/devtools/build/lib/remote/RemoteExecutionCache.java
index 2414c9f..421313e 100644
--- a/src/main/java/com/google/devtools/build/lib/remote/RemoteExecutionCache.java
+++ b/src/main/java/com/google/devtools/build/lib/remote/RemoteExecutionCache.java
@@ -23,6 +23,7 @@
 import static com.google.devtools.build.lib.remote.util.RxUtils.toTransferResult;
 import static java.lang.String.format;
 
+import build.bazel.remote.execution.v2.CacheCapabilities;
 import build.bazel.remote.execution.v2.Digest;
 import build.bazel.remote.execution.v2.Directory;
 import com.google.common.base.Throwables;
@@ -59,10 +60,11 @@
 public class RemoteExecutionCache extends RemoteCache {
 
   public RemoteExecutionCache(
+      CacheCapabilities cacheCapabilities,
       RemoteCacheClient protocolImpl,
       RemoteOptions options,
       DigestUtil digestUtil) {
-    super(protocolImpl, options, digestUtil);
+    super(cacheCapabilities, protocolImpl, options, digestUtil);
   }
 
   /**
diff --git a/src/main/java/com/google/devtools/build/lib/remote/RemoteExecutionService.java b/src/main/java/com/google/devtools/build/lib/remote/RemoteExecutionService.java
index 154d139..ef33003 100644
--- a/src/main/java/com/google/devtools/build/lib/remote/RemoteExecutionService.java
+++ b/src/main/java/com/google/devtools/build/lib/remote/RemoteExecutionService.java
@@ -1270,6 +1270,7 @@
 
           return UploadManifest.create(
               remoteOptions,
+              remoteCache.getCacheCapabilities(),
               digestUtil,
               remotePathResolver,
               action.getActionKey(),
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 3d792d0..0c96be6 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
@@ -16,8 +16,10 @@
 
 import static java.util.concurrent.TimeUnit.SECONDS;
 
+import build.bazel.remote.execution.v2.CacheCapabilities;
 import build.bazel.remote.execution.v2.DigestFunction;
 import build.bazel.remote.execution.v2.ServerCapabilities;
+import build.bazel.remote.execution.v2.SymlinkAbsolutePathStrategy;
 import com.google.auth.Credentials;
 import com.google.common.annotations.VisibleForTesting;
 import com.google.common.base.Ascii;
@@ -125,6 +127,11 @@
 
   private static final GoogleLogger logger = GoogleLogger.forEnclosingClass();
 
+  private static final CacheCapabilities DISK_CACHE_CAPABILITIES =
+      CacheCapabilities.newBuilder()
+          .setSymlinkAbsolutePathStrategy(SymlinkAbsolutePathStrategy.Value.ALLOWED)
+          .build();
+
   private final ListeningScheduledExecutorService retryScheduler =
       MoreExecutors.listeningDecorator(Executors.newScheduledThreadPool(1));
 
@@ -181,7 +188,7 @@
     return !Strings.isNullOrEmpty(options.remoteDownloader);
   }
 
-  private static void verifyServerCapabilities(
+  private static ServerCapabilities getAndVerifyServerCapabilities(
       RemoteOptions remoteOptions,
       ReferenceCountedChannel channel,
       CallCredentials credentials,
@@ -202,7 +209,7 @@
       capabilities = rsc.get(env.getBuildRequestId(), env.getCommandId().toString());
     } catch (InterruptedException e) {
       Thread.currentThread().interrupt();
-      return;
+      return null;
     }
     checkClientServerCompatibility(
         capabilities,
@@ -210,6 +217,7 @@
         digestUtil.getDigestFunction(),
         env.getReporter(),
         requirement);
+    return capabilities;
   }
 
   private void initHttpAndDiskCache(
@@ -248,7 +256,8 @@
       handleInitFailure(env, e, Code.CACHE_INIT_FAILURE);
       return;
     }
-    RemoteCache remoteCache = new RemoteCache(cacheClient, remoteOptions, digestUtil);
+    RemoteCache remoteCache =
+        new RemoteCache(DISK_CACHE_CAPABILITIES, cacheClient, remoteOptions, digestUtil);
     actionContextProvider =
         RemoteActionContextProvider.createForRemoteCaching(
             executorService, env, remoteCache, /* retryScheduler= */ null, digestUtil);
@@ -483,44 +492,51 @@
     //
     // If they point to different endpoints, we check the endpoint with execution or cache
     // capabilities respectively.
+    ServerCapabilities execCapabilities = null;
+    ServerCapabilities cacheCapabilities = null;
     try {
       if (execChannel != null) {
         if (cacheChannel != execChannel) {
-          verifyServerCapabilities(
-              remoteOptions,
-              execChannel,
-              credentials,
-              retrier,
-              env,
-              digestUtil,
-              ServerCapabilitiesRequirement.EXECUTION);
-          verifyServerCapabilities(
-              remoteOptions,
-              cacheChannel,
-              credentials,
-              retrier,
-              env,
-              digestUtil,
-              ServerCapabilitiesRequirement.CACHE);
+          execCapabilities =
+              getAndVerifyServerCapabilities(
+                  remoteOptions,
+                  execChannel,
+                  credentials,
+                  retrier,
+                  env,
+                  digestUtil,
+                  ServerCapabilitiesRequirement.EXECUTION);
+          cacheCapabilities =
+              getAndVerifyServerCapabilities(
+                  remoteOptions,
+                  cacheChannel,
+                  credentials,
+                  retrier,
+                  env,
+                  digestUtil,
+                  ServerCapabilitiesRequirement.CACHE);
         } else {
-          verifyServerCapabilities(
-              remoteOptions,
-              execChannel,
-              credentials,
-              retrier,
-              env,
-              digestUtil,
-              ServerCapabilitiesRequirement.EXECUTION_AND_CACHE);
+          execCapabilities =
+              cacheCapabilities =
+                  getAndVerifyServerCapabilities(
+                      remoteOptions,
+                      execChannel,
+                      credentials,
+                      retrier,
+                      env,
+                      digestUtil,
+                      ServerCapabilitiesRequirement.EXECUTION_AND_CACHE);
         }
       } else {
-        verifyServerCapabilities(
-            remoteOptions,
-            cacheChannel,
-            credentials,
-            retrier,
-            env,
-            digestUtil,
-            ServerCapabilitiesRequirement.CACHE);
+        cacheCapabilities =
+            getAndVerifyServerCapabilities(
+                remoteOptions,
+                cacheChannel,
+                credentials,
+                retrier,
+                env,
+                digestUtil,
+                ServerCapabilitiesRequirement.CACHE);
       }
     } catch (IOException e) {
       String errorMessage =
@@ -602,7 +618,8 @@
       }
       execChannel.release();
       RemoteExecutionCache remoteCache =
-          new RemoteExecutionCache(cacheClient, remoteOptions, digestUtil);
+          new RemoteExecutionCache(
+              cacheCapabilities.getCacheCapabilities(), cacheClient, remoteOptions, digestUtil);
       actionContextProvider =
           RemoteActionContextProvider.createForRemoteExecution(
               executorService,
@@ -637,7 +654,9 @@
         }
       }
 
-      RemoteCache remoteCache = new RemoteCache(cacheClient, remoteOptions, digestUtil);
+      RemoteCache remoteCache =
+          new RemoteCache(
+              cacheCapabilities.getCacheCapabilities(), cacheClient, remoteOptions, digestUtil);
       actionContextProvider =
           RemoteActionContextProvider.createForRemoteCaching(
               executorService, env, remoteCache, retryScheduler, digestUtil);
diff --git a/src/main/java/com/google/devtools/build/lib/remote/UploadManifest.java b/src/main/java/com/google/devtools/build/lib/remote/UploadManifest.java
index a7fc6ed..b75c897 100644
--- a/src/main/java/com/google/devtools/build/lib/remote/UploadManifest.java
+++ b/src/main/java/com/google/devtools/build/lib/remote/UploadManifest.java
@@ -22,9 +22,11 @@
 
 import build.bazel.remote.execution.v2.Action;
 import build.bazel.remote.execution.v2.ActionResult;
+import build.bazel.remote.execution.v2.CacheCapabilities;
 import build.bazel.remote.execution.v2.Command;
 import build.bazel.remote.execution.v2.Digest;
 import build.bazel.remote.execution.v2.Directory;
+import build.bazel.remote.execution.v2.SymlinkAbsolutePathStrategy;
 import build.bazel.remote.execution.v2.Tree;
 import com.google.common.annotations.VisibleForTesting;
 import com.google.common.base.Preconditions;
@@ -77,6 +79,7 @@
   private final ActionResult.Builder result;
   private final boolean followSymlinks;
   private final boolean allowDanglingSymlinks;
+  private final boolean allowAbsoluteSymlinks;
   private final Map<Digest, Path> digestToFile = new HashMap<>();
   private final Map<Digest, ByteString> digestToBlobs = new HashMap<>();
   @Nullable private ActionKey actionKey;
@@ -85,6 +88,7 @@
 
   public static UploadManifest create(
       RemoteOptions remoteOptions,
+      CacheCapabilities cacheCapabilities,
       DigestUtil digestUtil,
       RemotePathResolver remotePathResolver,
       ActionKey actionKey,
@@ -105,7 +109,10 @@
             remotePathResolver,
             result,
             /* followSymlinks= */ !remoteOptions.incompatibleRemoteSymlinks,
-            /* allowDanglingSymlinks= */ remoteOptions.incompatibleRemoteDanglingSymlinks);
+            /* allowDanglingSymlinks= */ remoteOptions.incompatibleRemoteDanglingSymlinks,
+            /* allowAbsoluteSymlinks= */ cacheCapabilities
+                .getSymlinkAbsolutePathStrategy()
+                .equals(SymlinkAbsolutePathStrategy.Value.ALLOWED));
     manifest.addFiles(outputFiles);
     manifest.setStdoutStderr(outErr);
     manifest.addAction(actionKey, action, command);
@@ -143,12 +150,14 @@
       RemotePathResolver remotePathResolver,
       ActionResult.Builder result,
       boolean followSymlinks,
-      boolean allowDanglingSymlinks) {
+      boolean allowDanglingSymlinks,
+      boolean allowAbsoluteSymlinks) {
     this.digestUtil = digestUtil;
     this.remotePathResolver = remotePathResolver;
     this.result = result;
     this.followSymlinks = followSymlinks;
     this.allowDanglingSymlinks = allowDanglingSymlinks;
+    this.allowAbsoluteSymlinks = allowAbsoluteSymlinks;
   }
 
   private void setStdoutStderr(FileOutErr outErr) throws IOException {
@@ -191,13 +200,19 @@
         FileStatus statFollow = file.statIfFound(Symlinks.FOLLOW);
         if (statFollow == null) {
           if (allowDanglingSymlinks) {
+            if (target.isAbsolute() && !allowAbsoluteSymlinks) {
+              throw new IOException(
+                  String.format(
+                      "Action output %s is an absolute symbolic link to %s, which is not allowed by"
+                          + " the remote cache",
+                      file, target));
+            }
             // Report symlink to a file since we don't know any better.
-            // TODO(tjgq): Check for the SymlinkAbsolutePathStrategy.ALLOW server capability before
-            // uploading an absolute symlink.
             addFileSymbolicLink(file, target);
           } else {
             throw new IOException(
-                String.format("Action output %s is a dangling symbolic link to %s ", file, target));
+                String.format(
+                    "Action output %s is a dangling symbolic link to %s. ", file, target));
           }
         } else if (statFollow.isSpecialFile()) {
           illegalOutput(file);
diff --git a/src/test/java/com/google/devtools/build/lib/remote/ByteStreamBuildEventArtifactUploaderTest.java b/src/test/java/com/google/devtools/build/lib/remote/ByteStreamBuildEventArtifactUploaderTest.java
index 5cc44a9..42602fa 100644
--- a/src/test/java/com/google/devtools/build/lib/remote/ByteStreamBuildEventArtifactUploaderTest.java
+++ b/src/test/java/com/google/devtools/build/lib/remote/ByteStreamBuildEventArtifactUploaderTest.java
@@ -22,6 +22,7 @@
 import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
 
+import build.bazel.remote.execution.v2.CacheCapabilities;
 import build.bazel.remote.execution.v2.Digest;
 import com.google.bytestream.ByteStreamProto.WriteRequest;
 import com.google.bytestream.ByteStreamProto.WriteResponse;
@@ -469,7 +470,8 @@
         .when(cacheClient)
         .findMissingDigests(any(), any());
 
-    return new RemoteCache(cacheClient, remoteOptions, DIGEST_UTIL);
+    return new RemoteCache(
+        CacheCapabilities.getDefaultInstance(), cacheClient, remoteOptions, DIGEST_UTIL);
   }
 
   private ByteStreamBuildEventArtifactUploader newArtifactUploader(RemoteCache remoteCache) {
diff --git a/src/test/java/com/google/devtools/build/lib/remote/FakeActionInputFileCache.java b/src/test/java/com/google/devtools/build/lib/remote/FakeActionInputFileCache.java
index a876bc4..c11f2e2 100644
--- a/src/test/java/com/google/devtools/build/lib/remote/FakeActionInputFileCache.java
+++ b/src/test/java/com/google/devtools/build/lib/remote/FakeActionInputFileCache.java
@@ -27,6 +27,7 @@
 import com.google.devtools.build.lib.vfs.FileStatus;
 import com.google.devtools.build.lib.vfs.FileSystemUtils;
 import com.google.devtools.build.lib.vfs.Path;
+import com.google.devtools.build.lib.vfs.PathFragment;
 import com.google.devtools.build.lib.vfs.Symlinks;
 import com.google.devtools.build.lib.vfs.SyscallCache;
 import java.io.IOException;
@@ -77,4 +78,13 @@
     setDigest(input, digest.getHash());
     return digest;
   }
+
+  public Digest createScratchInputSymlink(ActionInput input, String target) throws IOException {
+    Path inputFile = execRoot.getRelative(input.getExecPath());
+    inputFile.getParentDirectory().createDirectoryAndParents();
+    inputFile.createSymbolicLink(PathFragment.create(target));
+    Digest digest = digestUtil.compute(inputFile);
+    setDigest(input, digest.getHash());
+    return digest;
+  }
 }
diff --git a/src/test/java/com/google/devtools/build/lib/remote/GrpcCacheClientTest.java b/src/test/java/com/google/devtools/build/lib/remote/GrpcCacheClientTest.java
index 5bb3122..4df12d4 100644
--- a/src/test/java/com/google/devtools/build/lib/remote/GrpcCacheClientTest.java
+++ b/src/test/java/com/google/devtools/build/lib/remote/GrpcCacheClientTest.java
@@ -26,6 +26,7 @@
 import build.bazel.remote.execution.v2.Action;
 import build.bazel.remote.execution.v2.ActionCacheGrpc.ActionCacheImplBase;
 import build.bazel.remote.execution.v2.ActionResult;
+import build.bazel.remote.execution.v2.CacheCapabilities;
 import build.bazel.remote.execution.v2.Command;
 import build.bazel.remote.execution.v2.ContentAddressableStorageGrpc.ContentAddressableStorageImplBase;
 import build.bazel.remote.execution.v2.Digest;
@@ -98,7 +99,8 @@
   public void testVirtualActionInputSupport() throws Exception {
     RemoteOptions options = Options.getDefaults(RemoteOptions.class);
     RemoteExecutionCache client =
-        new RemoteExecutionCache(newClient(options), options, DIGEST_UTIL);
+        new RemoteExecutionCache(
+            CacheCapabilities.getDefaultInstance(), newClient(options), options, DIGEST_UTIL);
     PathFragment execPath = PathFragment.create("my/exec/path");
     VirtualActionInput virtualActionInput =
         ActionsTestUtil.createVirtualActionInput(execPath, "hello");
@@ -286,7 +288,8 @@
     // arrange
     RemoteOptions remoteOptions = Options.getDefaults(RemoteOptions.class);
     GrpcCacheClient client = newClient(remoteOptions);
-    RemoteCache remoteCache = new RemoteCache(client, remoteOptions, DIGEST_UTIL);
+    RemoteCache remoteCache =
+        new RemoteCache(CacheCapabilities.getDefaultInstance(), client, remoteOptions, DIGEST_UTIL);
 
     Digest fooDigest = DIGEST_UTIL.computeAsUtf8("foo-contents");
     Digest barDigest = DIGEST_UTIL.computeAsUtf8("bar-contents");
@@ -309,7 +312,8 @@
   public void testUploadDirectory() throws Exception {
     RemoteOptions remoteOptions = Options.getDefaults(RemoteOptions.class);
     GrpcCacheClient client = newClient(remoteOptions);
-    RemoteCache remoteCache = new RemoteCache(client, remoteOptions, DIGEST_UTIL);
+    RemoteCache remoteCache =
+        new RemoteCache(CacheCapabilities.getDefaultInstance(), client, remoteOptions, DIGEST_UTIL);
 
     final Digest fooDigest =
         fakeFileCache.createScratchInput(ActionInputHelper.fromPath("a/foo"), "xyz");
@@ -373,7 +377,8 @@
   public void testUploadDirectoryEmpty() throws Exception {
     RemoteOptions remoteOptions = Options.getDefaults(RemoteOptions.class);
     GrpcCacheClient client = newClient(remoteOptions);
-    RemoteCache remoteCache = new RemoteCache(client, remoteOptions, DIGEST_UTIL);
+    RemoteCache remoteCache =
+        new RemoteCache(CacheCapabilities.getDefaultInstance(), client, remoteOptions, DIGEST_UTIL);
 
     final Digest barDigest =
         fakeFileCache.createScratchInputDirectory(
@@ -412,7 +417,8 @@
   public void testUploadDirectoryNested() throws Exception {
     RemoteOptions remoteOptions = Options.getDefaults(RemoteOptions.class);
     GrpcCacheClient client = newClient(remoteOptions);
-    RemoteCache remoteCache = new RemoteCache(client, remoteOptions, DIGEST_UTIL);
+    RemoteCache remoteCache =
+        new RemoteCache(CacheCapabilities.getDefaultInstance(), client, remoteOptions, DIGEST_UTIL);
 
     final Digest wobbleDigest =
         fakeFileCache.createScratchInput(ActionInputHelper.fromPath("bar/test/wobble"), "xyz");
@@ -480,6 +486,7 @@
     UploadManifest uploadManifest =
         UploadManifest.create(
             remoteCache.options,
+            remoteCache.cacheCapabilities,
             remoteCache.digestUtil,
             remotePathResolver,
             actionKey,
@@ -565,7 +572,8 @@
     serviceRegistry.addService(ServerInterceptors.intercept(actionCache, interceptor));
 
     GrpcCacheClient client = newClient(remoteOptions);
-    RemoteCache remoteCache = new RemoteCache(client, remoteOptions, DIGEST_UTIL);
+    RemoteCache remoteCache =
+        new RemoteCache(CacheCapabilities.getDefaultInstance(), client, remoteOptions, DIGEST_UTIL);
     remoteCache.downloadActionResult(
         context,
         DIGEST_UTIL.asActionKey(DIGEST_UTIL.computeAsUtf8("key")),
@@ -576,7 +584,8 @@
   public void testUpload() throws Exception {
     RemoteOptions remoteOptions = Options.getDefaults(RemoteOptions.class);
     GrpcCacheClient client = newClient(remoteOptions);
-    RemoteCache remoteCache = new RemoteCache(client, remoteOptions, DIGEST_UTIL);
+    RemoteCache remoteCache =
+        new RemoteCache(CacheCapabilities.getDefaultInstance(), client, remoteOptions, DIGEST_UTIL);
 
     final Digest fooDigest =
         fakeFileCache.createScratchInput(ActionInputHelper.fromPath("a/foo"), "xyz");
@@ -652,7 +661,8 @@
     RemoteOptions remoteOptions = Options.getDefaults(RemoteOptions.class);
     remoteOptions.maxOutboundMessageSize = 80; // Enough for one digest, but not two.
     GrpcCacheClient client = newClient(remoteOptions);
-    RemoteCache remoteCache = new RemoteCache(client, remoteOptions, DIGEST_UTIL);
+    RemoteCache remoteCache =
+        new RemoteCache(CacheCapabilities.getDefaultInstance(), client, remoteOptions, DIGEST_UTIL);
 
     final Digest fooDigest =
         fakeFileCache.createScratchInput(ActionInputHelper.fromPath("a/foo"), "xyz");
@@ -717,7 +727,8 @@
   public void testUploadCacheMissesWithRetries() throws Exception {
     RemoteOptions remoteOptions = Options.getDefaults(RemoteOptions.class);
     GrpcCacheClient client = newClient(remoteOptions);
-    RemoteCache remoteCache = new RemoteCache(client, remoteOptions, DIGEST_UTIL);
+    RemoteCache remoteCache =
+        new RemoteCache(CacheCapabilities.getDefaultInstance(), client, remoteOptions, DIGEST_UTIL);
 
     final Digest fooDigest =
         fakeFileCache.createScratchInput(ActionInputHelper.fromPath("a/foo"), "xyz");
diff --git a/src/test/java/com/google/devtools/build/lib/remote/InMemoryRemoteCache.java b/src/test/java/com/google/devtools/build/lib/remote/InMemoryRemoteCache.java
index 3a79cac..4c8e3e1 100644
--- a/src/test/java/com/google/devtools/build/lib/remote/InMemoryRemoteCache.java
+++ b/src/test/java/com/google/devtools/build/lib/remote/InMemoryRemoteCache.java
@@ -15,6 +15,7 @@
 
 import static java.nio.charset.StandardCharsets.UTF_8;
 
+import build.bazel.remote.execution.v2.CacheCapabilities;
 import build.bazel.remote.execution.v2.Digest;
 import com.google.devtools.build.lib.remote.common.RemoteActionExecutionContext;
 import com.google.devtools.build.lib.remote.common.RemoteCacheClient;
@@ -30,19 +31,21 @@
 class InMemoryRemoteCache extends RemoteExecutionCache {
 
   InMemoryRemoteCache(
-      Map<Digest, byte[]> casEntries,
-      RemoteOptions options,
-      DigestUtil digestUtil) {
-    super(new InMemoryCacheClient(casEntries), options, digestUtil);
+      Map<Digest, byte[]> casEntries, RemoteOptions options, DigestUtil digestUtil) {
+    super(
+        CacheCapabilities.getDefaultInstance(),
+        new InMemoryCacheClient(casEntries),
+        options,
+        digestUtil);
   }
 
   InMemoryRemoteCache(RemoteOptions options, DigestUtil digestUtil) {
-    super(new InMemoryCacheClient(), options, digestUtil);
+    super(CacheCapabilities.getDefaultInstance(), new InMemoryCacheClient(), options, digestUtil);
   }
 
   InMemoryRemoteCache(
       RemoteCacheClient cacheProtocol, RemoteOptions options, DigestUtil digestUtil) {
-    super(cacheProtocol, options, digestUtil);
+    super(CacheCapabilities.getDefaultInstance(), cacheProtocol, options, digestUtil);
   }
 
   Digest addContents(RemoteActionExecutionContext context, String txt)
diff --git a/src/test/java/com/google/devtools/build/lib/remote/RemoteActionInputFetcherTest.java b/src/test/java/com/google/devtools/build/lib/remote/RemoteActionInputFetcherTest.java
index 4b13e10..352f258 100644
--- a/src/test/java/com/google/devtools/build/lib/remote/RemoteActionInputFetcherTest.java
+++ b/src/test/java/com/google/devtools/build/lib/remote/RemoteActionInputFetcherTest.java
@@ -15,6 +15,7 @@
 
 import static com.google.common.truth.Truth.assertThat;
 
+import build.bazel.remote.execution.v2.CacheCapabilities;
 import build.bazel.remote.execution.v2.Digest;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.Maps;
@@ -107,6 +108,10 @@
           DigestUtil.buildDigest(entry.getKey().asBytes(), entry.getValue().length),
           entry.getValue());
     }
-    return new RemoteCache(new InMemoryCacheClient(cacheEntries), options, digestUtil);
+    return new RemoteCache(
+        CacheCapabilities.getDefaultInstance(),
+        new InMemoryCacheClient(cacheEntries),
+        options,
+        digestUtil);
   }
 }
diff --git a/src/test/java/com/google/devtools/build/lib/remote/RemoteCacheTest.java b/src/test/java/com/google/devtools/build/lib/remote/RemoteCacheTest.java
index ff70a90..196938e 100644
--- a/src/test/java/com/google/devtools/build/lib/remote/RemoteCacheTest.java
+++ b/src/test/java/com/google/devtools/build/lib/remote/RemoteCacheTest.java
@@ -22,6 +22,7 @@
 import static org.mockito.Mockito.spy;
 
 import build.bazel.remote.execution.v2.ActionResult;
+import build.bazel.remote.execution.v2.CacheCapabilities;
 import build.bazel.remote.execution.v2.Digest;
 import build.bazel.remote.execution.v2.RequestMetadata;
 import com.google.common.collect.ImmutableList;
@@ -588,11 +589,18 @@
   }
 
   private RemoteCache newRemoteCache(RemoteCacheClient remoteCacheClient) {
-    return new RemoteCache(remoteCacheClient, Options.getDefaults(RemoteOptions.class), digestUtil);
+    return new RemoteCache(
+        CacheCapabilities.getDefaultInstance(),
+        remoteCacheClient,
+        Options.getDefaults(RemoteOptions.class),
+        digestUtil);
   }
 
   private RemoteExecutionCache newRemoteExecutionCache(RemoteCacheClient remoteCacheClient) {
     return new RemoteExecutionCache(
-        remoteCacheClient, Options.getDefaults(RemoteOptions.class), digestUtil);
+        CacheCapabilities.getDefaultInstance(),
+        remoteCacheClient,
+        Options.getDefaults(RemoteOptions.class),
+        digestUtil);
   }
 }
diff --git a/src/test/java/com/google/devtools/build/lib/remote/RemoteExecutionServiceTest.java b/src/test/java/com/google/devtools/build/lib/remote/RemoteExecutionServiceTest.java
index a52073c..4fb988a6 100644
--- a/src/test/java/com/google/devtools/build/lib/remote/RemoteExecutionServiceTest.java
+++ b/src/test/java/com/google/devtools/build/lib/remote/RemoteExecutionServiceTest.java
@@ -34,6 +34,7 @@
 import static org.mockito.Mockito.when;
 
 import build.bazel.remote.execution.v2.ActionResult;
+import build.bazel.remote.execution.v2.CacheCapabilities;
 import build.bazel.remote.execution.v2.Digest;
 import build.bazel.remote.execution.v2.Directory;
 import build.bazel.remote.execution.v2.DirectoryNode;
@@ -43,6 +44,7 @@
 import build.bazel.remote.execution.v2.OutputSymlink;
 import build.bazel.remote.execution.v2.Platform;
 import build.bazel.remote.execution.v2.RequestMetadata;
+import build.bazel.remote.execution.v2.SymlinkAbsolutePathStrategy;
 import build.bazel.remote.execution.v2.SymlinkNode;
 import build.bazel.remote.execution.v2.Tree;
 import com.google.common.base.Throwables;
@@ -1367,11 +1369,10 @@
         .isEmpty();
   }
 
-  @Test
-  public void uploadOutputs_uploadRelativeDanglingSymlink_works() throws Exception {
+  private void doUploadDanglingSymlink(PathFragment targetPath) throws Exception {
     // arrange
     Path linkPath = execRoot.getRelative("outputs/link");
-    linkPath.createSymbolicLink(PathFragment.create("some/path"));
+    linkPath.createSymbolicLink(targetPath);
     Artifact outputSymlink =
         ActionsTestUtil.createUnresolvedSymlinkArtifactWithExecPath(
             artifactRoot, linkPath.relativeTo(execRoot));
@@ -1392,11 +1393,30 @@
 
     // assert
     ActionResult.Builder expectedResult = ActionResult.newBuilder();
-    expectedResult.addOutputFileSymlinksBuilder().setPath("outputs/link").setTarget("some/path");
+    expectedResult
+        .addOutputFileSymlinksBuilder()
+        .setPath("outputs/link")
+        .setTarget(targetPath.toString());
     assertThat(manifest.getActionResult()).isEqualTo(expectedResult.build());
   }
 
   @Test
+  public void uploadOutputs_uploadRelativeDanglingSymlink() throws Exception {
+    doUploadDanglingSymlink(PathFragment.create("some/path"));
+  }
+
+  @Test
+  public void uploadOutputs_uploadAbsoluteDanglingSymlink() throws Exception {
+    when(cache.getCacheCapabilities())
+        .thenReturn(
+            CacheCapabilities.newBuilder()
+                .setSymlinkAbsolutePathStrategy(SymlinkAbsolutePathStrategy.Value.ALLOWED)
+                .build());
+
+    doUploadDanglingSymlink(PathFragment.create("/some/path"));
+  }
+
+  @Test
   public void uploadOutputs_emptyOutputs_doNotPerformUpload() throws Exception {
     // Test that uploading an empty output does not try to perform an upload.
 
diff --git a/src/test/java/com/google/devtools/build/lib/remote/RemoteModuleTest.java b/src/test/java/com/google/devtools/build/lib/remote/RemoteModuleTest.java
index a01856e..2deeff8 100644
--- a/src/test/java/com/google/devtools/build/lib/remote/RemoteModuleTest.java
+++ b/src/test/java/com/google/devtools/build/lib/remote/RemoteModuleTest.java
@@ -25,6 +25,7 @@
 import build.bazel.remote.execution.v2.ExecutionCapabilities;
 import build.bazel.remote.execution.v2.GetCapabilitiesRequest;
 import build.bazel.remote.execution.v2.ServerCapabilities;
+import build.bazel.remote.execution.v2.SymlinkAbsolutePathStrategy;
 import com.google.auth.Credentials;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableMap;
@@ -76,6 +77,32 @@
 @RunWith(JUnit4.class)
 public final class RemoteModuleTest {
 
+  private static final ServerCapabilities CACHE_ONLY_CAPS =
+      ServerCapabilities.newBuilder()
+          .setLowApiVersion(ApiVersion.current.toSemVer())
+          .setHighApiVersion(ApiVersion.current.toSemVer())
+          .setCacheCapabilities(
+              CacheCapabilities.newBuilder()
+                  .addDigestFunctions(Value.SHA256)
+                  .setActionCacheUpdateCapabilities(
+                      ActionCacheUpdateCapabilities.newBuilder().setUpdateEnabled(true).build())
+                  .setSymlinkAbsolutePathStrategy(SymlinkAbsolutePathStrategy.Value.ALLOWED)
+                  .build())
+          .build();
+
+  private static final ServerCapabilities EXEC_AND_CACHE_CAPS =
+      ServerCapabilities.newBuilder()
+          .setLowApiVersion(ApiVersion.current.toSemVer())
+          .setHighApiVersion(ApiVersion.current.toSemVer())
+          .setExecutionCapabilities(
+              ExecutionCapabilities.newBuilder()
+                  .setExecEnabled(true)
+                  .setDigestFunction(Value.SHA256)
+                  .build())
+          .setCacheCapabilities(
+              CacheCapabilities.newBuilder().addDigestFunctions(Value.SHA256).build())
+          .build();
+
   private static CommandEnvironment createTestCommandEnvironment(RemoteOptions remoteOptions)
       throws IOException, AbruptExitException {
     CoreOptions coreOptions = Options.getDefaults(CoreOptions.class);
@@ -157,19 +184,7 @@
 
   @Test
   public void testVerifyCapabilities_executionAndCacheForSingleEndpoint() throws Exception {
-    ServerCapabilities caps =
-        ServerCapabilities.newBuilder()
-            .setLowApiVersion(ApiVersion.current.toSemVer())
-            .setHighApiVersion(ApiVersion.current.toSemVer())
-            .setExecutionCapabilities(
-                ExecutionCapabilities.newBuilder()
-                    .setExecEnabled(true)
-                    .setDigestFunction(Value.SHA256)
-                    .build())
-            .setCacheCapabilities(
-                CacheCapabilities.newBuilder().addDigestFunctions(Value.SHA256).build())
-            .build();
-    CapabilitiesImpl executionServerCapabilitiesImpl = new CapabilitiesImpl(caps);
+    CapabilitiesImpl executionServerCapabilitiesImpl = new CapabilitiesImpl(EXEC_AND_CACHE_CAPS);
     String executionServerName = "execution-server";
     Server executionServer = createFakeServer(executionServerName, executionServerCapabilitiesImpl);
     executionServer.start();
@@ -197,18 +212,7 @@
 
   @Test
   public void testVerifyCapabilities_cacheOnlyEndpoint() throws Exception {
-    ServerCapabilities cacheOnlyCaps =
-        ServerCapabilities.newBuilder()
-            .setLowApiVersion(ApiVersion.current.toSemVer())
-            .setHighApiVersion(ApiVersion.current.toSemVer())
-            .setCacheCapabilities(
-                CacheCapabilities.newBuilder()
-                    .addDigestFunctions(Value.SHA256)
-                    .setActionCacheUpdateCapabilities(
-                        ActionCacheUpdateCapabilities.newBuilder().setUpdateEnabled(true).build())
-                    .build())
-            .build();
-    CapabilitiesImpl cacheServerCapabilitiesImpl = new CapabilitiesImpl(cacheOnlyCaps);
+    CapabilitiesImpl cacheServerCapabilitiesImpl = new CapabilitiesImpl(CACHE_ONLY_CAPS);
     String cacheServerName = "cache-server";
     Server cacheServer = createFakeServer(cacheServerName, cacheServerCapabilitiesImpl);
     cacheServer.start();
@@ -505,4 +509,68 @@
     assertThat(credentials.getRequestMetadata(URI.create("https://foo.example.org"))).isNotEmpty();
     assertThat(credentials.getRequestMetadata(URI.create("https://bar.example.org"))).isEmpty();
   }
+
+  @Test
+  public void testCacheCapabilities_propagatedToRemoteCache() throws Exception {
+    CapabilitiesImpl cacheServerCapabilitiesImpl = new CapabilitiesImpl(CACHE_ONLY_CAPS);
+    String cacheServerName = "cache-server";
+    Server cacheServer = createFakeServer(cacheServerName, cacheServerCapabilitiesImpl);
+    cacheServer.start();
+
+    try {
+      RemoteModule remoteModule = new RemoteModule();
+      remoteModule.setChannelFactory(
+          (target, proxy, options, interceptors) ->
+              InProcessChannelBuilder.forName(target).directExecutor().build());
+
+      RemoteOptions remoteOptions = Options.getDefaults(RemoteOptions.class);
+      remoteOptions.remoteCache = cacheServerName;
+
+      CommandEnvironment env = createTestCommandEnvironment(remoteOptions);
+
+      remoteModule.beforeCommand(env);
+
+      assertThat(Thread.interrupted()).isFalse();
+      RemoteActionContextProvider actionContextProvider = remoteModule.getActionContextProvider();
+      assertThat(actionContextProvider).isNotNull();
+      assertThat(actionContextProvider.getRemoteCache()).isNotNull();
+      assertThat(actionContextProvider.getRemoteCache().getCacheCapabilities())
+          .isEqualTo(CACHE_ONLY_CAPS.getCacheCapabilities());
+    } finally {
+      cacheServer.shutdownNow();
+      cacheServer.awaitTermination();
+    }
+  }
+
+  @Test
+  public void testCacheCapabilities_propagatedToRemoteExecutionCache() throws Exception {
+    CapabilitiesImpl executionServerCapabilitiesImpl = new CapabilitiesImpl(EXEC_AND_CACHE_CAPS);
+    String executionServerName = "execution-server";
+    Server executionServer = createFakeServer(executionServerName, executionServerCapabilitiesImpl);
+    executionServer.start();
+
+    try {
+      RemoteModule remoteModule = new RemoteModule();
+      remoteModule.setChannelFactory(
+          (target, proxy, options, interceptors) ->
+              InProcessChannelBuilder.forName(target).directExecutor().build());
+
+      RemoteOptions remoteOptions = Options.getDefaults(RemoteOptions.class);
+      remoteOptions.remoteExecutor = executionServerName;
+
+      CommandEnvironment env = createTestCommandEnvironment(remoteOptions);
+
+      remoteModule.beforeCommand(env);
+
+      assertThat(Thread.interrupted()).isFalse();
+      RemoteActionContextProvider actionContextProvider = remoteModule.getActionContextProvider();
+      assertThat(actionContextProvider).isNotNull();
+      assertThat(actionContextProvider.getRemoteCache()).isNotNull();
+      assertThat(actionContextProvider.getRemoteCache().getCacheCapabilities())
+          .isEqualTo(EXEC_AND_CACHE_CAPS.getCacheCapabilities());
+    } finally {
+      executionServer.shutdownNow();
+      executionServer.awaitTermination();
+    }
+  }
 }
diff --git a/src/test/java/com/google/devtools/build/lib/remote/RemoteSpawnRunnerWithGrpcRemoteExecutorTest.java b/src/test/java/com/google/devtools/build/lib/remote/RemoteSpawnRunnerWithGrpcRemoteExecutorTest.java
index 0559485..ccbfd7a 100644
--- a/src/test/java/com/google/devtools/build/lib/remote/RemoteSpawnRunnerWithGrpcRemoteExecutorTest.java
+++ b/src/test/java/com/google/devtools/build/lib/remote/RemoteSpawnRunnerWithGrpcRemoteExecutorTest.java
@@ -27,6 +27,7 @@
 
 import build.bazel.remote.execution.v2.ActionCacheGrpc.ActionCacheImplBase;
 import build.bazel.remote.execution.v2.ActionResult;
+import build.bazel.remote.execution.v2.CacheCapabilities;
 import build.bazel.remote.execution.v2.Command;
 import build.bazel.remote.execution.v2.ContentAddressableStorageGrpc.ContentAddressableStorageImplBase;
 import build.bazel.remote.execution.v2.Digest;
@@ -292,7 +293,8 @@
         new GrpcCacheClient(
             channel.retain(), callCredentialsProvider, remoteOptions, retrier, DIGEST_UTIL);
     RemoteExecutionCache remoteCache =
-        new RemoteExecutionCache(cacheProtocol, remoteOptions, DIGEST_UTIL);
+        new RemoteExecutionCache(
+            CacheCapabilities.getDefaultInstance(), cacheProtocol, remoteOptions, DIGEST_UTIL);
     RemoteExecutionService remoteExecutionService =
         new RemoteExecutionService(
             directExecutor(),
diff --git a/src/test/java/com/google/devtools/build/lib/remote/UploadManifestTest.java b/src/test/java/com/google/devtools/build/lib/remote/UploadManifestTest.java
index 21116c4..bc75807 100644
--- a/src/test/java/com/google/devtools/build/lib/remote/UploadManifestTest.java
+++ b/src/test/java/com/google/devtools/build/lib/remote/UploadManifestTest.java
@@ -207,7 +207,8 @@
             remotePathResolver,
             result,
             /*followSymlinks=*/ true,
-            /*allowDanglingSymlinks=*/ false);
+            /*allowDanglingSymlinks=*/ false,
+            /*allowAbsoluteSymlinks=*/ false);
     um.addFiles(ImmutableList.of(link));
     Digest digest = digestUtil.compute(target);
     assertThat(um.getDigestToFile()).containsExactly(digest, link);
@@ -233,7 +234,8 @@
             remotePathResolver,
             result,
             /*followSymlinks=*/ true,
-            /*allowDanglingSymlinks=*/ false);
+            /*allowDanglingSymlinks=*/ false,
+            /*allowAbsoluteSymlinks=*/ false);
     um.addFiles(ImmutableList.of(link));
     Digest digest = digestUtil.compute(foo);
     assertThat(um.getDigestToFile()).containsExactly(digest, execRoot.getRelative("link/foo"));
@@ -265,7 +267,8 @@
             remotePathResolver,
             result,
             /*followSymlinks=*/ false,
-            /*allowDanglingSymlinks=*/ false);
+            /*allowDanglingSymlinks=*/ false,
+            /*allowAbsoluteSymlinks=*/ false);
     um.addFiles(ImmutableList.of(link));
     Digest digest = digestUtil.compute(target);
     assertThat(um.getDigestToFile()).containsExactly(digest, link);
@@ -291,7 +294,8 @@
             remotePathResolver,
             result,
             /*followSymlinks=*/ false,
-            /*allowDanglingSymlinks=*/ false);
+            /*allowDanglingSymlinks=*/ false,
+            /*allowAbsoluteSymlinks=*/ false);
     um.addFiles(ImmutableList.of(link));
     Digest digest = digestUtil.compute(foo);
     assertThat(um.getDigestToFile()).containsExactly(digest, execRoot.getRelative("link/foo"));
@@ -323,7 +327,8 @@
             remotePathResolver,
             result,
             /*followSymlinks=*/ true,
-            /*allowDanglingSymlinks=*/ false);
+            /*allowDanglingSymlinks=*/ false,
+            /*allowAbsoluteSymlinks=*/ false);
     um.addFiles(ImmutableList.of(link));
     Digest digest = digestUtil.compute(target);
     assertThat(um.getDigestToFile()).containsExactly(digest, link);
@@ -349,7 +354,8 @@
             remotePathResolver,
             result,
             /*followSymlinks=*/ true,
-            /*allowDanglingSymlinks=*/ false);
+            /*allowDanglingSymlinks=*/ false,
+            /*allowAbsoluteSymlinks=*/ false);
     um.addFiles(ImmutableList.of(link));
     Digest digest = digestUtil.compute(foo);
     assertThat(um.getDigestToFile()).containsExactly(digest, execRoot.getRelative("link/foo"));
@@ -381,7 +387,8 @@
             remotePathResolver,
             result,
             /*followSymlinks=*/ false,
-            /*allowDanglingSymlinks=*/ false);
+            /*allowDanglingSymlinks=*/ false,
+            /*allowAbsoluteSymlinks=*/ false);
     um.addFiles(ImmutableList.of(link));
     assertThat(um.getDigestToFile()).isEmpty();
 
@@ -406,7 +413,8 @@
             remotePathResolver,
             result,
             /*followSymlinks=*/ false,
-            /*allowDanglingSymlinks=*/ false);
+            /*allowDanglingSymlinks=*/ false,
+            /*allowAbsoluteSymlinks=*/ false);
     um.addFiles(ImmutableList.of(link));
     assertThat(um.getDigestToFile()).isEmpty();
 
@@ -429,7 +437,8 @@
             remotePathResolver,
             result,
             /*followSymlinks=*/ false,
-            /*allowDanglingSymlinks=*/ false);
+            /*allowDanglingSymlinks=*/ false,
+            /*allowAbsoluteSymlinks=*/ false);
     IOException e = assertThrows(IOException.class, () -> um.addFiles(ImmutableList.of(link)));
     assertThat(e).hasMessageThat().contains("dangling");
     assertThat(e).hasMessageThat().contains("/execroot/link");
@@ -437,8 +446,9 @@
   }
 
   @Test
-  public void actionResult_noFollowSymlinks_allowDanglingSymlinks_absoluteDanglingSymlinkAsSymlink()
-      throws Exception {
+  public void
+      actionResult_noFollowSymlinks_allowDanglingSymlinks_noAllowAbsoluteSymlinks_absoluteDanglingSymlinkAsError()
+          throws Exception {
     ActionResult.Builder result = ActionResult.newBuilder();
     Path link = execRoot.getRelative("link");
     Path target = execRoot.getRelative("target");
@@ -450,7 +460,31 @@
             remotePathResolver,
             result,
             /*followSymlinks=*/ false,
-            /*allowDanglingSymlinks=*/ true);
+            /*allowDanglingSymlinks=*/ true,
+            /*allowAbsoluteSymlinks=*/ false);
+    IOException e = assertThrows(IOException.class, () -> um.addFiles(ImmutableList.of(link)));
+    assertThat(e).hasMessageThat().contains("absolute");
+    assertThat(e).hasMessageThat().contains("/execroot/link");
+    assertThat(e).hasMessageThat().contains("target");
+  }
+
+  @Test
+  public void
+      actionResult_noFollowSymlinks_allowDanglingSymlinks_allowAbsoluteSymlinks_absoluteDanglingSymlinkAsSymlink()
+          throws Exception {
+    ActionResult.Builder result = ActionResult.newBuilder();
+    Path link = execRoot.getRelative("link");
+    Path target = execRoot.getRelative("target");
+    link.createSymbolicLink(target);
+
+    UploadManifest um =
+        new UploadManifest(
+            digestUtil,
+            remotePathResolver,
+            result,
+            /*followSymlinks=*/ false,
+            /*allowDanglingSymlinks=*/ true,
+            /*allowAbsoluteSymlinks=*/ true);
     um.addFiles(ImmutableList.of(link));
     assertThat(um.getDigestToFile()).isEmpty();
 
@@ -473,7 +507,8 @@
             remotePathResolver,
             result,
             /*followSymlinks=*/ false,
-            /*allowDanglingSymlinks=*/ false);
+            /*allowDanglingSymlinks=*/ false,
+            /*allowAbsoluteSymlinks=*/ false);
     IOException e = assertThrows(IOException.class, () -> um.addFiles(ImmutableList.of(link)));
     assertThat(e).hasMessageThat().contains("dangling");
     assertThat(e).hasMessageThat().contains("/execroot/link");
@@ -494,7 +529,8 @@
             remotePathResolver,
             result,
             /*followSymlinks=*/ false,
-            /*allowDanglingSymlinks=*/ true);
+            /*allowDanglingSymlinks=*/ true,
+            /*allowAbsoluteSymlinks=*/ false);
     um.addFiles(ImmutableList.of(link));
     assertThat(um.getDigestToFile()).isEmpty();
 
@@ -519,7 +555,8 @@
             remotePathResolver,
             result,
             /*followSymlinks=*/ true,
-            /*allowDanglingSymlinks=*/ false);
+            /*allowDanglingSymlinks=*/ false,
+            /*allowAbsoluteSymlinks=*/ false);
     um.addFiles(ImmutableList.of(dir));
     Digest digest = digestUtil.compute(target);
     assertThat(um.getDigestToFile()).containsExactly(digest, link);
@@ -556,7 +593,8 @@
             remotePathResolver,
             result,
             /*followSymlinks=*/ true,
-            /*allowDanglingSymlinks=*/ false);
+            /*allowDanglingSymlinks=*/ false,
+            /*allowAbsoluteSymlinks=*/ false);
     um.addFiles(ImmutableList.of(dir));
     Digest digest = digestUtil.compute(foo);
     assertThat(um.getDigestToFile()).containsExactly(digest, execRoot.getRelative("dir/link/foo"));
@@ -598,7 +636,8 @@
             remotePathResolver,
             result,
             /*followSymlinks=*/ false,
-            /*allowDanglingSymlinks=*/ false);
+            /*allowDanglingSymlinks=*/ false,
+            /*allowAbsoluteSymlinks=*/ false);
     um.addFiles(ImmutableList.of(dir));
     Digest digest = digestUtil.compute(target);
     assertThat(um.getDigestToFile()).containsExactly(digest, link);
@@ -635,7 +674,8 @@
             remotePathResolver,
             result,
             /*followSymlinks=*/ false,
-            /*allowDanglingSymlinks=*/ false);
+            /*allowDanglingSymlinks=*/ false,
+            /*allowAbsoluteSymlinks=*/ false);
     um.addFiles(ImmutableList.of(dir));
     Digest digest = digestUtil.compute(foo);
     assertThat(um.getDigestToFile()).containsExactly(digest, execRoot.getRelative("dir/link/foo"));
@@ -676,7 +716,8 @@
             remotePathResolver,
             result,
             /*followSymlinks=*/ true,
-            /*allowDanglingSymlinks=*/ false);
+            /*allowDanglingSymlinks=*/ false,
+            /*allowAbsoluteSymlinks=*/ false);
     um.addFiles(ImmutableList.of(dir));
     Digest digest = digestUtil.compute(target);
     assertThat(um.getDigestToFile()).containsExactly(digest, link);
@@ -713,7 +754,8 @@
             remotePathResolver,
             result,
             /*followSymlinks=*/ true,
-            /*allowDanglingSymlinks=*/ false);
+            /*allowDanglingSymlinks=*/ false,
+            /*allowAbsoluteSymlinks=*/ false);
     um.addFiles(ImmutableList.of(dir));
     Digest digest = digestUtil.compute(foo);
     assertThat(um.getDigestToFile()).containsExactly(digest, execRoot.getRelative("dir/link/foo"));
@@ -755,7 +797,8 @@
             remotePathResolver,
             result,
             /*followSymlinks=*/ false,
-            /*allowDanglingSymlinks=*/ false);
+            /*allowDanglingSymlinks=*/ false,
+            /*allowAbsoluteSymlinks=*/ false);
     um.addFiles(ImmutableList.of(dir));
     assertThat(um.getDigestToFile()).isEmpty();
 
@@ -791,7 +834,8 @@
             remotePathResolver,
             result,
             /*followSymlinks=*/ false,
-            /*allowDanglingSymlinks=*/ false);
+            /*allowDanglingSymlinks=*/ false,
+            /*allowAbsoluteSymlinks=*/ false);
     um.addFiles(ImmutableList.of(dir));
     assertThat(um.getDigestToFile()).isEmpty();
 
@@ -825,7 +869,8 @@
             remotePathResolver,
             result,
             /*followSymlinks=*/ false,
-            /*allowDanglingSymlinks=*/ false);
+            /*allowDanglingSymlinks=*/ false,
+            /*allowAbsoluteSymlinks=*/ false);
     IOException e = assertThrows(IOException.class, () -> um.addFiles(ImmutableList.of(dir)));
     assertThat(e).hasMessageThat().contains("dangling");
     assertThat(e).hasMessageThat().contains("/execroot/dir/link");
@@ -849,7 +894,8 @@
             remotePathResolver,
             result,
             /*followSymlinks=*/ false,
-            /*allowDanglingSymlinks=*/ true);
+            /*allowDanglingSymlinks=*/ true,
+            /*allowAbsoluteSymlinks=*/ false);
     IOException e = assertThrows(IOException.class, () -> um.addFiles(ImmutableList.of(dir)));
     assertThat(e).hasMessageThat().contains("dangling");
     assertThat(e).hasMessageThat().contains("/execroot/dir/link");
@@ -873,7 +919,8 @@
             remotePathResolver,
             result,
             /*followSymlinks=*/ false,
-            /*allowDanglingSymlinks=*/ false);
+            /*allowDanglingSymlinks=*/ false,
+            /*allowAbsoluteSymlinks=*/ false);
     um.addFiles(ImmutableList.of(dir));
     assertThat(um.getDigestToFile()).isEmpty();
 
@@ -907,7 +954,8 @@
             remotePathResolver,
             result,
             /*followSymlinks=*/ false,
-            /*allowDanglingSymlinks=*/ true);
+            /*allowDanglingSymlinks=*/ true,
+            /*allowAbsoluteSymlinks=*/ false);
     um.addFiles(ImmutableList.of(dir));
     assertThat(um.getDigestToFile()).isEmpty();
 
@@ -939,7 +987,8 @@
             remotePathResolver,
             result,
             /*followSymlinks=*/ false,
-            /*allowDanglingSymlinks=*/ false);
+            /*allowDanglingSymlinks=*/ false,
+            /*allowAbsoluteSymlinks=*/ false);
     UserExecException e =
         assertThrows(UserExecException.class, () -> um.addFiles(ImmutableList.of(special)));
     assertThat(e).hasMessageThat().contains("special file");
@@ -958,7 +1007,8 @@
             remotePathResolver,
             result,
             /*followSymlinks=*/ true,
-            /*allowDanglingSymlinks=*/ false);
+            /*allowDanglingSymlinks=*/ false,
+            /*allowAbsoluteSymlinks=*/ false);
     UserExecException e =
         assertThrows(UserExecException.class, () -> um.addFiles(ImmutableList.of(link)));
     assertThat(e).hasMessageThat().contains("special file");
@@ -976,7 +1026,8 @@
             remotePathResolver,
             result,
             /*followSymlinks=*/ false,
-            /*allowDanglingSymlinks=*/ false);
+            /*allowDanglingSymlinks=*/ false,
+            /*allowAbsoluteSymlinks=*/ false);
     UserExecException e =
         assertThrows(UserExecException.class, () -> um.addFiles(ImmutableList.of(dir)));
     assertThat(e).hasMessageThat().contains("special file");
@@ -994,7 +1045,8 @@
             remotePathResolver,
             result,
             /*followSymlinks=*/ true,
-            /*allowDanglingSymlinks=*/ false);
+            /*allowDanglingSymlinks=*/ false,
+            /*allowAbsoluteSymlinks=*/ false);
     UserExecException e =
         assertThrows(UserExecException.class, () -> um.addFiles(ImmutableList.of(dir)));
     assertThat(e).hasMessageThat().contains("special file");
diff --git a/src/tools/remote/src/main/java/com/google/devtools/build/remote/worker/ExecutionServer.java b/src/tools/remote/src/main/java/com/google/devtools/build/remote/worker/ExecutionServer.java
index be8a26a..fef1f08 100644
--- a/src/tools/remote/src/main/java/com/google/devtools/build/remote/worker/ExecutionServer.java
+++ b/src/tools/remote/src/main/java/com/google/devtools/build/remote/worker/ExecutionServer.java
@@ -356,6 +356,7 @@
         UploadManifest manifest =
             UploadManifest.create(
                 cache.getRemoteOptions(),
+                cache.getCacheCapabilities(),
                 digestUtil,
                 RemotePathResolver.createDefault(workingDirectory),
                 actionKey,
diff --git a/src/tools/remote/src/main/java/com/google/devtools/build/remote/worker/OnDiskBlobStoreCache.java b/src/tools/remote/src/main/java/com/google/devtools/build/remote/worker/OnDiskBlobStoreCache.java
index 300afcb..8ce78cf 100644
--- a/src/tools/remote/src/main/java/com/google/devtools/build/remote/worker/OnDiskBlobStoreCache.java
+++ b/src/tools/remote/src/main/java/com/google/devtools/build/remote/worker/OnDiskBlobStoreCache.java
@@ -15,10 +15,12 @@
 
 import static com.google.devtools.build.lib.remote.util.Utils.getFromFuture;
 
+import build.bazel.remote.execution.v2.CacheCapabilities;
 import build.bazel.remote.execution.v2.Digest;
 import build.bazel.remote.execution.v2.Directory;
 import build.bazel.remote.execution.v2.DirectoryNode;
 import build.bazel.remote.execution.v2.FileNode;
+import build.bazel.remote.execution.v2.SymlinkAbsolutePathStrategy;
 import build.bazel.remote.execution.v2.SymlinkNode;
 import com.google.devtools.build.lib.remote.RemoteCache;
 import com.google.devtools.build.lib.remote.common.RemoteActionExecutionContext;
@@ -32,8 +34,14 @@
 /** A {@link RemoteCache} backed by an {@link DiskCacheClient}. */
 class OnDiskBlobStoreCache extends RemoteCache {
 
+  private static final CacheCapabilities CAPABILITIES =
+      CacheCapabilities.newBuilder()
+          .setSymlinkAbsolutePathStrategy(SymlinkAbsolutePathStrategy.Value.ALLOWED)
+          .build();
+
   public OnDiskBlobStoreCache(RemoteOptions options, Path cacheDir, DigestUtil digestUtil) {
     super(
+        CAPABILITIES,
         new DiskCacheClient(cacheDir, /* verifyDownloads= */ true, digestUtil),
         options,
         digestUtil);