Repository: bazelbuild/bazel
Issue: I/O exception during sandboxed execution: input dependency could not be checked for modifications during execution
Labels: type: bug
P2
team-Local-Exec

### Description of the bug:


My build fails with `I/O exception during sandboxed execution: input dependency <path> could not be checked for modifications during execution`.  The error seems to be about a symlink inside a tree artifact, with the symlink pointing to a directory inside the same tree artifact, and only happens when using `--experimental_use_hermetic_linux_sandbox`.

Using bazel 5.1.1 (official binaries), and also reproduced with recent master at 4e6153e, on Ubuntu 20.04.

Simplified reproduction below.
I'm sure that I'm doing a number of questionable things that can be achieved in better ways, but the error looks more like a bug than a deliberate error message, so I figured I'd open an issue.

```
#!/bin/bash


cat << 'EOF' > .bazelrc
build --experimental_use_hermetic_linux_sandbox
build --experimental_use_sandboxfs
build --spawn_strategy=linux-sandbox
EOF


cat << 'EOF' > BUILD
load(":defs.bzl", "custom_python_binary", "generate_sum", "python_virtualenv")

python_virtualenv(
    name = "virtualenv",
    output = "env",
)

custom_python_binary(
    name = "add_numbers",
    main = "add_numbers.py",
    venv = "virtualenv",
)

generate_sum(
    name = "two_plus_two",
    a = 2,
    b = 2,
    output = "result.txt",
)
EOF


cat << 'EOF' > defs.bzl
# buildifier: disable=module-docstring
def _python_virtualenv_impl(ctx):
    test_directory = ctx.actions.declare_directory(ctx.attr.output)

    ctx.actions.run_shell(
        command = " && ".join([
            'mkdir -p "$DIR_PATH/lib/python3.8/site-packages"',
            'echo "add = lambda a, b: a + b" > "$DIR_PATH/lib/python3.8/site-packages/addition.py"',
            'ln -s lib "$DIR_PATH/lib64"',
            'echo "Change this to trigger rebuild: 1" > /dev/null',
        ]),
        outputs = [test_directory],
        env = {"DIR_PATH": test_directory.path},
    )

    return [DefaultInfo(files = depset([test_directory]))]

python_virtualenv = rule(
    implementation = _python_virtualenv_impl,
    attrs = {
        "output": attr.string(mandatory = True),
    },
)

def _custom_python_binary_impl(ctx):
    output_file = ctx.actions.declare_file(ctx.label.name)

    ctx.actions.write(output_file, """#!/bin/bash
set -Eeuo pipefail
BASE_PATH="$(dirname "${BASH_SOURCE[0]}")"
export PYTHONPATH="$BASE_PATH/env/lib/python3.8/site-packages"
exec python3 -B -s -S "%MAIN_FILE_PATH%" "$@"
""".replace("%MAIN_FILE_PATH%", ctx.file.main.path), is_executable = True)

    return [
        DefaultInfo(
            files = depset(direct = [output_file, ctx.file.main] + ctx.files.venv),
            runfiles = ctx.runfiles(files = [ctx.file.main] + ctx.files.venv),
            executable = output_file,
        ),
    ]

custom_python_binary = rule(
    implementation = _custom_python_binary_impl,
    attrs = {
        "main": attr.label(allow_single_file = [".py"], mandatory = True),
        "venv": attr.label(allow_files = True, mandatory = True),
    },
    executable = True,
)

def _generate_sum_impl(ctx):
    output_file = ctx.actions.declare_file(ctx.attr.output)

    ctx.actions.run_shell(
        inputs = ctx.files.tool,
        command = " && ".join([
            '"$TOOL_PATH" $A $B > "$OUTPUT_PATH"',
        ]),
        outputs = [output_file],
        env = {
            "A": str(ctx.attr.a),
            "B": str(ctx.attr.b),
            "TOOL_PATH": ctx.executable.tool.path,
            "OUTPUT_PATH": output_file.path,
        },
    )

    return [DefaultInfo(files = depset(direct = [output_file]))]

generate_sum = rule(
    implementation = _generate_sum_impl,
    attrs = {
        "a": attr.int(mandatory = True),
        "b": attr.int(mandatory = True),
        "output": attr.string(mandatory = True),
        "tool": attr.label(executable = True, cfg = "target", default = Label("//:add_numbers")),
    },
)
EOF


cat << 'EOF' > add_numbers.py
import sys
from addition import add

a = int(sys.argv[1])
b = int(sys.argv[2])
sum = add(a, b)
print(sum)
EOF


cat << 'EOF' > WORKSPACE
EOF


bazel clean --expunge
bazel build :two_plus_two
```

