Skip to content

do_masked_or_readonly_path skips masked/readonly paths on EACCES — intentional? (fail-closed consistency with runc) #2139

Description

@logical-misha

Summary

do_masked_or_readonly_path() (src/libcrun/linux.c) skips a configured maskedPaths /
readonlyPaths entry when the O_PATH open of its target fails with EACCES (in addition
to ENOENT). So if a configured mask/readonly target cannot be opened due to a permission
error, crun proceeds without applying that protection, and does not fail the start. runc,
for the same operation, fails closed: it skips only the not-exist case and errors on any other
open failure, including EACCES.

This is a code-consistency / defense-in-depth question, not a reported vulnerability — I
have no exploit and, as noted below, the obvious trigger is not reachable. Filing to ask
whether the EACCES-skip is intentional and, if not, to align the fail-closed discipline with
runc.

Where

src/libcrun/linux.c, do_masked_or_readonly_path():

pathfd = safe_openat (get_private_data (container)->rootfsfd, rootfs, rel_path,
                      O_PATH | O_CLOEXEC, 0, err);
if (UNLIKELY (pathfd < 0))
  {
    if (errno != ENOENT && errno != EACCES)   // <-- EACCES is treated like ENOENT
      return pathfd;
    crun_error_release (err);
    return 0;                                  // <-- skip the path, keep going
  }

Why this is likely not a security issue (context checked)

At the point do_masked_or_readonly_path runs, inside container_init_setup
(src/libcrun/container.c), the order is:

libcrun_set_mounts        -> do_masked_and_readonly_paths   (the skip)
libcrun_finalize_mounts
libcrun_set_selinux_label / libcrun_set_apparmor_profile    (process LSM label set AFTER)
apply_security_settings   -> libcrun_set_caps               (capabilities dropped AFTER)
exec

So crun is still fully privileged during masking (it holds CAP_SYS_ADMIN for the mounts
and CAP_DAC_OVERRIDE / CAP_DAC_READ_SEARCH). A root holder of CAP_DAC_OVERRIDE does not
get EACCES from filesystem DAC on an O_PATH open, so the obvious "crafted rootfs makes the
target unopenable" path is not reachable via DAC. I could not construct a case (LSM denial, or
a rootless unmapped-uid path) where a mask target ends up both unmasked and
container-reachable. Hence this is raised as a consistency question, not an exposure report.

Comparison with runc

runc's maskPaths (libcontainer/rootfs_linux.go) skips only the not-exist case and fails
closed otherwise:

dstFh, err := os.OpenFile(path, unix.O_PATH|unix.O_CLOEXEC, 0)
if err != nil {
    if errors.Is(err, os.ErrNotExist) {
        continue                 // skip only if it doesn't exist
    }
    return fmt.Errorf("can't mask path %q: %w", path, err)   // fail-closed on EACCES etc.
}

Question / suggested change

Is the EACCES skip intentional (e.g. a known class of targets expected to be
permission-denied yet safe)? If so, a short comment documenting it would prevent it reading as
an accidental weakening. If not, treating EACCES as fail-closed like every other non-ENOENT
error would match runc:

if (errno != ENOENT)
  return pathfd;

Version

  • crun commit 2f1be57b318734d3c29b50006f3a830a20969e9c; compared against runc fc89fbd.
  • Please confirm against latest main and check for existing/duplicate reports.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions