Change the behavior of RESOLVE to log a warning instead of crashing if a relative symlink escapes a Fileset.

PiperOrigin-RevId: 210731120
diff --git a/src/main/java/com/google/devtools/build/lib/exec/FilesetManifest.java b/src/main/java/com/google/devtools/build/lib/exec/FilesetManifest.java
index af6232b..40e3842 100644
--- a/src/main/java/com/google/devtools/build/lib/exec/FilesetManifest.java
+++ b/src/main/java/com/google/devtools/build/lib/exec/FilesetManifest.java
@@ -15,25 +15,31 @@
 
 import static java.nio.charset.StandardCharsets.UTF_8;
 
+import com.google.common.base.Preconditions;
 import com.google.common.collect.ImmutableMap;
 import com.google.common.io.LineProcessor;
 import com.google.devtools.build.lib.actions.FileArtifactValue;
 import com.google.devtools.build.lib.actions.FilesetOutputSymlink;
 import com.google.devtools.build.lib.analysis.AnalysisUtils;
 import com.google.devtools.build.lib.vfs.FileSystemUtils;
+import com.google.devtools.build.lib.vfs.IORuntimeException;
 import com.google.devtools.build.lib.vfs.Path;
 import com.google.devtools.build.lib.vfs.PathFragment;
 import java.io.IOException;
 import java.util.Collections;
 import java.util.HashMap;
 import java.util.LinkedHashMap;
+import java.util.LinkedHashSet;
 import java.util.List;
 import java.util.Map;
+import java.util.logging.Logger;
 
 /**
  * Representation of a Fileset manifest.
  */
 public final class FilesetManifest {
+  private static final Logger logger = Logger.getLogger(FilesetManifest.class.getName());
+
   /**
    * Mode that determines how to handle relative target paths.
    */
@@ -44,13 +50,7 @@
     /** Give an error if a relative target path is encountered. */
     ERROR,
 
-    /**
-     * Attempt to locally resolve the relative target path. Consider a manifest with two entries,
-     * foo points to bar, and bar points to the absolute path /foobar. In that case, we can
-     * determine that foo actually points at /foobar. Throw an exception if the local resolution
-     * fails, e.g., if the target is not in the current manifest, or if it points at another
-     * symlink (we could theoretically resolve recursively, but that's more complexity).
-     */
+    /** Resolve all relative target paths. */
     RESOLVE;
   }
 
@@ -65,7 +65,7 @@
       return FileSystemUtils.asByteSource(file).asCharSource(UTF_8)
           .readLines(
               new ManifestLineProcessor(workspaceName, manifest, relSymlinkBehavior));
-    } catch (IllegalStateException e) {
+    } catch (IORuntimeException e) {
       // We can't throw IOException from getResult below, so we instead use an unchecked exception,
       // and convert it to an IOException here.
       throw new IOException(e.getMessage(), e);
@@ -89,7 +89,11 @@
         artifactValues.put(artifact, (FileArtifactValue) outputSymlink.getMetadata());
       }
     }
-    return constructFilesetManifest(entries, relativeLinks, artifactValues);
+    try {
+      return constructFilesetManifest(entries, relativeLinks, artifactValues);
+    } catch (IORuntimeException e) {
+      throw new IOException(e.getMessage(), e);
+    }
   }
 
   private static final class ManifestLineProcessor implements LineProcessor<FilesetManifest> {
@@ -178,30 +182,61 @@
     }
   }
 