Output:

```
user@localhost:~/path$ ./bug_report.sh 
INFO: Starting clean (this may take a while). Consider using --async if the clean takes more than several minutes.
Starting local Bazel server and connecting to it...
INFO: Analyzed target //:two_plus_two (4 packages loaded, 9 targets configured).
INFO: Found 1 target...
ERROR: /home/user/path/BUILD:14:13: Action result.txt failed: I/O exception during sandboxed execution: input dependency /home/user/.cache/bazel/_bazel_user/c45d20c83f728d794469b8036365b9b4/execroot/__main__/bazel-out/k8-fastbuild/bin/env/lib64 could not be checked for modifications during execution.
Target //:two_plus_two failed to build
INFO: Elapsed time: 2.627s, Critical Path: 0.07s
INFO: 4 processes: 3 internal, 1 linux-sandbox.
FAILED: Build did NOT complete successfully
```

Workaround that makes the build work (and also works in the real codebase that this example is derived from), but I have no idea what this actually does (based on 4e6153e):

```
diff --git a/src/main/java/com/google/devtools/build/lib/sandbox/LinuxSandboxedSpawnRunner.java b/src/main/java/com/google/devtools/build/lib/sandbox/LinuxSandboxedSpawnRunner.java
index ffb7a5f3b7..eb075319e1 100644
--- a/src/main/java/com/google/devtools/build/lib/sandbox/LinuxSandboxedSpawnRunner.java
+++ b/src/main/java/com/google/devtools/build/lib/sandbox/LinuxSandboxedSpawnRunner.java
@@ -24,6 +24,7 @@ import com.google.devtools.build.lib.actions.ExecException;
 import com.google.devtools.build.lib.actions.ExecutionRequirements;
 import com.google.devtools.build.lib.actions.FileArtifactValue;
 import com.google.devtools.build.lib.actions.FileContentsProxy;
+import com.google.devtools.build.lib.actions.FileStateType;
 import com.google.devtools.build.lib.actions.ForbiddenActionInputException;
 import com.google.devtools.build.lib.actions.Spawn;
 import com.google.devtools.build.lib.actions.Spawns;
@@ -391,6 +392,11 @@ final class LinuxSandboxedSpawnRunner extends AbstractSandboxSpawnRunner {
       }
 
       FileArtifactValue metadata = context.getMetadataProvider().getMetadata(input);
+
+      if (metadata.getType() == FileStateType.DIRECTORY) {
+        continue;
+      }
+
       Path path = execRoot.getRelative(input.getExecPath());
 
       try {
```

### What's the simplest, easiest way to reproduce this bug? Please provide a minimal example if possible.


_No response_

### Which operating system are you running Bazel on?


_No response_

### What is the output of `bazel info release`?


_No response_

### If `bazel info release` returns `development version` or `(@non-git)`, tell us how you built Bazel.


_No response_

### What's the output of `git remote get-url origin; git rev-parse master; git rev-parse HEAD` ?


_No response_

### Have you found anything relevant by searching the web?

_No response_

### Any other information, logs, or outputs that you want to share?

_No response_

Issue comments:
Looping in @frazze-jobb who wrote the hermetic sandbox code.
Leaving out `--experimental_use_sandboxfs` doesn't change this but reduces the problem space.
@frazze-jobb is on another project now. I worked with him on this sandbox and answer instead.

Thanks for reporting this issue.

I will try to find time to investigate this and planning to create test cases for a few related special cases.

