juicedata/juicefs · error · AclException

Invalid ACL: mask is required and cannot be deleted.

Error message

Invalid ACL: mask is required and cannot be deleted.

What it means

calculateMasks validates the effective-mask invariant: whenever the ACL contains maskable entries (named users/groups, group mask) a mask entry is required. This is thrown when the caller's operation would delete the mask in a scope that needs one without supplying a replacement — POSIX ACL semantics forbid a maskless ACL with maskable entries.

Source

Thrown at sdk/java/src/main/java/io/juicefs/utils/AclTransformation.java:231

    // union of group class permissions in each scope.
    for (AclEntry entry : aclBuilder) {
      scopeFound.add(entry.getScope());
      if (entry.getType() == GROUP || entry.getName() != null) {
        FsAction scopeUnionPerms = unionPerms.get(entry.getScope());
        if (scopeUnionPerms == null) {
          scopeUnionPerms = FsAction.NONE;
        }
        unionPerms.put(entry.getScope(), scopeUnionPerms.or(entry.getPermission()));
      }
      if (entry.getName() != null) {
        maskNeeded.add(entry.getScope());
      }
    }
    // Add mask entry if needed in each scope.
    for (AclEntryScope scope : scopeFound) {
      if (!providedMask.containsKey(scope) && maskNeeded.contains(scope) && maskDirty.contains(scope)) {
        // Caller explicitly removed mask entry, but it's required.
        throw new AclException("Invalid ACL: mask is required and cannot be deleted.");
      } else if (providedMask.containsKey(scope) && (!scopeDirty.contains(scope) || maskDirty.contains(scope))) {
        // Caller explicitly provided new mask, or we are preserving the existing
        // mask in an unchanged scope.
        aclBuilder.add(providedMask.get(scope));
      } else if (maskNeeded.contains(scope) || providedMask.containsKey(scope)) {
        // Otherwise, if there are maskable entries present, or the ACL
        // previously had a mask, then recalculate a mask automatically.
        aclBuilder.add(new AclEntry.Builder().setScope(scope).setType(MASK).setPermission(unionPerms.get(scope)).build());
      }
    }
  }

  private static void copyDefaultsIfNeeded(List<AclEntry> aclBuilder) {
    Collections.sort(aclBuilder, ACL_ENTRY_COMPARATOR);
    ScopedAclEntries scopedEntries = new ScopedAclEntries(aclBuilder);
    if (!scopedEntries.getDefaultEntries().isEmpty()) {
      List<AclEntry> accessEntries = scopedEntries.getAccessEntries();
      List<AclEntry> defaultEntries = scopedEntries.getDefaultEntries();

View on GitHub (pinned to c9a67b23e8)

Solutions

  1. Keep or re-add the mask entry (e.g. 'mask::rwx' or the desired effective mask) in the spec.
  2. If the mask should be recomputed automatically, don't mention mask in the filter/replace spec at all — let calculateMasks generate it.
  3. Remove the maskable named entries too, if the intent was to drop everything needing a mask.

Example fix

// before
filterAclEntriesByAclSpec(path, "mask::"); // deletes required mask
// after
filterAclEntriesByAclSpec(path, "user:alice:"); // remove a named entry; mask is regenerated
// or explicitly
replaceAclEntries(path, "user::rwx,user:alice:rwx,group::r-x,mask::rwx,other::r--");
Defensive patterns

Strategy: validation

Validate before calling

if has_maskable_entries(entries) and mask_entry_missing(resulting_entries):
    raise ValueError("spec would drop the required mask; add mask:: or remove mask from the spec")

Try / catch

try {
  filterAclEntriesByAclSpec(path, spec);
} catch (AclException e) {
  if (e.getMessage().contains("mask is required")) {
    spec = removeMaskFromSpec(spec); // let the lib recompute the mask
    filterAclEntriesByAclSpec(path, spec);
  } else throw e;
}

Prevention

When it happens

Trigger: filterAclEntriesByAclSpec/replaceAclEntries with a spec that removes the mask entry while named user/group entries remain; merging in a named entry but the caller-supplied spec explicitly dropped 'mask::'.

Common situations: Filtering an ACL with 'mask::' in the spec to 'clean up'; hand-writing a replace spec omitting mask; tools migrating ACLs that dropped mask entries.

Understand the failure class

Background: "Invalid state transition" errors: "status must be X, actually Y", "already rejected/charging/uninstalled", "cannot ... while running" — what they mean when a library rejects your call — this error's family across 31 libraries.

Related errors


AI-assisted analysis of juicedata/juicefs@c9a67b23e8 (2026-09-06). Data as JSON: /api/errors/4e135b66549f8edc. Report an issue: GitHub.