+  private static final int MAX_SYMLINK_TRAVERSALS = 256;
+
   private static FilesetManifest constructFilesetManifest(
-      Map<PathFragment, String> entries, Map<PathFragment, String> relativeLinks,
-      Map<String, FileArtifactValue> artifactValues) {
-    // Resolve relative symlinks if possible. Note that relativeLinks only contains entries in
-    // RESOLVE mode.
+      Map<PathFragment, String> entries,
+      Map<PathFragment, String> relativeLinks,
+      Map<String, FileArtifactValue> artifactValues)
+      throws IORuntimeException {
+    // Resolve relative symlinks. Note that relativeLinks only contains entries in RESOLVE mode.
+    // We must find targets for these symlinks that are not inside the Fileset itself.
     for (Map.Entry<PathFragment, String> e : relativeLinks.entrySet()) {
       PathFragment location = e.getKey();
       String value = e.getValue();
-      PathFragment actualLocation = location.getParentDirectory().getRelative(value);
-      String actual = entries.get(actualLocation);
+      String actual = Preconditions.checkNotNull(value, e);
+      Preconditions.checkState(!actual.startsWith("/"), e);
+      PathFragment actualLocation = location;
+      // Recursively resolve relative symlinks.
+      LinkedHashSet<String> seen = new LinkedHashSet<>();
+      int i = 0;
+      do {
+        actualLocation = actualLocation.getParentDirectory().getRelative(actual);
+        actual = entries.get(actualLocation);
+      } while (actual != null
+          && !actual.startsWith("/")
+          && seen.add(actual)
+          && ++i < MAX_SYMLINK_TRAVERSALS);
       if (actual == null) {
-        throw new IllegalStateException(
-            String.format(
-                "runfiles target '%s' is a relative symlink, and could not be resolved within the "
-                    + "same Fileset",
-                value));
+        // We've found a relative symlink that points out of the fileset. We should really always
+        // throw here, but current behavior is that we tolerate such symlinks when they occur in
+        // runfiles, which is the only time this code is hit.
+        // TODO(b/113128395): throw here.
+        logger.warning(
+            "Symlink "
+                + location
+                + " (transitively) points to "
+                + actualLocation
+                + " that is not in this fileset (or was pruned because of a cycle)");
+        entries.remove(location);
+      } else if (i >= MAX_SYMLINK_TRAVERSALS) {
+        logger.warning(
+            "Symlink "
+                + location
+                + " is part of a chain of length at least "
+                + i
+                + " which exceeds Blaze's maximum allowable symlink chain length");
+        entries.remove(location);
+      } else if (!actual.startsWith("/")) {
+        // TODO(b/113128395): throw here.
+        logger.warning("Symlink " + location + " forms a symlink cycle: " + seen);
+        // Removing the entry here will lead to slightly vague log lines for the other entries in
+        // the cycle, since they will fail when they don't find this entry, as opposed to
+        // discovering their own cycles. But this log line should be informative enough.
+        entries.remove(location);
+      } else {
+        entries.put(location, actual);
       }
-      if (!actual.startsWith("/")) {
-        throw new IllegalStateException(
-            String.format(
-                "runfiles target '%s' is a relative symlink, and points to another symlink",
-                value));
-      }
-      entries.put(location, actual);
     }
     return new FilesetManifest(entries, artifactValues);
   }
@@ -222,5 +257,4 @@
   public Map<String, FileArtifactValue> getArtifactValues() {
     return artifactValues;
   }
-
-}
\ No newline at end of file
+}
diff --git a/src/test/java/com/google/devtools/build/lib/exec/FilesetManifestTest.java b/src/test/java/com/google/devtools/build/lib/exec/FilesetManifestTest.java
index 2db666e..93c484c 100644
--- a/src/test/java/com/google/devtools/build/lib/exec/FilesetManifestTest.java
+++ b/src/test/java/com/google/devtools/build/lib/exec/FilesetManifestTest.java
@@ -273,15 +273,10 @@
     ArtifactRoot outputRoot =
         ArtifactRoot.asDerivedRoot(fs.getPath("/root"), fs.getPath("/root/out"));
     Artifact artifact = new Artifact(fs.getPath("/root/out/foo"), outputRoot);
-    try {
-      FilesetManifest.parseManifestFile(artifact.getExecPath(), execRoot, "workspace", RESOLVE);
-      fail();
-    } catch (IOException e) {
-      assertThat(e).hasMessageThat()
-          .isEqualTo(
-              "runfiles target 'foo' is a relative symlink, and could not be resolved within the "
-              +  "same Fileset");
-    }
+    FilesetManifest filesetManifest =
+        FilesetManifest.parseManifestFile(artifact.getExecPath(), execRoot, "workspace", RESOLVE);
+    assertThat(filesetManifest.getEntries()).isEmpty();
+    assertThat(filesetManifest.getArtifactValues()).isEmpty();
   }
 
   @Test
@@ -297,14 +292,10 @@
     ArtifactRoot outputRoot =
         ArtifactRoot.asDerivedRoot(fs.getPath("/root"), fs.getPath("/root/out"));
     Artifact artifact = new Artifact(fs.getPath("/root/out/foo"), outputRoot);
-    try {
-      FilesetManifest.parseManifestFile(artifact.getExecPath(), execRoot, "workspace", RESOLVE);
-      fail();
-    } catch (IOException e) {
-      assertThat(e).hasMessageThat()
-          .isEqualTo(
-              "runfiles target 'foo' is a relative symlink, and points to another symlink");
-    }
+    FilesetManifest manifest =
+        FilesetManifest.parseManifestFile(artifact.getExecPath(), execRoot, "workspace", RESOLVE);
+    assertThat(manifest.getEntries()).isEmpty();
+    assertThat(manifest.getArtifactValues()).isEmpty();
   }
 
   /** Current behavior is first one wins. */