FYI, I'm encountering the same problem in the context of a hermtic cc_toolchain using a symlink.
Minimal reproducible example:
```
#!/bin/bash
set -exuo pipefail

# Create files for bazel
touch src.cc
mkdir a
ln -s a b

touch WORKSPACE
cat > BUILD << EOF
load("toolchain.bzl", "gcc_toolchain")

cc_binary(
    name = "test",
    srcs = ["src.cc"],
)

cc_toolchain_suite(
    name = "my_toolchain",
    toolchains = {"k8": "dummy_toolchain"},
)

filegroup(
    name = "compiler_files",
    srcs = ["b"],
)

gcc_toolchain("dummy_toolchain")
EOF

cat > toolchain.bzl << EOF
def _impl(ctx):
    crosstool_proto = cc_common.create_cc_toolchain_config_info(
        ctx = ctx,
        toolchain_identifier = "cc-compiler-" + ctx.label.name,
        host_system_name = "x86_64-linux-gnu",
        target_system_name = "linux",
        target_cpu = "k8",
        target_libc = "unknown-glibc",
        compiler = "gcc",
    )
    return [crosstool_proto]

gcc_toolchain_configuration = rule(
    attrs = {},
    provides = [CcToolchainConfigInfo],
    implementation = _impl,
)

def gcc_toolchain(name):
    gcc_toolchain_configuration(name = name + "_configuration")

    native.cc_toolchain(
        name = name,
        toolchain_config = name + "_configuration",
        all_files = "//:compiler_files",
        compiler_files = "//:compiler_files",
        dwp_files = "//:compiler_files",
        linker_files = "//:compiler_files",
        objcopy_files = "//:compiler_files",
        strip_files = "//:compiler_files",
        supports_param_files = True,
        supports_header_parsing = True,
    )
EOF
```

Trying to build the binary via
```
bazel build //:test --crosstool_top=//:my_toolchain --experimental_use_hermetic_linux_sandbox
```

results in
```
ERROR: .../BUILD:3:10: Compiling src.cc failed: I/O exception during sandboxed execution: input dependency /home/.../de7a35bfd5cefbaf6a88e91201c016c4/execroot/__main__/b could not be checked for modifications during execution.
```
So the command-line output is not that useful, but looking into the server log I see this stack trace which shows a UnsupportedOperationException at FileArtifactValue.java:352 - it's trying to get a ContentsProxy of a directory, which isn't supported. Despite what FileArtifactValue may think, it's not always an actual file. So the modification check should take appropriate action on things that are not files.

```
com.google.devtools.build.lib.skyframe.ActionExecutionFunction$ActionExecutionFunctionException: com.google.devtools.build.lib.actions.AlreadyReportedActionEx
ecutionException: I/O exception during sandboxed execution: input dependency /usr/local/google/home/larsrc/.cache/bazel/_bazel_larsrc/b91bdcc9c8a35d79096b4ba9
b5794356/execroot/__main__/b could not be checked for modifications during execution.
        at com.google.devtools.build.lib.skyframe.ActionExecutionFunction.computeInternal(ActionExecutionFunction.java:368)
        at com.google.devtools.build.lib.skyframe.ActionExecutionFunction.compute(ActionExecutionFunction.java:169)
        at com.google.devtools.build.skyframe.AbstractParallelEvaluator$Evaluate.run(AbstractParallelEvaluator.java:590)
        at com.google.devtools.build.lib.concurrent.AbstractQueueVisitor$WrappedRunnable.run(AbstractQueueVisitor.java:382)
        at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(Unknown Source)
        at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(Unknown Source)
        at java.base/java.lang.Thread.run(Unknown Source)
Caused by: com.google.devtools.build.lib.actions.AlreadyReportedActionExecutionException: I/O exception during sandboxed execution: input dependency /usr/loca
l/google/home/larsrc/.cache/bazel/_bazel_larsrc/b91bdcc9c8a35d79096b4ba9b5794356/execroot/__main__/b could not be checked for modifications during execution.
        ... 7 more
Caused by: com.google.devtools.build.lib.actions.UserExecException: I/O exception during sandboxed execution: input dependency /usr/local/google/home/larsrc/.
cache/bazel/_bazel_larsrc/b91bdcc9c8a35d79096b4ba9b5794356/execroot/__main__/b could not be checked for modifications during execution.
        at com.google.devtools.build.lib.sandbox.AbstractSandboxSpawnRunner.exec(AbstractSandboxSpawnRunner.java:97)
        at com.google.devtools.build.lib.sandbox.SandboxModule$SandboxFallbackSpawnRunner.exec(SandboxModule.java:494)
        at com.google.devtools.build.lib.exec.SpawnRunner.execAsync(SpawnRunner.java:245)
        at com.google.devtools.build.lib.exec.AbstractSpawnStrategy.exec(AbstractSpawnStrategy.java:146)
        at com.google.devtools.build.lib.exec.AbstractSpawnStrategy.exec(AbstractSpawnStrategy.java:108)
        at com.google.devtools.build.lib.actions.SpawnStrategy.beginExecution(SpawnStrategy.java:47)
        at com.google.devtools.build.lib.exec.SpawnStrategyResolver.beginExecution(SpawnStrategyResolver.java:68)
        at com.google.devtools.build.lib.rules.cpp.CppCompileAction.beginExecution(CppCompileAction.java:1454)
        at com.google.devtools.build.lib.actions.Action.execute(Action.java:133)
        at com.google.devtools.build.lib.skyframe.SkyframeActionExecutor$5.execute(SkyframeActionExecutor.java:907)
        at com.google.devtools.build.lib.skyframe.SkyframeActionExecutor$ActionRunner.continueAction(SkyframeActionExecutor.java:1076)
        at com.google.devtools.build.lib.skyframe.SkyframeActionExecutor$ActionR
unner.run(SkyframeActionExecutor.java:1031)
        at com.google.devtools.build.lib.skyframe.ActionExecutionState.runStateMachine(ActionExecutionState.java:152)
        at com.google.devtools.build.lib.skyframe.ActionExecutionState.getResultOrDependOnFuture(ActionExecutionState.java:91)
        at com.google.devtools.build.lib.skyframe.SkyframeActionExecutor.executeAction(SkyframeActionExecutor.java:492)
        at com.google.devtools.build.lib.skyframe.ActionExecutionFunction.checkCacheAndExecuteIfNeeded(ActionExecutionFunction.java:856)
        at com.google.devtools.build.lib.skyframe.ActionExecutionFunction.computeInternal(ActionExecutionFunction.java:349)
        ... 6 more
Caused by: java.io.IOException: input dependency /usr/local/google/home/larsrc/.cache/bazel/_bazel_larsrc/b91bdcc9c8a35d79096b4ba9b5794356/execroot/__main__/b could not be checked for modifications during execution.
        at com.google.devtools.build.lib.sandbox.LinuxSandboxedSpawnRunner.checkForConcurrentModifications(LinuxSandboxedSpawnRunner.java:399)
        at com.google.devtools.build.lib.sandbox.LinuxSandboxedSpawnRunner.verifyPostCondition(LinuxSandboxedSpawnRunner.java:380)
        at com.google.devtools.build.lib.sandbox.AbstractSandboxSpawnRunner.runSpawn(AbstractSandboxSpawnRunner.java:136)
        at com.google.devtools.build.lib.sandbox.AbstractSandboxSpawnRunner.exec(AbstractSandboxSpawnRunner.java:92)
        ... 22 more
Caused by: java.lang.UnsupportedOperationException
        at com.google.devtools.build.lib.actions.FileArtifactValue$DirectoryArtifactValue.getContentsProxy(FileArtifactValue.java:352)
        at com.google.devtools.build.lib.sandbox.LinuxSandboxedSpawnRunner.checkForConcurrentModifications(LinuxSandboxedSpawnRunner.java:395)
        ... 25 more
```
Looking at the possible subclasses of FileArtifactValue, they mostly don't support the operations needed for the modification check. @frazze-jobb we'll have to figure out which kinds of inputs to ignore and which to handle properly. Here are the direct subclasses:

```
google3/third_party/bazel/src/main/java/com/google/devtools/build/lib/actions/ActionCacheChecker.java
851: private static final class ConstantMetadataValue extends FileArtifactValue
google3/third_party/bazel/src/main/java/com/google/devtools/build/lib/actions/FileArtifactValue.java
332: private static final class DirectoryArtifactValue extends FileArtifactValue {
399: private static final class HashedDirectoryArtifactValue extends FileArtifactValue {
462: private static final class RegularFileArtifactValue extends FileArtifactValue {
549: public static class RemoteFileArtifactValue extends FileArtifactValue {
718: public static final class UnresolvedSymlinkArtifactValue extends FileArtifactValue {
758: public static final class InlineFileArtifactValue extends FileArtifactValue {
832: public static final class SourceFileArtifactValue extends FileArtifactValue {
895: private static final class SingletonMarkerValue extends FileArtifactValue implements Singleton {
940: private static final class OmittedFileValue extends FileArtifactValue implements Singleton {
google3/third_party/bazel/src/main/java/com/google/devtools/build/lib/skyframe/TreeArtifactValue.java
210: private static final class TreeArtifactCompositeFileArtifactValue extends FileArtifactValue {
```
Thanks for looking into this. Not that it would help much, but leaving this smaller minimal example here for sake of simplicity
```
#!/bin/bash
touch WORKSPACE
mkdir a
ln -s a b

cat > BUILD << EOF
genrule(
    name = "target",
    srcs = [":b"],
    outs = ["dummy"],
    cmd = "ls $< > \$@",
)
EOF
```

```
bazel build //:target --experimental_use_hermetic_linux_sandbox
```
That's a nice small repro. It can also be done with just a directory as an input, so both directory and symlink handling needs to be fixed.
The purpose of `checkForConcurrentModifications` is to check that an action has not changed the content of hardlinked files outside of the sandbox. `HardlinkedSandboxedSpawn.hardLinkRecursive` is resolving symbolic links and are only creating hardlinks for files, not directories. Therefor it should be safe to skip the concurrent modification check when `metadata.getType().isFile()` is false. That should address the use cases in this ticket reliably.

However, then the following subclasses of FileArtifactValue, mentioned by @larsrc-google, remains:

- ConstantMetadataValue
- InlineFileArtifactValue
- RemoteFileArtifactValue
- SourceFileArtifactValue

I don't see any good way to handle them. Are they rare? As workaround, should we catch UnsupportedOperationException and log a warning each time it occurs? (That could be up to one warning per action.)
Thanks for the fix!

I don't know anything about Bazel release policies - is this fix likely to make it to a 6.x release?
@lunch-glide-pepper You can use the `@bazel-io flag` command in a GitHub comment to mark it for cherry-picking and then create a PR with the commit cherry-picked on `release-6.0.0`.

Pull request review comments:

Patch that resolved the issue:
diff --git a/src/main/java/com/google/devtools/build/lib/sandbox/LinuxSandboxedSpawnRunner.java b/src/main/java/com/google/devtools/build/lib/sandbox/LinuxSandboxedSpawnRunner.java
index 1da4ee5b2dfa6b..89eb4a70f0668f 100644
--- a/src/main/java/com/google/devtools/build/lib/sandbox/LinuxSandboxedSpawnRunner.java
+++ b/src/main/java/com/google/devtools/build/lib/sandbox/LinuxSandboxedSpawnRunner.java
@@ -66,6 +66,8 @@ final class LinuxSandboxedSpawnRunner extends AbstractSandboxSpawnRunner {
   private static final Map<Path, Boolean> isSupportedMap = new HashMap<>();
   private static final AtomicBoolean warnedAboutNonHermeticTmp = new AtomicBoolean();
 
+  private static final AtomicBoolean warnedAboutUnsupportedModificationCheck = new AtomicBoolean();
+
   /**
    * Returns whether the linux sandbox is supported on the local machine by running a small command
    * in it.
@@ -216,9 +218,7 @@ protected SandboxedSpawn prepareSpawn(Spawn spawn, SpawnExecutionContext context
     ImmutableSet<Path> writableDirs = getWritableDirs(sandboxExecRoot, environment);
 
     SandboxInputs inputs =
-        helpers.processInputFiles(
-            context.getInputMapping(PathFragment.EMPTY_FRAGMENT),
-            execRoot);
+        helpers.processInputFiles(context.getInputMapping(PathFragment.EMPTY_FRAGMENT), execRoot);
     SandboxOutputs outputs = helpers.getOutputs(spawn);
 
     Duration timeout = context.getTimeout();
@@ -439,22 +439,39 @@ private void checkForConcurrentModifications(SpawnExecutionContext context)
       throws IOException, ForbiddenActionInputException {
     for (ActionInput input : context.getInputMapping(PathFragment.EMPTY_FRAGMENT).values()) {
       if (input instanceof VirtualActionInput) {
+        // Virtual inputs are not existing in file system and can't be tampered with via sandbox. No
+        // need to check them.
         continue;
       }
 
       FileArtifactValue metadata = context.getMetadataProvider().getMetadata(input);
-      Path path = execRoot.getRelative(input.getExecPath());
+      if (!metadata.getType().isFile()) {
+        // The hermetic sandbox creates hardlinks from files inside sandbox to files outside
+        // sandbox. The content of the files outside the sandbox could have been tampered with via
+        // the hardlinks. Therefore files are checked for modifications. But directories and
+        // unresolved symlinks are not represented as hardlinks in sandbox and don't need to be
+        // checked. By continue and not checking them, we avoid UnsupportedOperationException and
+        // IllegalStateException.
+        continue;
+      }
 
+      Path path = execRoot.getRelative(input.getExecPath());
       try {
         if (wasModifiedSinceDigest(metadata.getContentsProxy(), path)) {
           throw new IOException("input dependency " + path + " was modified during execution.");
         }
       } catch (UnsupportedOperationException e) {
-        throw new IOException(
-            "input dependency "
-                + path
-                + " could not be checked for modifications during execution.",
-            e);
+        // Some FileArtifactValue implementations are ignored safely and silently already by the
+        // isFile check above. The remaining ones should probably be checked, but some are not
+        // supporting necessary operations.
+        if (warnedAboutUnsupportedModificationCheck.compareAndSet(false, true)) {
+          reporter.handle(
+              Event.warn(
+                  String.format(
+                      "Input dependency %s of type %s could not be checked for modifications during"
+                          + " execution. Suppressing similar warnings.",
+                      path, metadata.getClass().getSimpleName())));
+        }
       }
     }
   }


Test patch:
diff --git a/src/test/shell/bazel/bazel_hermetic_sandboxing_test.sh b/src/test/shell/bazel/bazel_hermetic_sandboxing_test.sh
index a67f4be7421ebb..51fdf239f25ce7 100755
--- a/src/test/shell/bazel/bazel_hermetic_sandboxing_test.sh
+++ b/src/test/shell/bazel/bazel_hermetic_sandboxing_test.sh
@@ -95,6 +95,37 @@ import import_module
 EOF
 
   cat << 'EOF' > examples/hermetic/BUILD
+
+load(
+  "test.bzl",
+  "overwrite_via_symlink",
+  "overwrite_file_from_declared_directory",
+  "subdirectories_in_declared_directory",
+  "other_artifacts",
+)
+
+overwrite_via_symlink(
+  name = "overwrite_via_resolved_symlink",
+  resolve_symlink = True
+)
+
+overwrite_via_symlink(
+  name = "overwrite_via_unresolved_symlink",
+  resolve_symlink = False
+)
+
+overwrite_file_from_declared_directory(
+  name = "overwrite_file_from_declared_directory"
+)
+
+subdirectories_in_declared_directory(
+  name = "subdirectories_in_declared_directory"
+)
+
+other_artifacts(
+  name = "other_artifacts"
+)
+
 genrule(
   name = "absolute_path",
   srcs = ["script_absolute_path.sh"], # unknown_file.txt not referenced.
@@ -129,6 +160,141 @@ genrule(
          (echo overwrite text > $(location :input_file)) && \
          (echo success > $@)) || (echo fail > $@)",
 )
+EOF
+
+  cat << 'EOF' > examples/hermetic/test.bzl
+
+def _overwrite_via_symlink_impl(ctx):
+    file = ctx.actions.declare_file(ctx.attr.name + ".file")
+    if ctx.attr.resolve_symlink:
+        symlink = ctx.actions.declare_file(ctx.attr.name + ".symlink")
+    else:
+        symlink = ctx.actions.declare_symlink(ctx.attr.name + ".symlink")
+
+    ctx.actions.write(file, "")
+
+    if ctx.attr.resolve_symlink:
+        ctx.actions.symlink(
+            output = symlink,
+            target_file = file
+        )
+        # Symlink become resolved to RegularFileArtifactValue.
+        needed_inputs = [symlink]
+    else:
+        ctx.actions.symlink(
+            output = symlink,
+            target_path = file.basename
+        )
+        # Symlink become UnresolvedSymlinkArtifactValue and would be
+        # dangling unless also providing the actual file as input to sandbox.
+        needed_inputs = [symlink, file]
+
+    result_file = ctx.actions.declare_file(ctx.attr.name + ".result")
+
+    # Try invalid write to the input file via the symlink
+    ctx.actions.run_shell(
+       command = "chmod u+w $1 && echo hello >> $1 && ls -lR > $2",
+       arguments = [symlink.path, result_file.path],
+       inputs = needed_inputs,
+       outputs = [result_file],
+    )
+
+    return [DefaultInfo(files = depset([result_file]))]
+
+overwrite_via_symlink = rule(
+    attrs = {
+        "resolve_symlink" : attr.bool(),
+    },
+    implementation = _overwrite_via_symlink_impl,
+)
+
+
+def _overwrite_file_from_declared_directory_impl(ctx):
+    dir = ctx.actions.declare_directory(ctx.attr.name + ".dir")
+
+    ctx.actions.run_shell(
+        command = "mkdir -p $1/subdir && touch $1/subdir/file",
+        arguments = [dir.path],
+        outputs = [dir],
+    )
+
+    # Try invalid write to input file, with file as implicit input
+    # from declared directory.
+    result_file = ctx.actions.declare_file(ctx.attr.name + ".result")
+    ctx.actions.run_shell(
+       command = "chmod -R u+w $1 && echo hello >> $1/subdir/file && touch $2",
+       arguments = [dir.path, result_file.path],
+       inputs = [dir],
+       outputs = [result_file],
+    )
+
+    return [DefaultInfo(files = depset([result_file]))]
+
+overwrite_file_from_declared_directory = rule(
+    implementation = _overwrite_file_from_declared_directory_impl,
+)
+
+
+def _subdirectories_in_declared_directory_impl(ctx):
+    dir = ctx.actions.declare_directory(ctx.attr.name + ".dir")
+
+    ctx.actions.run_shell(
+        command = "mkdir -p %s/subdir1/subdir2" % dir.path,
+        outputs = [dir],
+    )
+
+    result_file = ctx.actions.declare_file(ctx.attr.name + ".result")
+    ctx.actions.run_shell(
+        command = "ls -lRH %s > %s" % (dir.path, result_file.path),
+        inputs = [dir],
+        outputs = [result_file],
+    )
+
+    return [DefaultInfo(files = depset([result_file]))]
+
+subdirectories_in_declared_directory = rule(
+    implementation = _subdirectories_in_declared_directory_impl,
+)
+
+
+def _other_artifacts_impl(ctx):
+
+    # Produce artifacts of other types
+
+    regular_file_artifact = ctx.actions.declare_file(ctx.attr.name + ".regular_file_artifact")
+    directory_artifact = ctx.actions.declare_file(ctx.attr.name + ".directory_artifact")
+    tree_artifact = ctx.actions.declare_directory(ctx.attr.name + ".tree_artifact")
+    unresolved_symlink_artifact = ctx.actions.declare_symlink(ctx.attr.name + ".unresolved_symlink_artifact")
+
+    ctx.actions.run_shell(
+        command = "touch %s && mkdir %s" % (regular_file_artifact.path, directory_artifact.path),
+        outputs = [regular_file_artifact, tree_artifact, directory_artifact],
+    )
+
+    ctx.actions.symlink(
+        output = unresolved_symlink_artifact,
+        target_path="dangling"
+    )
+
+    # Test other artifact types as input to hermetic sandbox.
+
+    all_artifacts = [regular_file_artifact,
+                     directory_artifact,
+                     tree_artifact,
+                     unresolved_symlink_artifact]
+    input_paths_string = " ".join([a.path for a in all_artifacts])
+    result_file = ctx.actions.declare_file(ctx.attr.name + ".result")
+    ctx.actions.run_shell(
+        command = "ls -lR %s > %s" % (input_paths_string, result_file.path),
+        inputs = all_artifacts,
+        outputs = [result_file],
+    )
+
+    return [DefaultInfo(files = depset([result_file]))]
+
+other_artifacts = rule(
+    implementation = _other_artifacts_impl,
+)
 EOF
 }
 
@@ -141,8 +307,6 @@ function test_absolute_path() {
 
 # Test that the build can't escape the sandbox by resolving symbolic link.
 function test_symbolic_link() {
-  [ "$PLATFORM" != "darwin" ] || return 0
-
   bazel build examples/hermetic:symbolic_link &> $TEST_log \
     && fail "Fail due to non hermetic sandbox: examples/hermetic:symbolic_link" || true
   expect_log "cat: \/execroot\/main\/examples\/hermetic\/unknown_file.txt: No such file or directory"
@@ -150,8 +314,6 @@ function test_symbolic_link() {
 
 # Test that the sandbox discover if the bazel python rule miss dependencies.
 function test_missing_python_deps() {
-  [ "$PLATFORM" != "darwin" ] || return 0
-
   bazel test examples/hermetic:py_module_test --test_output=all &> $TEST_TMPDIR/log \
     && fail "Fail due to non hermetic sandbox: examples/hermetic:py_module_test" || true
 
@@ -160,7 +322,6 @@ function test_missing_python_deps() {
 
 # Test that the intermediate corrupt input file gets re:evaluated
 function test_writing_input_file() {
-  [ "$PLATFORM" != "darwin" ] || return 0
   # Write an input file, this should cause the hermetic sandbox to fail with an exception
   bazel build examples/hermetic:write_input_test &> $TEST_log  \
     && fail "Fail due to non hermetic sandbox: examples/hermetic:write_input_test" || true
@@ -177,7 +338,48 @@ function test_writing_input_file() {
   expect_log "original text input"
 }
 
+# Test that invalid write of input file is detected, when file is accessed via resolved symlink.
+function test_overwrite_via_resolved_symlink() {
+  bazel build examples/hermetic:overwrite_via_resolved_symlink &> $TEST_log  \
+    && fail "Hermetic sandbox did not detect invalid write to input file"
+  expect_log "input dependency .* was modified during execution."
+}
+
+# Test that invalid write of input file is detected, when file is accessed via unresolved symlink.
+function test_overwrite_via_unresolved_symlink() {
+  bazel build examples/hermetic:overwrite_via_unresolved_symlink &> $TEST_log  \
+    && fail "Hermetic sandbox did not detect invalid write to input file"
+  expect_log "input dependency .* was modified during execution."
+}
+
+# Test that invalid write of input file is detected, when file is found implicit via declared directory.
+function test_overwrite_file_from_declared_directory() {
+  bazel build examples/hermetic:overwrite_file_from_declared_directory &> $TEST_log  \
+    && fail "Hermetic sandbox did not detect invalid write to input file"
+  expect_log "input dependency .* was modified during execution."
+}
+
+# Test that the sandbox can handle deep directory trees from declared directory.
+function test_subdirectories_in_declared_directory() {
+  bazel build examples/hermetic:subdirectories_in_declared_directory &> $TEST_log
+  cat bazel-bin/examples/hermetic/subdirectories_in_declared_directory.result
+  assert_contains "dir/subdir1/subdir2" "bazel-bin/examples/hermetic/subdirectories_in_declared_directory.result"
+}
+
+# Test that the sandbox is not crashing and not producing warnings for various types of artifacts.
+# Regression test for Issue #15340
+function test_other_artifacts() {
+  bazel shutdown # Clear memory about duplicated warnings
+  bazel build examples/hermetic:other_artifacts &> $TEST_log
+  expect_not_log "WARNING"
+  assert_contains "regular_file_artifact" "bazel-bin/examples/hermetic/other_artifacts.result"
+  assert_contains "unresolved_symlink_artifact" "bazel-bin/examples/hermetic/other_artifacts.result"
+  assert_contains "directory_artifact" "bazel-bin/examples/hermetic/other_artifacts.result"
+  assert_contains "tree_artifact" "bazel-bin/examples/hermetic/other_artifacts.result"
+}
+
 # The test shouldn't fail if the environment doesn't support running it.
 check_sandbox_allowed || exit 0
+[ "$PLATFORM" != "darwin" ] || exit 0
 
 run_suite "hermetic_sandbox"