linux-nfs.vger.kernel.org archive mirror
 help / color / mirror / Atom feed
* [PATCH v2 0/9] pynfs tests for setting ACL+MODE
@ 2025-11-25 19:49 Chuck Lever
  2025-11-25 19:49 ` [PATCH v2 1/9] Add helper to report unsupported protocol features Chuck Lever
                   ` (8 more replies)
  0 siblings, 9 replies; 10+ messages in thread
From: Chuck Lever @ 2025-11-25 19:49 UTC (permalink / raw)
  To: linux-nfs; +Cc: Calum Mackay, aurelien.couderc2002, Chuck Lever

From: Chuck Lever <chuck.lever@oracle.com>

There are plenty of corner cases when an NFSv4 client requests
setting an NFSv4 ACL and POSIX mode bits in the a single SETATTR
request. It's even worse for NFS server implementations that have
to translate NFSv4 ACLs to POSIX ACLs.

Note that the in-kernel Linux NFS client itself does not support 
NFSv4 ACLs since Linux is a POSIX ACL ecosystem. I believe it will
never send an OPEN(create) or SETATTR that sets an NFSv4 ACL and
mode bits simultaneously, relying on only user space tooling to set
the ACL. So we must depend on only pynfs for testing this particular
NFSD facility.

pynfs didn't have many tests in this particular category when I set
out to troubleshoot a recently reported ACL+MODE bug in NFSD. So
I've written a handful to exercise this specific case.

Changes since RFC:
* De-duplicate code shared between NFSv4.0 and NFSv4.1
* Remove named principals tests for now
* Replace incorrect usage of binary strings
* Add missing patch descriptions

Chuck Lever (9):
  Add helper to report unsupported protocol features
  Add helper to format attribute bitmaps
  Add a helper to compute POSIX mode bits from NFSv4 ACLs
  Add access_mask_to_str() helper to nfs4.0/nfs4acl.py
  Add make_test_acl() helper to nfs4acl modules
  Add verify_acl() helper to nfs4acl modules
  Add verify_mode_and_acl() helper to nfs4acl modules
  Add tests for SETATTR with MODE and ACL
  Add tests for OPEN(create) with ACLs

 nfs4.0/nfs4acl.py                   | 180 ++++++
 nfs4.0/servertests/st_setattr.py    | 863 +++++++++++++++++++++++++++-
 nfs4.1/nfs4acl.py                   |   1 +
 nfs4.1/nfs4lib.py                   |   8 +
 nfs4.1/server41tests/environment.py |   3 +
 nfs4.1/server41tests/st_open.py     | 723 ++++++++++++++++++++++-
 6 files changed, 1774 insertions(+), 4 deletions(-)
 create mode 120000 nfs4.1/nfs4acl.py

-- 
2.51.1


^ permalink raw reply	[flat|nested] 10+ messages in thread

* [PATCH v2 1/9] Add helper to report unsupported protocol features
  2025-11-25 19:49 [PATCH v2 0/9] pynfs tests for setting ACL+MODE Chuck Lever
@ 2025-11-25 19:49 ` Chuck Lever
  2025-11-25 19:49 ` [PATCH v2 2/9] Add helper to format attribute bitmaps Chuck Lever
                   ` (7 subsequent siblings)
  8 siblings, 0 replies; 10+ messages in thread
From: Chuck Lever @ 2025-11-25 19:49 UTC (permalink / raw)
  To: linux-nfs; +Cc: Calum Mackay, aurelien.couderc2002, Chuck Lever

From: Chuck Lever <chuck.lever@oracle.com>

In some cases, specifications permit a server to not implement
a feature (or part of a feature) being tested. That should not
count as a test failure. The helper added here can be used to
indicate that the test did not pass or fail, but that the server
under test does not support the tested feature.

Signed-off-by: Chuck Lever <chuck.lever@oracle.com>
---
 nfs4.1/server41tests/environment.py | 3 +++
 1 file changed, 3 insertions(+)

diff --git a/nfs4.1/server41tests/environment.py b/nfs4.1/server41tests/environment.py
index 48284e029634..2914cd7ddb1e 100644
--- a/nfs4.1/server41tests/environment.py
+++ b/nfs4.1/server41tests/environment.py
@@ -277,6 +277,9 @@ debug_fail = False
 def fail(msg):
     raise testmod.FailureException(msg)
 
+def unsupported(msg):
+    raise testmod.UnsupportedException(msg)
+
 def check(res, stat=NFS4_OK, msg=None, warnlist=[]):
 
     if type(stat) is str:
-- 
2.51.1


^ permalink raw reply related	[flat|nested] 10+ messages in thread

* [PATCH v2 2/9] Add helper to format attribute bitmaps
  2025-11-25 19:49 [PATCH v2 0/9] pynfs tests for setting ACL+MODE Chuck Lever
  2025-11-25 19:49 ` [PATCH v2 1/9] Add helper to report unsupported protocol features Chuck Lever
@ 2025-11-25 19:49 ` Chuck Lever
  2025-11-25 19:49 ` [PATCH v2 3/9] Add a helper to compute POSIX mode bits from NFSv4 ACLs Chuck Lever
                   ` (6 subsequent siblings)
  8 siblings, 0 replies; 10+ messages in thread
From: Chuck Lever @ 2025-11-25 19:49 UTC (permalink / raw)
  To: linux-nfs; +Cc: Calum Mackay, aurelien.couderc2002, Chuck Lever

From: Chuck Lever <chuck.lever@oracle.com>

I'm about to add several new attribute-related tests. Introduce
attr_bitmap_to_str() in nfs4lib.py to convert attribute bitmaps to
human-readable symbolic strings.

Signed-off-by: Chuck Lever <chuck.lever@oracle.com>
---
 nfs4.1/nfs4lib.py | 8 ++++++++
 1 file changed, 8 insertions(+)

diff --git a/nfs4.1/nfs4lib.py b/nfs4.1/nfs4lib.py
index d3a1550f1ce1..9652cf41bef4 100644
--- a/nfs4.1/nfs4lib.py
+++ b/nfs4.1/nfs4lib.py
@@ -564,6 +564,14 @@ def attr_name(bitnum):
     """Returns string corresponding to attr bitnum"""
     return bitnum2attr.get(bitnum, "unknown_%r" % bitnum)
 
+def attr_bitmap_to_str(bitmap):
+    """Convert an attribute bitmap to a symbolic string representation"""
+    bits = bitmap2list(bitmap)
+    if not bits:
+        return "(none)"
+    names = [bitnum2attr.get(bit, "unknown_%d" % bit).upper() for bit in bits]
+    return ", ".join(names)
+
 class NFS4Error(Exception):
     def __init__(self, status, attrs=0, lock_denied=None, tag=None, check_msg=None):
         self.status = status
-- 
2.51.1


^ permalink raw reply related	[flat|nested] 10+ messages in thread

* [PATCH v2 3/9] Add a helper to compute POSIX mode bits from NFSv4 ACLs
  2025-11-25 19:49 [PATCH v2 0/9] pynfs tests for setting ACL+MODE Chuck Lever
  2025-11-25 19:49 ` [PATCH v2 1/9] Add helper to report unsupported protocol features Chuck Lever
  2025-11-25 19:49 ` [PATCH v2 2/9] Add helper to format attribute bitmaps Chuck Lever
@ 2025-11-25 19:49 ` Chuck Lever
  2025-11-25 19:49 ` [PATCH v2 4/9] Add access_mask_to_str() helper to nfs4.0/nfs4acl.py Chuck Lever
                   ` (5 subsequent siblings)
  8 siblings, 0 replies; 10+ messages in thread
From: Chuck Lever @ 2025-11-25 19:49 UTC (permalink / raw)
  To: linux-nfs; +Cc: Calum Mackay, aurelien.couderc2002, Chuck Lever

From: Chuck Lever <chuck.lever@oracle.com>

NFSv4 servers are permitted to adjust a file's mode bits when
the file's ACL is set. RFC 8881 Section 6.3.2 describes what is
expected.

I'm about to add new tests that set an ACL and then retrieve
the file's mode to verify that the server is following that
section of RFC 8881. Mode bit verification is common, so it
is added as a utility function.

To avoid duplicating this code for both NFSv4.0 and NFSv4.1
ACL-related tests (I'm expecting to see a common NFSv4 ACL
document published soon), a symlink is added to make ACL
utility functions visible to tests for both minor versions.

Signed-off-by: Chuck Lever <chuck.lever@oracle.com>
---
 nfs4.0/nfs4acl.py | 60 +++++++++++++++++++++++++++++++++++++++++++++++
 nfs4.1/nfs4acl.py |  1 +
 2 files changed, 61 insertions(+)
 create mode 120000 nfs4.1/nfs4acl.py

diff --git a/nfs4.0/nfs4acl.py b/nfs4.0/nfs4acl.py
index 69e0d0bcfbe1..62193123ef7c 100644
--- a/nfs4.0/nfs4acl.py
+++ b/nfs4.0/nfs4acl.py
@@ -208,6 +208,66 @@ def chk_groups(acl, flags, not_mask):
             chk_triple(mask, allow, acl[0], flags | GROUP, not_mask)
         del acl[:1]
 
+def acl2mode_rfc8881(acl):
+    """
+    Compute mode from ACL according to RFC 8881 Section 6.3.2.
+
+    For each special identifier (OWNER@, GROUP@, EVERYONE@), evaluate the
+    ACL in order considering only ALLOW and DENY ACEs for EVERYONE@ and
+    the identifier under consideration. Then translate to mode bits:
+    - Read bit: Set if ACE4_READ_DATA is permitted
+    - Write bit: Set if BOTH ACE4_WRITE_DATA AND ACE4_APPEND_DATA are permitted
+    - Execute bit: Set if ACE4_EXECUTE is permitted
+
+    Returns the low-order 9 bits of the mode (user/group/other permissions).
+    """
+    identifiers = [
+        ("OWNER@", MODE4_RUSR, MODE4_WUSR, MODE4_XUSR),
+        ("GROUP@", MODE4_RGRP, MODE4_WGRP, MODE4_XGRP),
+        ("EVERYONE@", MODE4_ROTH, MODE4_WOTH, MODE4_XOTH)
+    ]
+
+    mode = 0
+
+    for who, read_bit, write_bit, exec_bit in identifiers:
+        # Start with no permissions
+        allowed_mask = 0
+        denied_mask = 0
+
+        # Evaluate ACL in order, considering only ALLOW/DENY for this
+        # identifier and EVERYONE@
+        for ace in acl:
+            # Skip non-relevant ACEs
+            if ace.who not in (who, "EVERYONE@"):
+                continue
+            if ace.type not in (ALLOWED, DENIED):
+                continue
+            # Skip inherit-only ACEs (they don't affect current permissions)
+            if ace.flag & ACE4_INHERIT_ONLY_ACE:
+                continue
+
+            if ace.type == ALLOWED:
+                # Add allowed permissions not already denied
+                allowed_mask |= (ace.access_mask & ~denied_mask)
+            elif ace.type == DENIED:
+                # Add denied permissions not already allowed
+                denied_mask |= (ace.access_mask & ~allowed_mask)
+
+        # Translate permitted mask to mode bits per RFC 8881 §6.3.2
+        # Read bit: ACE4_READ_DATA must be set
+        if allowed_mask & ACE4_READ_DATA:
+            mode |= read_bit
+
+        # Write bit: BOTH ACE4_WRITE_DATA and ACE4_APPEND_DATA must be set
+        if (allowed_mask & ACE4_WRITE_DATA) and (allowed_mask & ACE4_APPEND_DATA):
+            mode |= write_bit
+
+        # Execute bit: ACE4_EXECUTE must be set
+        if allowed_mask & ACE4_EXECUTE:
+            mode |= exec_bit
+
+    return mode
+
 def printableacl(acl):
     type_str = ["ACCESS", "DENY"]
     out = ""
diff --git a/nfs4.1/nfs4acl.py b/nfs4.1/nfs4acl.py
new file mode 120000
index 000000000000..f05cb4015424
--- /dev/null
+++ b/nfs4.1/nfs4acl.py
@@ -0,0 +1 @@
+../nfs4.0/nfs4acl.py
\ No newline at end of file
-- 
2.51.1


^ permalink raw reply related	[flat|nested] 10+ messages in thread

* [PATCH v2 4/9] Add access_mask_to_str() helper to nfs4.0/nfs4acl.py
  2025-11-25 19:49 [PATCH v2 0/9] pynfs tests for setting ACL+MODE Chuck Lever
                   ` (2 preceding siblings ...)
  2025-11-25 19:49 ` [PATCH v2 3/9] Add a helper to compute POSIX mode bits from NFSv4 ACLs Chuck Lever
@ 2025-11-25 19:49 ` Chuck Lever
  2025-11-25 19:49 ` [PATCH v2 5/9] Add make_test_acl() helper to nfs4acl modules Chuck Lever
                   ` (4 subsequent siblings)
  8 siblings, 0 replies; 10+ messages in thread
From: Chuck Lever @ 2025-11-25 19:49 UTC (permalink / raw)
  To: linux-nfs; +Cc: Calum Mackay, aurelien.couderc2002, Chuck Lever

From: Chuck Lever <chuck.lever@oracle.com>

I'm about to add several new ACL-related tests. Introduce
access_mask_to_str() to convert ACE access_mask values to human-
readable symbolic strings for the display output in these new
tests.

Signed-off-by: Chuck Lever <chuck.lever@oracle.com>
---
 nfs4.0/nfs4acl.py | 20 ++++++++++++++++++++
 1 file changed, 20 insertions(+)

diff --git a/nfs4.0/nfs4acl.py b/nfs4.0/nfs4acl.py
index 62193123ef7c..0fabd0860cff 100644
--- a/nfs4.0/nfs4acl.py
+++ b/nfs4.0/nfs4acl.py
@@ -268,6 +268,26 @@ def acl2mode_rfc8881(acl):
 
     return mode
 
+def access_mask_to_str(mask):
+    """Convert an ACE access_mask to a symbolic string representation"""
+    perms = [
+        (ACE4_READ_DATA, "READ_DATA"),
+        (ACE4_WRITE_DATA, "WRITE_DATA"),
+        (ACE4_APPEND_DATA, "APPEND_DATA"),
+        (ACE4_READ_NAMED_ATTRS, "READ_NAMED_ATTRS"),
+        (ACE4_WRITE_NAMED_ATTRS, "WRITE_NAMED_ATTRS"),
+        (ACE4_EXECUTE, "EXECUTE"),
+        (ACE4_DELETE_CHILD, "DELETE_CHILD"),
+        (ACE4_READ_ATTRIBUTES, "READ_ATTRIBUTES"),
+        (ACE4_WRITE_ATTRIBUTES, "WRITE_ATTRIBUTES"),
+        (ACE4_DELETE, "DELETE"),
+        (ACE4_READ_ACL, "READ_ACL"),
+        (ACE4_WRITE_ACL, "WRITE_ACL"),
+        (ACE4_WRITE_OWNER, "WRITE_OWNER"),
+        (ACE4_SYNCHRONIZE, "SYNCHRONIZE"),
+    ]
+    return " | ".join(name for bit, name in perms if mask & bit) or "(none)"
+
 def printableacl(acl):
     type_str = ["ACCESS", "DENY"]
     out = ""
-- 
2.51.1


^ permalink raw reply related	[flat|nested] 10+ messages in thread

* [PATCH v2 5/9] Add make_test_acl() helper to nfs4acl modules
  2025-11-25 19:49 [PATCH v2 0/9] pynfs tests for setting ACL+MODE Chuck Lever
                   ` (3 preceding siblings ...)
  2025-11-25 19:49 ` [PATCH v2 4/9] Add access_mask_to_str() helper to nfs4.0/nfs4acl.py Chuck Lever
@ 2025-11-25 19:49 ` Chuck Lever
  2025-11-25 19:49 ` [PATCH v2 6/9] Add verify_acl() " Chuck Lever
                   ` (3 subsequent siblings)
  8 siblings, 0 replies; 10+ messages in thread
From: Chuck Lever @ 2025-11-25 19:49 UTC (permalink / raw)
  To: linux-nfs; +Cc: Calum Mackay, aurelien.couderc2002, Chuck Lever

From: Chuck Lever <chuck.lever@oracle.com>

I'm about to add several new tests that set a test ACL. This is
common code for all these tests, so introduce a utility function
to construct this ACL for each test.

Signed-off-by: Chuck Lever <chuck.lever@oracle.com>
---
 nfs4.0/nfs4acl.py | 22 ++++++++++++++++++++++
 1 file changed, 22 insertions(+)

diff --git a/nfs4.0/nfs4acl.py b/nfs4.0/nfs4acl.py
index 0fabd0860cff..e1dfcf0ae371 100644
--- a/nfs4.0/nfs4acl.py
+++ b/nfs4.0/nfs4acl.py
@@ -78,6 +78,28 @@ def mode2acl(mode, dir=False):
              nfsace4(DENIED, 0, negate(other), "EVERYONE@")
              ]
 
+def make_test_acl():
+    """Create a test ACL that maps cleanly to POSIX ACLs
+
+    Uses OWNER@, GROUP@, and EVERYONE@ to match POSIX user/group/other
+    structure, which helps servers that map NFSv4 ACLs to POSIX ACLs.
+
+    Includes both WRITE_DATA and APPEND_DATA for write permission, since
+    Linux NFS server's conservative NFSv4-to-POSIX mapping requires both
+    to grant POSIX write permission.
+    """
+    return [
+        nfsace4(ACE4_ACCESS_ALLOWED_ACE_TYPE, 0,
+                ACE4_READ_DATA | ACE4_WRITE_DATA | ACE4_APPEND_DATA | ACE4_READ_ACL,
+                "OWNER@"),
+        nfsace4(ACE4_ACCESS_ALLOWED_ACE_TYPE, 0,
+                ACE4_READ_DATA,
+                "GROUP@"),
+        nfsace4(ACE4_ACCESS_ALLOWED_ACE_TYPE, 0,
+                ACE4_READ_DATA,
+                "EVERYONE@")
+    ]
+
 def acl2mode(acl):
     """Translate an acl into a 3-digit octal mode"""
     names = ["OWNER@", "GROUP@", "EVERYONE@"]
-- 
2.51.1


^ permalink raw reply related	[flat|nested] 10+ messages in thread

* [PATCH v2 6/9] Add verify_acl() helper to nfs4acl modules
  2025-11-25 19:49 [PATCH v2 0/9] pynfs tests for setting ACL+MODE Chuck Lever
                   ` (4 preceding siblings ...)
  2025-11-25 19:49 ` [PATCH v2 5/9] Add make_test_acl() helper to nfs4acl modules Chuck Lever
@ 2025-11-25 19:49 ` Chuck Lever
  2025-11-25 19:49 ` [PATCH v2 7/9] Add verify_mode_and_acl() " Chuck Lever
                   ` (2 subsequent siblings)
  8 siblings, 0 replies; 10+ messages in thread
From: Chuck Lever @ 2025-11-25 19:49 UTC (permalink / raw)
  To: linux-nfs; +Cc: Calum Mackay, aurelien.couderc2002, Chuck Lever

From: Chuck Lever <chuck.lever@oracle.com>

I'm about to add several new tests that verify a test ACL. This is
common code for all these tests, so introduce a utility function
to verify the test ACL.

Signed-off-by: Chuck Lever <chuck.lever@oracle.com>
---
 nfs4.0/nfs4acl.py | 39 +++++++++++++++++++++++++++++++++++++++
 1 file changed, 39 insertions(+)

diff --git a/nfs4.0/nfs4acl.py b/nfs4.0/nfs4acl.py
index e1dfcf0ae371..5197b1238ea8 100644
--- a/nfs4.0/nfs4acl.py
+++ b/nfs4.0/nfs4acl.py
@@ -310,6 +310,45 @@ def access_mask_to_str(mask):
     ]
     return " | ".join(name for bit, name in perms if mask & bit) or "(none)"
 
+def verify_acl(returned_acl, expected_acl):
+    """Verify that returned ACL contains expected ACEs
+
+    Server may add additional ACEs, but the requested ones must be present
+    with at least the requested permissions.
+
+    Raises AssertionError if verification fails.
+    """
+    if len(returned_acl) < len(expected_acl):
+        raise AssertionError(
+            "Returned ACL has fewer entries than requested: "
+            "expected at least %d, got %d" % (len(expected_acl), len(returned_acl)))
+
+    # Verify the ACEs we set are present (server may add additional ACEs)
+    for i, expected_ace in enumerate(expected_acl):
+        if i >= len(returned_acl):
+            raise AssertionError("Missing ACE %d in returned ACL" % i)
+        returned_ace = returned_acl[i]
+        if returned_ace.type != expected_ace.type:
+            raise AssertionError(
+                "ACE %d type mismatch: expected %d, got %d" %
+                (i, expected_ace.type, returned_ace.type))
+        if returned_ace.who != expected_ace.who:
+            raise AssertionError(
+                "ACE %d who mismatch: expected %s, got %s" %
+                (i, expected_ace.who, returned_ace.who))
+        # Check that requested permissions are present (server may add more)
+        if (returned_ace.access_mask & expected_ace.access_mask) != expected_ace.access_mask:
+            missing = expected_ace.access_mask & ~returned_ace.access_mask
+            raise AssertionError(
+                "ACE %d access_mask mismatch:\n"
+                "  Expected: %s\n"
+                "  Got:      %s\n"
+                "  Missing:  %s" %
+                (i,
+                 access_mask_to_str(expected_ace.access_mask),
+                 access_mask_to_str(returned_ace.access_mask),
+                 access_mask_to_str(missing)))
+
 def printableacl(acl):
     type_str = ["ACCESS", "DENY"]
     out = ""
-- 
2.51.1


^ permalink raw reply related	[flat|nested] 10+ messages in thread

* [PATCH v2 7/9] Add verify_mode_and_acl() helper to nfs4acl modules
  2025-11-25 19:49 [PATCH v2 0/9] pynfs tests for setting ACL+MODE Chuck Lever
                   ` (5 preceding siblings ...)
  2025-11-25 19:49 ` [PATCH v2 6/9] Add verify_acl() " Chuck Lever
@ 2025-11-25 19:49 ` Chuck Lever
  2025-11-25 19:49 ` [PATCH v2 8/9] Add tests for SETATTR with MODE and ACL Chuck Lever
  2025-11-25 19:49 ` [PATCH v2 9/9] Add tests for OPEN(create) with ACLs Chuck Lever
  8 siblings, 0 replies; 10+ messages in thread
From: Chuck Lever @ 2025-11-25 19:49 UTC (permalink / raw)
  To: linux-nfs; +Cc: Calum Mackay, aurelien.couderc2002, Chuck Lever

From: Chuck Lever <chuck.lever@oracle.com>

I'm about to add new tests that set a test ACL and mode bits
simultaneously. This is common code for the new tests, so introduce
a utility function to verify the ACL and mode bits.

Signed-off-by: Chuck Lever <chuck.lever@oracle.com>
---
 nfs4.0/nfs4acl.py | 39 +++++++++++++++++++++++++++++++++++++++
 1 file changed, 39 insertions(+)

diff --git a/nfs4.0/nfs4acl.py b/nfs4.0/nfs4acl.py
index 5197b1238ea8..f33fdb0752bc 100644
--- a/nfs4.0/nfs4acl.py
+++ b/nfs4.0/nfs4acl.py
@@ -349,6 +349,45 @@ def verify_acl(returned_acl, expected_acl):
                  access_mask_to_str(returned_ace.access_mask),
                  access_mask_to_str(missing)))
 
+def verify_mode_and_acl(attrs_dict, expected_acl, operation="operation"):
+    """Verify that MODE and ACL attributes match expectations
+
+    This helper encapsulates the common pattern of verifying both ACL
+    and mode derivation per RFC 8881 Section 6.3.2.
+
+    Args:
+        attrs_dict: Dictionary of attributes (must contain FATTR4_ACL and FATTR4_MODE)
+        expected_acl: The ACL that should be present
+        operation: Name of operation for error messages (default: "operation")
+
+    Returns:
+        tuple: (returned_mode, expected_mode) - both as integers with low 9 bits
+
+    Raises:
+        AssertionError: If verification fails
+    """
+    # Check that both attributes are present
+    if FATTR4_ACL not in attrs_dict:
+        raise AssertionError(
+            "ACL attribute not returned after %s" % operation)
+    if FATTR4_MODE not in attrs_dict:
+        raise AssertionError(
+            "MODE attribute not returned after %s" % operation)
+
+    # Verify ACL matches expected
+    verify_acl(attrs_dict[FATTR4_ACL], expected_acl)
+
+    # Verify mode matches RFC 8881 derivation from ACL
+    returned_mode = attrs_dict[FATTR4_MODE] & 0o777
+    expected_mode = acl2mode_rfc8881(attrs_dict[FATTR4_ACL])
+
+    if returned_mode != expected_mode:
+        raise AssertionError(
+            "MODE (0%o) does not match RFC 8881 §6.3.2 derivation "
+            "from ACL (expected 0%o)" % (returned_mode, expected_mode))
+
+    return returned_mode, expected_mode
+
 def printableacl(acl):
     type_str = ["ACCESS", "DENY"]
     out = ""
-- 
2.51.1


^ permalink raw reply related	[flat|nested] 10+ messages in thread

* [PATCH v2 8/9] Add tests for SETATTR with MODE and ACL
  2025-11-25 19:49 [PATCH v2 0/9] pynfs tests for setting ACL+MODE Chuck Lever
                   ` (6 preceding siblings ...)
  2025-11-25 19:49 ` [PATCH v2 7/9] Add verify_mode_and_acl() " Chuck Lever
@ 2025-11-25 19:49 ` Chuck Lever
  2025-11-25 19:49 ` [PATCH v2 9/9] Add tests for OPEN(create) with ACLs Chuck Lever
  8 siblings, 0 replies; 10+ messages in thread
From: Chuck Lever @ 2025-11-25 19:49 UTC (permalink / raw)
  To: linux-nfs; +Cc: Calum Mackay, aurelien.couderc2002, Chuck Lever

From: Chuck Lever <chuck.lever@oracle.com>

Add comprehensive tests that verify SETATTR behavior when both
mode bits and an ACL are set simultaneously. The new tests cover:

1. SATT19: testSetattrModeWithACL (lines 837-870)
 - Basic test that sets both MODE (0o640) and ACL in a single
   SETATTR
 - Verifies both attributes are correctly set and retrieved
2. SATT20: testSetattrModePreservedWithACL (lines 872-905)
 - Tests MODE preservation with a specific mode (0o600) set
   alongside ACL
 - Ensures MODE is preserved exactly as requested
3. SATT21: testSetattrRestrictiveModeWithACL (lines 907-941)
 - Tests the edge case of restrictive MODE (0o400 read-only) with
   ACL
 - Verifies server handles the case where MODE is more restrictive
   than ACL
4. SATT22: testSetattrACLThenMode (lines 943-987)
 - Compares setting MODE+ACL together vs separately in sequence
 - Creates two files and verifies consistent behavior

These tests ensure that servers correctly handle the interaction
between MODE and ACL attributes when setting file attributes, and
verify that the ACL specified in the OPEN operation matches the ACL
after the OPEN completes.

SETATTR: ACL-to-mode verification tests

These new tests verify:

- Mode is correctly derived from ACL per RFC 8881 §6.3.2
- Write bit requires both ACE4_WRITE_DATA and ACE4_APPEND_DATA
- ACL evaluation order (first ACE takes precedence)
- EVERYONE@ ACEs affect all three permission classes
- Complex ACLs with multiple identifiers

SETATTR: Two additional ACL verification tests

New test SATT28: Verify that the final ACL is the same no matter what
the file's previous mode bits were.

New test SATT29: Verify that SETATTR(ACL) preserves high-order mode
bits (SUID/SGID/SVTX).

SETATTR: Verify behavior when setting both MODE and ACL

Add three comprehensive tests that verify SETATTR(MODE+ACL) behavior
per RFC 8881 §6.4.1.3:

SATT30:
Verify attrsset bitmap and final mode consistency when MODE+ACL set
together. Whether or not the server includes MODE in attrsset, the
final mode must match what the ACL derives to per §6.4.1.3.

SATT31:
Test MODE+ACL interaction when they conflict significantly.
Regardless of requested MODE, final mode always matches ACL
derivation. This shows that ACL wins when both are set (per
§6.4.1.3: "possibly changing the final mode").

Signed-off-by: Chuck Lever <chuck.lever@oracle.com>
---
 nfs4.0/servertests/st_setattr.py | 863 ++++++++++++++++++++++++++++++-
 1 file changed, 862 insertions(+), 1 deletion(-)

diff --git a/nfs4.0/servertests/st_setattr.py b/nfs4.0/servertests/st_setattr.py
index 5d51054c29b4..513fe8ce8223 100644
--- a/nfs4.0/servertests/st_setattr.py
+++ b/nfs4.0/servertests/st_setattr.py
@@ -1,9 +1,10 @@
 from xdrdef.nfs4_const import *
 from .environment import check, get_invalid_utf8strings
 from nfs4lib import bitmap2list, dict2fattr
-from xdrdef.nfs4_type import nfstime4, settime4
+from xdrdef.nfs4_type import nfstime4, settime4, nfsace4
 import nfs_ops
 op = nfs_ops.NFS4ops()
+import nfs4acl
 
 def _set_mode(t, c, file, stateid=None, msg=" using stateid=0",
               warnlist=[]):
@@ -783,3 +784,863 @@ def testMixed(t, env):
     c.init_connection()
     fh, stateid = c.create_confirm(t.word(), deny=OPEN4_SHARE_DENY_NONE)
     _set_mixed(t, c, fh)
+
+def testSetattrModeWithACL(t, env):
+    """SETATTR with both MODE and ACL attributes
+
+    Per RFC 8881 Section 6.4.1.3, when both MODE and ACL are set together,
+    both are processed, but the final mode is derived from the ACL and may
+    differ from the requested MODE.
+
+    FLAGS: setattr acl all
+    DEPEND: MODE ACL0
+    CODE: SATT19
+    """
+    from nfs4acl import acl2mode_rfc8881
+    c = env.c1
+    c.init_connection()
+    fh, stateid = c.create_confirm(t.word())
+
+    acl = nfs4acl.make_test_acl()
+    mode = 0o640
+
+    # Set both MODE and ACL in a single SETATTR
+    attrs = {FATTR4_MODE: mode, FATTR4_ACL: acl}
+    ops = c.use_obj(fh) + [c.setattr(attrs)]
+    res = c.compound(ops)
+    check(res, msg="SETATTR with both MODE and ACL")
+
+    # Check which attributes were actually set by examining the reply bitmask
+    attrsset = bitmap2list(res.resarray[-1].attrsset)
+
+    # Verify both MODE and ACL were set (processed)
+    if FATTR4_MODE not in attrsset:
+        t.fail("MODE not in attrsset, but MODE was requested")
+    if FATTR4_ACL not in attrsset:
+        t.fail("ACL not in attrsset, but ACL was requested")
+
+    # Verify ACL was set correctly
+    attrs_dict = c.do_getattrdict(fh, [FATTR4_ACL, FATTR4_MODE])
+    try:
+        returned_mode, expected_mode = nfs4acl.verify_mode_and_acl(
+            attrs_dict, acl, "SETATTR")
+    except AssertionError as e:
+        t.fail(str(e))
+
+    # Display informational message about mode derivation
+    if returned_mode != mode:
+        t.pass_warn("MODE+ACL: requested 0%o, final 0%o (derived from ACL per RFC 8881 §6.3.2)"
+                    % (mode, returned_mode))
+
+def testSetattrModePreservedWithACL(t, env):
+    """Verify MODE derivation when SETATTR sets both MODE and ACL
+
+    Per RFC 8881 Section 6.4.1.3, when both MODE and ACL are set together,
+    both are processed, but the final mode is derived from the ACL and may
+    differ from the requested MODE.
+
+    FLAGS: setattr acl all
+    DEPEND: MODE ACL0
+    CODE: SATT20
+    """
+    from nfs4acl import acl2mode_rfc8881
+    c = env.c1
+    c.init_connection()
+    fh, stateid = c.create_confirm(t.word())
+
+    acl = nfs4acl.make_test_acl()
+    mode = 0o600
+
+    # Set both MODE and ACL
+    attrs = {FATTR4_MODE: mode, FATTR4_ACL: acl}
+    ops = c.use_obj(fh) + [c.setattr(attrs)]
+    res = c.compound(ops)
+    check(res, msg="SETATTR with MODE and ACL")
+
+    # Check which attributes were actually set by examining the reply bitmask
+    attrsset = bitmap2list(res.resarray[-1].attrsset)
+
+    # Verify both MODE and ACL were set (processed)
+    if FATTR4_MODE not in attrsset:
+        t.fail("MODE not in attrsset, but MODE was requested")
+    if FATTR4_ACL not in attrsset:
+        t.fail("ACL not in attrsset, but ACL was requested")
+
+    # Verify ACL was set correctly
+    attrs_dict = c.do_getattrdict(fh, [FATTR4_ACL, FATTR4_MODE])
+    try:
+        returned_mode, expected_mode = nfs4acl.verify_mode_and_acl(
+            attrs_dict, acl, "SETATTR")
+    except AssertionError as e:
+        t.fail(str(e))
+
+    # Display informational message about mode derivation
+    if returned_mode != mode:
+        t.pass_warn("MODE+ACL: requested 0%o, final 0%o (derived from ACL per RFC 8881 §6.3.2)"
+                    % (mode, returned_mode))
+
+def testSetattrRestrictiveModeWithACL(t, env):
+    """SETATTR with restrictive MODE (0o400) and ACL together
+
+    Per RFC 8881 Section 6.4.1.3, when both MODE and ACL are set together,
+    both are processed, but the final mode is derived from the ACL and may
+    differ from the requested MODE. This tests the case where the requested
+    MODE is more restrictive than what the ACL would grant.
+
+    FLAGS: setattr acl all
+    DEPEND: MODE ACL0
+    CODE: SATT21
+    """
+    from nfs4acl import acl2mode_rfc8881
+    c = env.c1
+    c.init_connection()
+    fh, stateid = c.create_confirm(t.word())
+
+    acl = nfs4acl.make_test_acl()
+    mode = 0o400  # Read-only for owner
+
+    # Set restrictive MODE with ACL
+    attrs = {FATTR4_MODE: mode, FATTR4_ACL: acl}
+    ops = c.use_obj(fh) + [c.setattr(attrs)]
+    res = c.compound(ops)
+    check(res, msg="SETATTR with restrictive MODE and ACL")
+
+    # Check which attributes were actually set by examining the reply bitmask
+    attrsset = bitmap2list(res.resarray[-1].attrsset)
+
+    # Verify both MODE and ACL were set (processed)
+    if FATTR4_MODE not in attrsset:
+        t.fail("MODE not in attrsset, but MODE was requested")
+    if FATTR4_ACL not in attrsset:
+        t.fail("ACL not in attrsset, but ACL was requested")
+
+    # Verify ACL was set correctly
+    attrs_dict = c.do_getattrdict(fh, [FATTR4_ACL, FATTR4_MODE])
+    if FATTR4_ACL not in attrs_dict or FATTR4_MODE not in attrs_dict:
+        t.fail("ACL or MODE not returned after SETATTR")
+
+    try:
+        nfs4acl.verify_acl(attrs_dict[FATTR4_ACL], acl)
+    except AssertionError as e:
+        t.fail(str(e))
+
+    # Per RFC 8881 §6.4.1.3, when both MODE and ACL are set, the final mode
+    # is derived from the ACL per §6.3.2, and may differ from requested MODE
+    returned_mode = attrs_dict[FATTR4_MODE] & 0o777
+    expected_mode = acl2mode_rfc8881(attrs_dict[FATTR4_ACL])
+
+    # Display informational message about mode derivation
+    if returned_mode != mode:
+        t.pass_warn("MODE+ACL: requested 0%o, final 0%o (derived from ACL per RFC 8881 §6.3.2)"
+                    % (mode, returned_mode))
+
+    if returned_mode != expected_mode:
+        t.fail("MODE (0%o) does not match RFC 8881 §6.3.2 derivation "
+               "from ACL (expected 0%o)" % (returned_mode, expected_mode))
+
+def testSetattrACLThenMode(t, env):
+    """SETATTR MODE+ACL together vs ACL then MODE separately
+
+    Per RFC 8881 sections 6.4.1.1-6.4.1.3, when MODE and ACL are set
+    together in a single SETATTR, the server computes the final mode
+    from the ACL (the MODE attribute is effectively ignored). When
+    ACL is set first and MODE is set in a separate operation, the
+    final mode is the explicitly-set MODE value.
+
+    This test verifies this difference by using a MODE (0o755) that
+    differs from the ACL-derived mode (0o644).
+
+    FLAGS: setattr acl all
+    DEPEND: MODE ACL0
+    CODE: SATT22
+    """
+    from nfs4acl import acl2mode_rfc8881
+    c = env.c1
+    c.init_connection()
+
+    # Create two test files
+    fh1, stateid1 = c.create_confirm(t.word() + b"_1")
+    fh2, stateid2 = c.create_confirm(t.word() + b"_2")
+
+    # make_test_acl() derives to mode 0o644 (rw-r--r--)
+    acl = nfs4acl.make_test_acl()
+    acl_derived_mode = acl2mode_rfc8881(acl)
+
+    # Use a different mode to demonstrate the RFC-defined difference
+    explicit_mode = 0o755
+
+    # File 1: Set MODE and ACL together
+    # Per RFC 8881 §6.4.1.2, mode is derived from ACL (MODE ignored)
+    attrs = {FATTR4_MODE: explicit_mode, FATTR4_ACL: acl}
+    ops = c.use_obj(fh1) + [c.setattr(attrs)]
+    res = c.compound(ops)
+    check(res, msg="SETATTR with MODE and ACL together")
+
+    # File 2: Set ACL first, then MODE separately
+    # Per RFC 8881 §6.4.1.1, final mode is the explicit MODE value
+    ops = c.use_obj(fh2) + [c.setattr({FATTR4_ACL: acl})]
+    res = c.compound(ops)
+    check(res, msg="SETATTR with ACL")
+
+    ops = c.use_obj(fh2) + [c.setattr({FATTR4_MODE: explicit_mode})]
+    res = c.compound(ops)
+    check(res, msg="SETATTR with MODE after ACL")
+
+    # Verify the modes differ as expected per RFC 8881
+    attrs1 = c.do_getattrdict(fh1, [FATTR4_MODE, FATTR4_ACL])
+    attrs2 = c.do_getattrdict(fh2, [FATTR4_MODE, FATTR4_ACL])
+
+    mode1 = attrs1[FATTR4_MODE] & 0o7777
+    mode2 = attrs2[FATTR4_MODE] & 0o7777
+
+    # File 1 (MODE+ACL together): mode should be derived from ACL
+    if mode1 != acl_derived_mode:
+        t.fail("MODE+ACL together: expected ACL-derived mode 0%o, got 0%o"
+               % (acl_derived_mode, mode1))
+
+    # File 2 (ACL then MODE): mode should be explicit MODE value
+    if mode2 != explicit_mode:
+        t.fail("ACL then MODE: expected explicit mode 0%o, got 0%o"
+               % (explicit_mode, mode2))
+
+def testSetattrACLModeDeriveBasic(t, env):
+    """SETATTR(ACL) should derive mode per RFC 8881 Section 6.3.2
+
+    Test basic mode derivation from ACL when setting ACL alone.
+    The mode's permission bits should match what is computed from the ACL.
+
+    FLAGS: setattr acl all
+    DEPEND: MODE ACL0
+    CODE: SATT23
+    """
+    from nfs4acl import acl2mode_rfc8881
+    c = env.c1
+    c.init_connection()
+
+    fh, stateid = c.create_confirm(t.word())
+
+    # Create ACL: OWNER@ gets read+write+execute, others get nothing
+    acl = [
+        nfsace4(ACE4_ACCESS_ALLOWED_ACE_TYPE, 0,
+                ACE4_READ_DATA | ACE4_WRITE_DATA | ACE4_APPEND_DATA | ACE4_EXECUTE,
+                b"OWNER@"),
+        nfsace4(ACE4_ACCESS_DENIED_ACE_TYPE, 0,
+                ACE4_READ_DATA | ACE4_WRITE_DATA | ACE4_APPEND_DATA | ACE4_EXECUTE,
+                b"GROUP@"),
+        nfsace4(ACE4_ACCESS_DENIED_ACE_TYPE, 0,
+                ACE4_READ_DATA | ACE4_WRITE_DATA | ACE4_APPEND_DATA | ACE4_EXECUTE,
+                b"EVERYONE@")
+    ]
+
+    # Set ACL only (not MODE)
+    ops = c.use_obj(fh) + [c.setattr({FATTR4_ACL: acl})]
+    res = c.compound(ops)
+    check(res, msg="SETATTR with ACL only")
+
+    # Get resulting mode
+    attrs = c.do_getattrdict(fh, [FATTR4_MODE, FATTR4_ACL])
+    returned_mode = attrs[FATTR4_MODE] & 0o777
+    returned_acl = attrs[FATTR4_ACL]
+
+    # Compute expected mode from returned ACL per RFC 8881 §6.3.2
+    expected_mode = acl2mode_rfc8881(returned_acl)
+
+    if returned_mode != expected_mode:
+        t.fail("Mode (0%o) does not match RFC 8881 §6.3.2 derivation "
+               "from ACL (expected 0%o)" % (returned_mode, expected_mode))
+
+def testSetattrACLModeDeriveWriteBits(t, env):
+    """SETATTR(ACL) write bit requires BOTH WRITE_DATA and APPEND_DATA
+
+    Per RFC 8881 §6.3.2, the write mode bit should only be set if BOTH
+    ACE4_WRITE_DATA and ACE4_APPEND_DATA are present in the ACL.
+
+    FLAGS: setattr acl all
+    DEPEND: MODE ACL0
+    CODE: SATT24
+    """
+    from nfs4acl import acl2mode_rfc8881
+    c = env.c1
+    c.init_connection()
+
+    # Test 1: Only WRITE_DATA (no APPEND_DATA) - write bit should NOT be set
+    fh1, stateid1 = c.create_confirm(t.word() + b"_1")
+    acl1 = [
+        nfsace4(ACE4_ACCESS_ALLOWED_ACE_TYPE, 0,
+                ACE4_WRITE_DATA,  # Missing APPEND_DATA
+                b"OWNER@")
+    ]
+    ops = c.use_obj(fh1) + [c.setattr({FATTR4_ACL: acl1})]
+    res = c.compound(ops)
+    check(res, msg="SETATTR ACL with only WRITE_DATA")
+
+    attrs1 = c.do_getattrdict(fh1, [FATTR4_MODE, FATTR4_ACL])
+    mode1 = attrs1[FATTR4_MODE] & 0o777
+    expected_mode1 = acl2mode_rfc8881(attrs1[FATTR4_ACL])
+
+    if mode1 != expected_mode1:
+        t.fail("Mode (0%o) with only WRITE_DATA does not match expected (0%o). "
+               "Write bit should NOT be set without APPEND_DATA." %
+               (mode1, expected_mode1))
+
+    # Test 2: Only APPEND_DATA (no WRITE_DATA) - write bit should NOT be set
+    fh2, stateid2 = c.create_confirm(t.word() + b"_2")
+    acl2 = [
+        nfsace4(ACE4_ACCESS_ALLOWED_ACE_TYPE, 0,
+                ACE4_APPEND_DATA,  # Missing WRITE_DATA
+                b"OWNER@")
+    ]
+    ops = c.use_obj(fh2) + [c.setattr({FATTR4_ACL: acl2})]
+    res = c.compound(ops)
+    check(res, msg="SETATTR ACL with only APPEND_DATA")
+
+    attrs2 = c.do_getattrdict(fh2, [FATTR4_MODE, FATTR4_ACL])
+    mode2 = attrs2[FATTR4_MODE] & 0o777
+    expected_mode2 = acl2mode_rfc8881(attrs2[FATTR4_ACL])
+
+    if mode2 != expected_mode2:
+        t.fail("Mode (0%o) with only APPEND_DATA does not match expected (0%o). "
+               "Write bit should NOT be set without WRITE_DATA." %
+               (mode2, expected_mode2))
+
+    # Test 3: Both WRITE_DATA and APPEND_DATA - write bit SHOULD be set
+    fh3, stateid3 = c.create_confirm(t.word() + b"_3")
+    acl3 = [
+        nfsace4(ACE4_ACCESS_ALLOWED_ACE_TYPE, 0,
+                ACE4_WRITE_DATA | ACE4_APPEND_DATA,  # Both present
+                b"OWNER@")
+    ]
+    ops = c.use_obj(fh3) + [c.setattr({FATTR4_ACL: acl3})]
+    res = c.compound(ops)
+    check(res, msg="SETATTR ACL with both WRITE_DATA and APPEND_DATA")
+
+    attrs3 = c.do_getattrdict(fh3, [FATTR4_MODE, FATTR4_ACL])
+    mode3 = attrs3[FATTR4_MODE] & 0o777
+    expected_mode3 = acl2mode_rfc8881(attrs3[FATTR4_ACL])
+
+    if mode3 != expected_mode3:
+        t.fail("Mode (0%o) with WRITE_DATA+APPEND_DATA does not match "
+               "expected (0%o)" % (mode3, expected_mode3))
+
+def testSetattrACLModeDeriveAllowDeny(t, env):
+    """SETATTR(ACL) with ALLOW/DENY interaction
+
+    Test that ACL evaluation order is correct when mixing ALLOW and DENY ACEs.
+    Per RFC 8881 §6.3.2, evaluate ACEs in order, with earlier ACEs taking
+    precedence.
+
+    FLAGS: setattr acl all
+    DEPEND: MODE ACL0
+    CODE: SATT25
+    """
+    from nfs4acl import acl2mode_rfc8881
+    c = env.c1
+    c.init_connection()
+
+    # Test: ALLOW first, then DENY - the ALLOW should win
+    fh, stateid = c.create_confirm(t.word())
+    acl = [
+        nfsace4(ACE4_ACCESS_ALLOWED_ACE_TYPE, 0,
+                ACE4_READ_DATA,
+                b"OWNER@"),
+        nfsace4(ACE4_ACCESS_DENIED_ACE_TYPE, 0,
+                ACE4_READ_DATA,  # This should be ignored (already allowed)
+                b"OWNER@")
+    ]
+
+    ops = c.use_obj(fh) + [c.setattr({FATTR4_ACL: acl})]
+    res = c.compound(ops)
+    check(res, msg="SETATTR ACL with ALLOW then DENY")
+
+    attrs = c.do_getattrdict(fh, [FATTR4_MODE, FATTR4_ACL])
+    returned_mode = attrs[FATTR4_MODE] & 0o777
+    expected_mode = acl2mode_rfc8881(attrs[FATTR4_ACL])
+
+    if returned_mode != expected_mode:
+        t.fail("Mode (0%o) with ALLOW/DENY does not match expected (0%o). "
+               "First ACE should take precedence." % (returned_mode, expected_mode))
+
+def testSetattrACLModeDeriveEveryone(t, env):
+    """SETATTR(ACL) with EVERYONE@ affecting all identifiers
+
+    Test that EVERYONE@ ACEs are considered when evaluating permissions
+    for OWNER@ and GROUP@ per RFC 8881 §6.3.2.
+
+    FLAGS: setattr acl all
+    DEPEND: MODE ACL0
+    CODE: SATT26
+    """
+    from nfs4acl import acl2mode_rfc8881
+    c = env.c1
+    c.init_connection()
+
+    # EVERYONE@ gets read, specific OWNER@ gets nothing extra
+    # Final result: OWNER@ should have read (from EVERYONE@)
+    fh, stateid = c.create_confirm(t.word())
+    acl = [
+        nfsace4(ACE4_ACCESS_ALLOWED_ACE_TYPE, 0,
+                ACE4_READ_DATA,
+                b"EVERYONE@")
+    ]
+
+    ops = c.use_obj(fh) + [c.setattr({FATTR4_ACL: acl})]
+    res = c.compound(ops)
+    check(res, msg="SETATTR ACL with only EVERYONE@")
+
+    attrs = c.do_getattrdict(fh, [FATTR4_MODE, FATTR4_ACL])
+    returned_mode = attrs[FATTR4_MODE] & 0o777
+    expected_mode = acl2mode_rfc8881(attrs[FATTR4_ACL])
+
+    if returned_mode != expected_mode:
+        t.fail("Mode (0%o) does not match expected (0%o). "
+               "EVERYONE@ should affect all mode bits." %
+               (returned_mode, expected_mode))
+
+def testSetattrACLModeDeriveComplex(t, env):
+    """SETATTR(ACL) with complex ACL including multiple identifiers
+
+    Test mode derivation with a more realistic ACL including OWNER@,
+    GROUP@, and EVERYONE@ with various permissions.
+
+    FLAGS: setattr acl all
+    DEPEND: MODE ACL0
+    CODE: SATT27
+    """
+    from nfs4acl import acl2mode_rfc8881
+    c = env.c1
+    c.init_connection()
+
+    fh, stateid = c.create_confirm(t.word())
+
+    # Create a complex ACL:
+    # OWNER@: read + write + execute
+    # GROUP@: read + execute (no write)
+    # EVERYONE@: read only
+    acl = [
+        nfsace4(ACE4_ACCESS_ALLOWED_ACE_TYPE, 0,
+                ACE4_READ_DATA | ACE4_WRITE_DATA | ACE4_APPEND_DATA | ACE4_EXECUTE,
+                b"OWNER@"),
+        nfsace4(ACE4_ACCESS_ALLOWED_ACE_TYPE, 0,
+                ACE4_READ_DATA | ACE4_EXECUTE,
+                b"GROUP@"),
+        nfsace4(ACE4_ACCESS_ALLOWED_ACE_TYPE, 0,
+                ACE4_READ_DATA,
+                b"EVERYONE@")
+    ]
+
+    ops = c.use_obj(fh) + [c.setattr({FATTR4_ACL: acl})]
+    res = c.compound(ops)
+    check(res, msg="SETATTR with complex ACL")
+
+    attrs = c.do_getattrdict(fh, [FATTR4_MODE, FATTR4_ACL])
+    returned_mode = attrs[FATTR4_MODE] & 0o777
+    expected_mode = acl2mode_rfc8881(attrs[FATTR4_ACL])
+
+    if returned_mode != expected_mode:
+        t.fail("Mode (0%o) does not match RFC 8881 §6.3.2 derivation "
+               "from complex ACL (expected 0%o)" %
+               (returned_mode, expected_mode))
+
+def testSetattrACLIndependentOfMode(t, env):
+    """SETATTR(ACL) outcome should not depend on existing mode bits
+
+    Per RFC 8881 §6.4.1.2, when setting ACL without mode, the ACL should
+    be set as given. The existing mode should not affect the ACL that gets
+    stored. This test verifies that setting the same ACL on files with
+    different initial modes results in identical ACLs being stored.
+
+    FLAGS: setattr acl all
+    DEPEND: MODE ACL0
+    CODE: SATT28
+    """
+    from nfs4acl import acl2mode_rfc8881
+    c = env.c1
+    c.init_connection()
+
+    # Create the same ACL to use for all files
+    # OWNER@: read + write, GROUP@: read, EVERYONE@: nothing
+    test_acl = [
+        nfsace4(ACE4_ACCESS_ALLOWED_ACE_TYPE, 0,
+                ACE4_READ_DATA | ACE4_WRITE_DATA | ACE4_APPEND_DATA,
+                b"OWNER@"),
+        nfsace4(ACE4_ACCESS_ALLOWED_ACE_TYPE, 0,
+                ACE4_READ_DATA,
+                b"GROUP@")
+    ]
+
+    # Test with various initial mode values
+    initial_modes = [0o600, 0o644, 0o755, 0o777, 0o400, 0o000]
+    files = []
+
+    # Create files with different initial modes
+    for initial_mode in initial_modes:
+        fh, stateid = c.create_confirm(t.word() + (b"_%o" % initial_mode))
+
+        # Set initial mode
+        ops = c.use_obj(fh) + [c.setattr({FATTR4_MODE: initial_mode})]
+        res = c.compound(ops)
+        check(res, msg="Setting initial mode to 0%o" % initial_mode)
+
+        files.append((fh, initial_mode))
+
+    # Now set the same ACL on all files
+    for fh, initial_mode in files:
+        ops = c.use_obj(fh) + [c.setattr({FATTR4_ACL: test_acl})]
+        res = c.compound(ops)
+        check(res, msg="SETATTR ACL on file with initial mode 0%o" % initial_mode)
+
+    # Retrieve the ACLs that were actually stored
+    acls = []
+    for fh, initial_mode in files:
+        attrs = c.do_getattrdict(fh, [FATTR4_ACL])
+        acls.append((initial_mode, attrs[FATTR4_ACL]))
+
+    # Helper function to compare ACLs (comparing ACE lists)
+    def acl_equal(acl1, acl2):
+        """Compare two ACLs for equality"""
+        if len(acl1) != len(acl2):
+            return False
+        for ace1, ace2 in zip(acl1, acl2):
+            if (ace1.type != ace2.type or
+                ace1.flag != ace2.flag or
+                ace1.access_mask != ace2.access_mask or
+                ace1.who != ace2.who):
+                return False
+        return True
+
+    def acl_to_string(acl):
+        """Convert ACL to readable string for error messages"""
+        aces = []
+        for ace in acl:
+            type_str = "ALLOW" if ace.type == ACE4_ACCESS_ALLOWED_ACE_TYPE else "DENY"
+            aces.append("<%s:%s:0x%x:0x%x>" %
+                       (type_str, ace.who.decode(), ace.access_mask, ace.flag))
+        return "[" + ", ".join(aces) + "]"
+
+    # Compare all ACLs - they should all be identical
+    reference_mode, reference_acl = acls[0]
+    for initial_mode, acl in acls[1:]:
+        if not acl_equal(reference_acl, acl):
+            t.fail("ACLs differ based on initial mode! "
+                   "File with mode 0%o has ACL %s, "
+                   "but file with mode 0%o has ACL %s. "
+                   "RFC 8881 §6.4.1.2: ACL should be set as given, "
+                   "independent of existing mode." %
+                   (reference_mode, acl_to_string(reference_acl),
+                    initial_mode, acl_to_string(acl)))
+
+    # Also verify that the mode derived from the ACL is consistent
+    modes = []
+    for initial_mode, acl in acls:
+        derived_mode = acl2mode_rfc8881(acl)
+        modes.append(derived_mode)
+
+    if len(set(modes)) != 1:
+        mode_str = ", ".join("0%o (from initial 0%o)" % (m, acls[i][0])
+                             for i, m in enumerate(modes))
+        t.fail("ACLs derive to different modes: %s. "
+               "This suggests ACLs were stored differently based on initial mode." %
+               mode_str)
+
+def testSetattrACLIndependentModeHighBits(t, env):
+    """SETATTR(ACL) should preserve high-order mode bits (SUID/SGID/SVTX)
+
+    Per RFC 8881 §6.4.1.2, when setting ACL without mode, the three
+    high-order bits of mode (SUID, SGID, SVTX) SHOULD remain unchanged.
+    Only the low-order nine permission bits should be modified.
+
+    FLAGS: setattr acl all
+    DEPEND: MODE ACL0
+    CODE: SATT29
+    """
+    from nfs4acl import acl2mode_rfc8881
+    c = env.c1
+    c.init_connection()
+
+    # Create ACL to set
+    test_acl = [
+        nfsace4(ACE4_ACCESS_ALLOWED_ACE_TYPE, 0,
+                ACE4_READ_DATA | ACE4_WRITE_DATA | ACE4_APPEND_DATA | ACE4_EXECUTE,
+                b"OWNER@"),
+        nfsace4(ACE4_ACCESS_ALLOWED_ACE_TYPE, 0,
+                ACE4_READ_DATA,
+                b"GROUP@")
+    ]
+
+    # Test with different high-order bits set
+    test_cases = [
+        (0o4755, "SUID"),      # Set-user-ID
+        (0o2755, "SGID"),      # Set-group-ID
+        (0o1755, "SVTX"),      # Sticky bit
+        (0o6755, "SUID+SGID"), # Both SUID and SGID
+        (0o7755, "ALL"),       # All three bits
+    ]
+
+    for initial_mode, description in test_cases:
+        fh, stateid = c.create_confirm(t.word() + (b"_%s" % description.encode()))
+
+        # Set initial mode with high-order bits
+        ops = c.use_obj(fh) + [c.setattr({FATTR4_MODE: initial_mode})]
+        res = c.compound(ops)
+        check(res, msg="Setting initial mode 0%o (%s)" % (initial_mode, description))
+
+        # Verify the mode was set
+        before_attrs = c.do_getattrdict(fh, [FATTR4_MODE])
+        before_mode = before_attrs[FATTR4_MODE] & 0o7777
+
+        # Set ACL only (not mode)
+        ops = c.use_obj(fh) + [c.setattr({FATTR4_ACL: test_acl})]
+        res = c.compound(ops)
+        check(res, msg="SETATTR ACL on file with %s" % description)
+
+        # Check that high-order bits are preserved
+        after_attrs = c.do_getattrdict(fh, [FATTR4_MODE, FATTR4_ACL])
+        after_mode = after_attrs[FATTR4_MODE] & 0o7777
+
+        # Extract high-order bits (SUID, SGID, SVTX)
+        before_high = before_mode & 0o7000
+        after_high = after_mode & 0o7000
+
+        if before_high != after_high:
+            t.fail("High-order mode bits not preserved for %s: "
+                   "before=0%o, after=0%o. RFC 8881 §6.4.1.2 says "
+                   "high-order bits SHOULD remain unchanged." %
+                   (description, before_mode, after_mode))
+
+        # Verify low-order bits match ACL derivation
+        after_low = after_mode & 0o777
+        expected_low = acl2mode_rfc8881(after_attrs[FATTR4_ACL])
+
+        if after_low != expected_low:
+            t.fail("Low-order mode bits (0%o) don't match RFC 8881 §6.3.2 "
+                   "derivation (expected 0%o) for %s" %
+                   (after_low, expected_low, description))
+
+def testSetattrModeACLattrsset(t, env):
+    """SETATTR(MODE+ACL) should indicate which attributes were actually set
+
+    Per RFC 8881 §6.4.1.3, when both MODE and ACL are set together, the
+    server processes MODE first, then ACL (which may modify the mode).
+    The attrsset bitmap indicates which attributes were actually set.
+    This test verifies the attrsset bitmap and final mode consistency.
+
+    FLAGS: setattr acl all
+    DEPEND: MODE ACL0
+    CODE: SATT30
+    """
+    from nfs4acl import acl2mode_rfc8881
+    c = env.c1
+    c.init_connection()
+
+    fh, stateid = c.create_confirm(t.word())
+
+    # Create an ACL that will derive to a specific mode (0754)
+    acl = [
+        nfsace4(ACE4_ACCESS_ALLOWED_ACE_TYPE, 0,
+                ACE4_READ_DATA | ACE4_WRITE_DATA | ACE4_APPEND_DATA | ACE4_EXECUTE,
+                b"OWNER@"),
+        nfsace4(ACE4_ACCESS_ALLOWED_ACE_TYPE, 0,
+                ACE4_READ_DATA | ACE4_EXECUTE,
+                b"GROUP@"),
+        nfsace4(ACE4_ACCESS_ALLOWED_ACE_TYPE, 0,
+                ACE4_READ_DATA,
+                b"EVERYONE@")
+    ]
+
+    # Request a different mode (0640)
+    requested_mode = 0o640
+
+    # Set both MODE and ACL together
+    attrs = {FATTR4_MODE: requested_mode, FATTR4_ACL: acl}
+    ops = c.use_obj(fh) + [c.setattr(attrs)]
+    res = c.compound(ops)
+    check(res, msg="SETATTR with MODE and ACL together")
+
+    # Check the attrsset bitmap
+    attrsset = bitmap2list(res.resarray[-1].attrsset)
+
+    # Get the final attributes
+    final_attrs = c.do_getattrdict(fh, [FATTR4_MODE, FATTR4_ACL])
+    final_mode = final_attrs[FATTR4_MODE] & 0o7777
+    final_acl = final_attrs[FATTR4_ACL]
+
+    # ACL should always be set
+    if FATTR4_ACL not in attrsset:
+        t.fail("FATTR4_ACL not in attrsset, but ACL was requested")
+
+    # Compute what mode should be derived from the ACL
+    acl_derived_mode = acl2mode_rfc8881(final_acl)
+
+    if FATTR4_MODE in attrsset:
+        # Server claims it set MODE - but per §6.4.1.3, the ACL processing
+        # will modify the mode. The final mode should match ACL derivation,
+        # not necessarily the requested mode.
+        if final_mode != acl_derived_mode:
+            t.fail("Server set MODE in attrsset, but final mode (0%o) "
+                   "doesn't match ACL-derived mode (0%o). "
+                   "Per RFC 8881 §6.4.1.3, ACL processing modifies mode." %
+                   (final_mode, acl_derived_mode))
+    else:
+        # Server did not include MODE in attrsset - this means it recognized
+        # that the ACL processing would override the requested mode.
+        # Final mode should still match ACL derivation.
+        if final_mode != acl_derived_mode:
+            t.fail("MODE not in attrsset (expected behavior), but final mode "
+                   "(0%o) doesn't match ACL-derived mode (0%o). "
+                   "Per RFC 8881 §6.4.1.2, mode should be derived from ACL." %
+                   (final_mode, acl_derived_mode))
+
+def testSetattrModeACLConflict(t, env):
+    """SETATTR(MODE+ACL) when MODE and ACL would produce different permissions
+
+    Test the interaction when the requested MODE differs significantly from
+    what the ACL would derive. Per RFC 8881 §6.4.1.3, MODE is applied first,
+    then ACL is applied (possibly changing the final mode).
+
+    FLAGS: setattr acl all
+    DEPEND: MODE ACL0
+    CODE: SATT31
+    """
+    from nfs4acl import acl2mode_rfc8881
+    c = env.c1
+    c.init_connection()
+
+    # Test multiple conflict scenarios
+    test_cases = [
+        # (requested_mode, ACL, description)
+        (0o777, [  # Request all permissions
+            nfsace4(ACE4_ACCESS_ALLOWED_ACE_TYPE, 0,
+                    ACE4_READ_DATA,  # But ACL only grants read
+                    b"OWNER@")
+        ], "MODE=0777 but ACL grants only read"),
+
+        (0o000, [  # Request no permissions
+            nfsace4(ACE4_ACCESS_ALLOWED_ACE_TYPE, 0,
+                    ACE4_READ_DATA | ACE4_WRITE_DATA | ACE4_APPEND_DATA | ACE4_EXECUTE,
+                    b"OWNER@")  # But ACL grants everything
+        ], "MODE=0000 but ACL grants rwx"),
+
+        (0o644, [  # Request read for all, write for owner
+            nfsace4(ACE4_ACCESS_ALLOWED_ACE_TYPE, 0,
+                    ACE4_EXECUTE,  # But ACL only grants execute
+                    b"OWNER@")
+        ], "MODE=0644 but ACL grants only execute"),
+    ]
+
+    for requested_mode, acl, description in test_cases:
+        fh, stateid = c.create_confirm(t.word() + ("_%o" % requested_mode).encode())
+
+        # Set both MODE and ACL together
+        attrs = {FATTR4_MODE: requested_mode, FATTR4_ACL: acl}
+        ops = c.use_obj(fh) + [c.setattr(attrs)]
+        res = c.compound(ops)
+        check(res, msg="SETATTR MODE+ACL: %s" % description)
+
+        # Get final attributes
+        final_attrs = c.do_getattrdict(fh, [FATTR4_MODE, FATTR4_ACL])
+        final_mode = final_attrs[FATTR4_MODE] & 0o777
+        final_acl = final_attrs[FATTR4_ACL]
+
+        # Verify final mode matches ACL derivation
+        expected_mode = acl2mode_rfc8881(final_acl)
+
+        if final_mode != expected_mode:
+            t.fail("Test case '%s': final mode (0%o) doesn't match "
+                   "ACL-derived mode (0%o). Per RFC 8881 §6.4.1.3, "
+                   "ACL should be set as given and mode derived from it." %
+                   (description, final_mode, expected_mode))
+
+def testSetattrACLNamedPrincipals(t, env):
+    """SETATTR(ACL) with named principals should preserve them
+
+    Per RFC 8881 §6.4.1.3: "the ACL attribute is set as given."
+    When an ACL contains named principals (not OWNER@/GROUP@/EVERYONE@),
+    the server must preserve those exact principals in the stored ACL,
+    not convert them to special identifiers.
+
+    FLAGS: setattr acl all
+    DEPEND: MODE ACL0
+    CODE: SATT32
+    """
+    c = env.c1
+    c.init_connection()
+
+    # First create a file and get its owner and group
+    temp_fh, temp_stateid = c.create_confirm(t.word() + b"_temp")
+    attrs = c.do_getattrdict(temp_fh, [FATTR4_OWNER, FATTR4_OWNER_GROUP])
+    current_owner = attrs[FATTR4_OWNER]
+    current_group = attrs[FATTR4_OWNER_GROUP]
+
+    # Test 1: SETATTR(ACL) with named principals only
+    fh1, stateid1 = c.create_confirm(t.word() + b"_acl_only")
+
+    acl = [
+        nfsace4(ACE4_ACCESS_ALLOWED_ACE_TYPE, 0,
+                ACE4_READ_DATA | ACE4_WRITE_DATA | ACE4_APPEND_DATA,
+                current_owner),  # Named principal, not OWNER@
+        nfsace4(ACE4_ACCESS_ALLOWED_ACE_TYPE, 0,
+                ACE4_READ_DATA,
+                current_group)  # Named principal, not GROUP@
+    ]
+
+    ops = c.use_obj(fh1) + [c.setattr({FATTR4_ACL: acl})]
+    res = c.compound(ops)
+    check(res, msg="SETATTR ACL with named principals")
+
+    # Verify ACL was preserved with named principals
+    returned_attrs = c.do_getattrdict(fh1, [FATTR4_ACL])
+    returned_acl = returned_attrs[FATTR4_ACL]
+
+    if len(returned_acl) < len(acl):
+        t.fail("Returned ACL has fewer entries than requested: "
+               "expected at least %d, got %d" % (len(acl), len(returned_acl)))
+
+    for i, expected_ace in enumerate(acl):
+        if i >= len(returned_acl):
+            t.fail("Missing ACE %d in returned ACL" % i)
+        returned_ace = returned_acl[i]
+
+        if returned_ace.who != expected_ace.who:
+            t.fail("ACE %d who mismatch: expected %s, got %s. "
+                   "Server converted named principal to special identifier, "
+                   "violating RFC 8881 §6.4.1.3 'ACL attribute is set as given'" %
+                   (i, expected_ace.who, returned_ace.who))
+
+    # Test 2: SETATTR(MODE+ACL) with named principals
+    fh2, stateid2 = c.create_confirm(t.word() + b"_mode_acl")
+
+    mode = 0o755
+    acl = [
+        nfsace4(ACE4_ACCESS_ALLOWED_ACE_TYPE, 0,
+                ACE4_READ_DATA | ACE4_WRITE_DATA | ACE4_APPEND_DATA | ACE4_EXECUTE,
+                current_owner),  # Named principal
+        nfsace4(ACE4_ACCESS_ALLOWED_ACE_TYPE, 0,
+                ACE4_READ_DATA | ACE4_EXECUTE,
+                current_group),  # Named principal
+        nfsace4(ACE4_ACCESS_ALLOWED_ACE_TYPE, 0,
+                ACE4_READ_DATA,
+                b"EVERYONE@")  # Special identifier is fine
+    ]
+
+    ops = c.use_obj(fh2) + [c.setattr({FATTR4_MODE: mode, FATTR4_ACL: acl})]
+    res = c.compound(ops)
+    check(res, msg="SETATTR MODE+ACL with named principals")
+
+    # Verify ACL was preserved
+    returned_attrs = c.do_getattrdict(fh2, [FATTR4_ACL])
+    returned_acl = returned_attrs[FATTR4_ACL]
+
+    if len(returned_acl) < len(acl):
+        t.fail("Returned ACL has fewer entries than requested: "
+               "expected at least %d, got %d" % (len(acl), len(returned_acl)))
+
+    for i, expected_ace in enumerate(acl):
+        if i >= len(returned_acl):
+            t.fail("Missing ACE %d in returned ACL" % i)
+        returned_ace = returned_acl[i]
+
+        if returned_ace.who != expected_ace.who:
+            t.fail("ACE %d who mismatch: expected %s, got %s. "
+                   "Server converted named principal to special identifier, "
+                   "violating RFC 8881 §6.4.1.3" %
+                   (i, expected_ace.who, returned_ace.who))
-- 
2.51.1


^ permalink raw reply related	[flat|nested] 10+ messages in thread

* [PATCH v2 9/9] Add tests for OPEN(create) with ACLs
  2025-11-25 19:49 [PATCH v2 0/9] pynfs tests for setting ACL+MODE Chuck Lever
                   ` (7 preceding siblings ...)
  2025-11-25 19:49 ` [PATCH v2 8/9] Add tests for SETATTR with MODE and ACL Chuck Lever
@ 2025-11-25 19:49 ` Chuck Lever
  8 siblings, 0 replies; 10+ messages in thread
From: Chuck Lever @ 2025-11-25 19:49 UTC (permalink / raw)
  To: linux-nfs; +Cc: Calum Mackay, aurelien.couderc2002, Chuck Lever

From: Chuck Lever <chuck.lever@oracle.com>

Check that the server can attach an ACL when it creates a file.

Add tests for OPEN(create) with MODE and ACL

Add comprehensive tests that verify OPEN(create) behavior when
both mode bits and an ACL are set simultaneously. The new tests
cover:

- EXCLUSIVE4_1 create with both MODE and ACL attributes (OPEN14)
- MODE preservation when set with ACL for all three create modes:
  UNCHECKED4 (OPEN15), GUARDED4 (OPEN16), and EXCLUSIVE4_1
  (OPEN17)
- Restrictive MODE (0o400) with ACL for all create modes (OPEN18)

These tests ensure that servers correctly handle the interaction
between MODE and ACL attributes during file creation, and verify
that the ACL specified in the OPEN operation matches the ACL
after the OPEN completes.

OPEN15-17: Fix MODE+ACL verification per RFC 8881

Apply the same fix used for SATT19-21 to OPEN15-17. When both
MODE and ACL are set together during OPEN CREATE, RFC 8881
§6.4.1.3 specifies that both attributes are processed, but the
final mode is derived from the ACL per §6.3.2, not from the
requested MODE value.

The tests now verify that:
1. Both MODE and ACL are processed (in attrset bitmap)
2. The final mode matches the ACL-derived mode per RFC 8881
   §6.3.2 using acl2mode_rfc8881()
3. The mode may differ from the requested MODE

This fixes OPEN15 (UNCHECKED4), OPEN16 (GUARDED4), and OPEN17
(EXCLUSIVE4_1) which were previously failing with "MODE not
preserved" errors.

OPEN41-42: Set ACE4_IDENTIFIER_GROUP flag for group principals

Fix OPEN41 and OPEN42 to properly set the ACE4_IDENTIFIER_GROUP
flag when creating ACEs for named group principals. Without this
flag, the kernel treats the principal as a user (ACL_USER) instead
of a group (ACL_GROUP), preventing proper synthetic GROUP@
detection and filtering.

The tests were creating group principal ACEs with flag=0, which
should have been flag=ACE4_IDENTIFIER_GROUP. This is required for
the kernel to distinguish between:
- Named user principals (no flag) -> ACL_USER
- Named group principals (with flag) -> ACL_GROUP

With this fix, both tests now pass, and the server correctly
returns ACLs with named principals "as given" per RFC 8881
§6.4.1.2, without exposing synthetic OWNER@ or GROUP@ entries.

Signed-off-by: Chuck Lever <chuck.lever@oracle.com>
---
 nfs4.1/server41tests/st_open.py | 723 +++++++++++++++++++++++++++++++-
 1 file changed, 720 insertions(+), 3 deletions(-)

diff --git a/nfs4.1/server41tests/st_open.py b/nfs4.1/server41tests/st_open.py
index 28540b59a8fe..115daf9f5273 100644
--- a/nfs4.1/server41tests/st_open.py
+++ b/nfs4.1/server41tests/st_open.py
@@ -1,15 +1,16 @@
 from .st_create_session import create_session
 from xdrdef.nfs4_const import *
 
-from .environment import check, fail, create_file, open_file, close_file, write_file, read_file
-from .environment import open_create_file_op
+from .environment import check, fail, unsupported, create_file, open_file, close_file, write_file, read_file
+from .environment import open_create_file_op, do_getattrdict
 from xdrdef.nfs4_type import open_owner4, openflag4, createhow4, open_claim4
 from xdrdef.nfs4_type import creatverfattr, fattr4, stateid4, locker4, lock_owner4
-from xdrdef.nfs4_type import open_to_lock_owner4
+from xdrdef.nfs4_type import open_to_lock_owner4, nfsace4
 import nfs_ops
 op = nfs_ops.NFS4ops()
 import threading
 import nfs4lib
+import nfs4acl
 
 def expect(res, seqid):
     """Verify that open result has expected stateid.seqid"""
@@ -195,3 +196,719 @@ def testCloseWithZeroSeqid(t, env):
     stateid.seqid = 0
     res = close_file(sess1, fh, stateid=stateid)
     check(res)
+
+def testSuppattrExclcreat(t, env):
+    """Check that FATTR4_SUPPATTR_EXCLCREAT is supported and valid
+
+    FLAGS: open all
+    CODE: OPEN12
+    """
+    sess = env.c1.new_client_session(env.testname(t))
+    res = sess.compound([op.putrootfh(),
+                        op.getattr(nfs4lib.list2bitmap([FATTR4_SUPPORTED_ATTRS,
+                                                         FATTR4_SUPPATTR_EXCLCREAT]))])
+    check(res)
+    attrs_info = res.resarray[-1].obj_attributes
+
+    if FATTR4_SUPPORTED_ATTRS not in attrs_info:
+        fail("Server did not return FATTR4_SUPPORTED_ATTRS")
+
+    # Check if SUPPATTR_EXCLCREAT is in the supported attributes
+    supported = attrs_info[FATTR4_SUPPORTED_ATTRS]
+    if not (supported & (1 << FATTR4_SUPPATTR_EXCLCREAT)):
+        unsupported("Server does not support FATTR4_SUPPATTR_EXCLCREAT")
+
+    # If supported, check that it was returned
+    if FATTR4_SUPPATTR_EXCLCREAT not in attrs_info:
+        fail("FATTR4_SUPPATTR_EXCLCREAT not returned even though it "
+             "appears in FATTR4_SUPPORTED_ATTRS")
+
+def testSuppattrExclcreatSubset(t, env):
+    """FATTR4_SUPPATTR_EXCLCREAT must be subset of SUPPORTED_ATTRS
+
+    FLAGS: open all
+    CODE: OPEN13
+    DEPEND: OPEN12
+    """
+    sess = env.c1.new_client_session(env.testname(t))
+    res = sess.compound([op.putrootfh(),
+                        op.getattr(nfs4lib.list2bitmap([FATTR4_SUPPORTED_ATTRS,
+                                                         FATTR4_SUPPATTR_EXCLCREAT]))])
+    check(res)
+    attrs_info = res.resarray[-1].obj_attributes
+
+    supported = attrs_info[FATTR4_SUPPORTED_ATTRS]
+    excl_supported = attrs_info[FATTR4_SUPPATTR_EXCLCREAT]
+
+    # SUPPATTR_EXCLCREAT must be a subset of SUPPORTED_ATTRS
+    # i.e., every bit set in excl_supported must also be set in supported
+    invalid = excl_supported & ~supported
+    if invalid != 0:
+        fail("FATTR4_SUPPATTR_EXCLCREAT contains attributes not in "
+             "FATTR4_SUPPORTED_ATTRS:\n"
+             "  Invalid attributes: %s" % nfs4lib.attr_bitmap_to_str(invalid))
+
+def testACLSupported(t, env):
+    """Check that server supports FATTR4_ACL attribute
+
+    FLAGS: open acl all
+    CODE: OPEN8a
+    """
+    sess = env.c1.new_client_session(env.testname(t))
+    res = sess.compound([op.putrootfh(),
+                        op.getattr(nfs4lib.list2bitmap([FATTR4_SUPPORTED_ATTRS]))])
+    check(res)
+    attrs_info = res.resarray[-1].obj_attributes
+    if FATTR4_SUPPORTED_ATTRS not in attrs_info:
+        fail("Server did not return FATTR4_SUPPORTED_ATTRS")
+    supported = attrs_info[FATTR4_SUPPORTED_ATTRS]
+    if not (supported & (1 << FATTR4_ACL)):
+        unsupported("Server does not support FATTR4_ACL attribute")
+
+def testACLExclusiveSupported(t, env):
+    """Check that server supports setting ACL during EXCLUSIVE4_1 create
+
+    FLAGS: open acl all
+    CODE: OPEN8b
+    """
+    sess = env.c1.new_client_session(env.testname(t))
+    res = sess.compound([op.putrootfh(),
+                        op.getattr(nfs4lib.list2bitmap([FATTR4_SUPPATTR_EXCLCREAT]))])
+    check(res)
+    attrs_info = res.resarray[-1].obj_attributes
+    if FATTR4_SUPPATTR_EXCLCREAT not in attrs_info:
+        unsupported("Server does not support FATTR4_SUPPATTR_EXCLCREAT")
+    excl_supported = attrs_info[FATTR4_SUPPATTR_EXCLCREAT]
+    if not (excl_supported & (1 << FATTR4_ACL)):
+        unsupported("Server does not support setting FATTR4_ACL during "
+                    "EXCLUSIVE4_1 create")
+
+def testOpenCreateWithACLUnchecked(t, env):
+    """OPEN with UNCHECKED4 CREATE setting NFSv4 ACL attribute
+
+    FLAGS: open acl all
+    CODE: OPEN9
+    DEPEND: OPEN8a
+    """
+    sess = env.c1.new_client_session(env.testname(t))
+    acl = nfs4acl.make_test_acl()
+
+    # Create file with ACL attribute using UNCHECKED4 mode
+    attrs = {FATTR4_MODE: 0o644, FATTR4_ACL: acl}
+    res = create_file(sess, env.testname(t), attrs=attrs, mode=UNCHECKED4)
+    check(res)
+    expect(res, seqid=1)
+
+    fh = res.resarray[-1].object
+    stateid = res.resarray[-2].stateid
+
+    # Verify the ACL was set correctly by reading it back
+    attrs_dict = do_getattrdict(sess, fh, [FATTR4_ACL])
+    if FATTR4_ACL not in attrs_dict:
+        fail("ACL attribute not returned after OPEN with CREATE")
+
+    try:
+        nfs4acl.verify_acl(attrs_dict[FATTR4_ACL], acl)
+    except AssertionError as e:
+        fail(str(e))
+
+    res = close_file(sess, fh, stateid=stateid)
+    check(res)
+
+def testOpenCreateWithACLGuarded(t, env):
+    """OPEN with GUARDED4 CREATE setting NFSv4 ACL attribute
+
+    FLAGS: open acl all
+    CODE: OPEN10
+    DEPEND: OPEN8a
+    """
+    sess = env.c1.new_client_session(env.testname(t))
+    acl = nfs4acl.make_test_acl()
+
+    # Create file with ACL attribute using GUARDED4 mode
+    attrs = {FATTR4_MODE: 0o644, FATTR4_ACL: acl}
+    res = create_file(sess, env.testname(t), attrs=attrs, mode=GUARDED4)
+    check(res)
+    expect(res, seqid=1)
+
+    fh = res.resarray[-1].object
+    stateid = res.resarray[-2].stateid
+
+    # Verify the ACL was set correctly by reading it back
+    attrs_dict = do_getattrdict(sess, fh, [FATTR4_ACL])
+    if FATTR4_ACL not in attrs_dict:
+        fail("ACL attribute not returned after OPEN with CREATE")
+
+    try:
+        nfs4acl.verify_acl(attrs_dict[FATTR4_ACL], acl)
+    except AssertionError as e:
+        fail(str(e))
+
+    res = close_file(sess, fh, stateid=stateid)
+    check(res)
+
+def testOpenCreateWithACLExclusive(t, env):
+    """OPEN with EXCLUSIVE4_1 CREATE setting NFSv4 ACL attribute
+
+    FLAGS: open acl all
+    CODE: OPEN11
+    DEPEND: OPEN8b
+    """
+    sess = env.c1.new_client_session(env.testname(t))
+    acl = nfs4acl.make_test_acl()
+
+    # Create file with ACL attribute using EXCLUSIVE4_1 mode
+    # EXCLUSIVE4_1 allows attributes to be set atomically with create
+    # Don't set MODE with ACL - let the ACL determine permissions
+    attrs = {FATTR4_ACL: acl}
+    verifier = b"testverif"
+    res = create_file(sess, env.testname(t), attrs=attrs,
+                      mode=EXCLUSIVE4_1, verifier=verifier)
+    check(res)
+    expect(res, seqid=1)
+
+    fh = res.resarray[-1].object
+    stateid = res.resarray[-2].stateid
+
+    # Verify the ACL was set correctly by reading it back
+    attrs_dict = do_getattrdict(sess, fh, [FATTR4_ACL])
+    if FATTR4_ACL not in attrs_dict:
+        fail("ACL attribute not returned after OPEN with CREATE")
+
+    try:
+        nfs4acl.verify_acl(attrs_dict[FATTR4_ACL], acl)
+    except AssertionError as e:
+        fail(str(e))
+
+    res = close_file(sess, fh, stateid=stateid)
+    check(res)
+
+def testOpenCreateModeWithACLExclusive(t, env):
+    """OPEN with EXCLUSIVE4_1 setting both MODE and ACL
+
+    FLAGS: open acl all
+    CODE: OPEN14
+    DEPEND: OPEN8b
+    """
+    sess = env.c1.new_client_session(env.testname(t))
+    acl = nfs4acl.make_test_acl()
+
+    # Create file with both MODE and ACL using EXCLUSIVE4_1
+    # This tests that server can handle both attributes together
+    attrs = {FATTR4_MODE: 0o640, FATTR4_ACL: acl}
+    verifier = b"testverif"
+    res = create_file(sess, env.testname(t), attrs=attrs,
+                      mode=EXCLUSIVE4_1, verifier=verifier)
+    check(res)
+    expect(res, seqid=1)
+
+    fh = res.resarray[-1].object
+    stateid = res.resarray[-2].stateid
+
+    # Verify both ACL and MODE were set correctly
+    attrs_dict = do_getattrdict(sess, fh, [FATTR4_ACL, FATTR4_MODE])
+    if FATTR4_ACL not in attrs_dict:
+        fail("ACL attribute not returned after OPEN with CREATE")
+    if FATTR4_MODE not in attrs_dict:
+        fail("MODE attribute not returned after OPEN with CREATE")
+
+    try:
+        nfs4acl.verify_acl(attrs_dict[FATTR4_ACL], acl)
+    except AssertionError as e:
+        fail(str(e))
+
+    res = close_file(sess, fh, stateid=stateid)
+    check(res)
+
+def testOpenCreateModePreservedUnchecked(t, env):
+    """Verify MODE derivation with ACL in UNCHECKED4 create
+
+    Per RFC 8881 Section 6.4.1.3, when both MODE and ACL are set together,
+    both are processed, but the final mode is derived from the ACL and may
+    differ from the requested MODE.
+
+    FLAGS: open acl all
+    CODE: OPEN15
+    DEPEND: OPEN8a
+    """
+    from nfs4acl import acl2mode_rfc8881
+    sess = env.c1.new_client_session(env.testname(t))
+    acl = nfs4acl.make_test_acl()
+    mode = 0o600
+
+    # Create file with both MODE and ACL
+    attrs = {FATTR4_MODE: mode, FATTR4_ACL: acl}
+    res = create_file(sess, env.testname(t), attrs=attrs, mode=UNCHECKED4)
+    check(res)
+    expect(res, seqid=1)
+
+    fh = res.resarray[-1].object
+    stateid = res.resarray[-2].stateid
+
+    # Check which attributes were actually set by examining the reply bitmask
+    attrset = nfs4lib.bitmap2list(res.resarray[-2].attrset)
+
+    # Verify both MODE and ACL were set (processed)
+    if FATTR4_MODE not in attrset:
+        fail("MODE not in attrset, but MODE was requested")
+    if FATTR4_ACL not in attrset:
+        fail("ACL not in attrset, but ACL was requested")
+
+    # Verify ACL was set correctly
+    attrs_dict = do_getattrdict(sess, fh, [FATTR4_ACL, FATTR4_MODE])
+    try:
+        nfs4acl.verify_mode_and_acl(attrs_dict, acl, "OPEN with CREATE")
+    except AssertionError as e:
+        fail(str(e))
+
+    res = close_file(sess, fh, stateid=stateid)
+    check(res)
+
+def testOpenCreateModePreservedGuarded(t, env):
+    """Verify MODE derivation with ACL in GUARDED4 create
+
+    Per RFC 8881 Section 6.4.1.3, when both MODE and ACL are set together,
+    both are processed, but the final mode is derived from the ACL and may
+    differ from the requested MODE.
+
+    FLAGS: open acl all
+    CODE: OPEN16
+    DEPEND: OPEN8a
+    """
+    from nfs4acl import acl2mode_rfc8881
+    sess = env.c1.new_client_session(env.testname(t))
+    acl = nfs4acl.make_test_acl()
+    mode = 0o640
+
+    # Create file with both MODE and ACL
+    attrs = {FATTR4_MODE: mode, FATTR4_ACL: acl}
+    res = create_file(sess, env.testname(t), attrs=attrs, mode=GUARDED4)
+    check(res)
+    expect(res, seqid=1)
+
+    fh = res.resarray[-1].object
+    stateid = res.resarray[-2].stateid
+
+    # Check which attributes were actually set by examining the reply bitmask
+    attrset = nfs4lib.bitmap2list(res.resarray[-2].attrset)
+
+    # Verify both MODE and ACL were set (processed)
+    if FATTR4_MODE not in attrset:
+        fail("MODE not in attrset, but MODE was requested")
+    if FATTR4_ACL not in attrset:
+        fail("ACL not in attrset, but ACL was requested")
+
+    # Verify ACL was set correctly
+    attrs_dict = do_getattrdict(sess, fh, [FATTR4_ACL, FATTR4_MODE])
+    try:
+        nfs4acl.verify_mode_and_acl(attrs_dict, acl, "OPEN with CREATE")
+    except AssertionError as e:
+        fail(str(e))
+
+    res = close_file(sess, fh, stateid=stateid)
+    check(res)
+
+def testOpenCreateModePreservedExclusive(t, env):
+    """Verify MODE derivation with ACL in EXCLUSIVE4_1 create
+
+    Per RFC 8881 Section 6.4.1.3, when both MODE and ACL are set together,
+    both are processed, but the final mode is derived from the ACL and may
+    differ from the requested MODE.
+
+    FLAGS: open acl all
+    CODE: OPEN17
+    DEPEND: OPEN8b
+    """
+    from nfs4acl import acl2mode_rfc8881
+    sess = env.c1.new_client_session(env.testname(t))
+    acl = nfs4acl.make_test_acl()
+    mode = 0o600
+
+    # Create file with both MODE and ACL using EXCLUSIVE4_1
+    attrs = {FATTR4_MODE: mode, FATTR4_ACL: acl}
+    verifier = b"testverif"
+    res = create_file(sess, env.testname(t), attrs=attrs,
+                      mode=EXCLUSIVE4_1, verifier=verifier)
+    check(res)
+    expect(res, seqid=1)
+
+    fh = res.resarray[-1].object
+    stateid = res.resarray[-2].stateid
+
+    # Check which attributes were actually set by examining the reply bitmask
+    attrset = nfs4lib.bitmap2list(res.resarray[-2].attrset)
+
+    # Verify both MODE and ACL were set (processed)
+    if FATTR4_MODE not in attrset:
+        fail("MODE not in attrset, but MODE was requested")
+    if FATTR4_ACL not in attrset:
+        fail("ACL not in attrset, but ACL was requested")
+
+    # Verify ACL was set correctly
+    attrs_dict = do_getattrdict(sess, fh, [FATTR4_ACL, FATTR4_MODE])
+    try:
+        nfs4acl.verify_mode_and_acl(attrs_dict, acl, "OPEN with CREATE")
+    except AssertionError as e:
+        fail(str(e))
+
+    res = close_file(sess, fh, stateid=stateid)
+    check(res)
+
+def testOpenCreateRestrictiveModeWithACL(t, env):
+    """Test OPEN CREATE with restrictive MODE and ACL together
+
+    Test all three create modes with a restrictive MODE (0o400) and ACL
+    to ensure the server handles the interaction correctly. The MODE
+    being more restrictive than the ACL is an interesting edge case.
+
+    FLAGS: open acl all
+    CODE: OPEN18
+    DEPEND: OPEN8a OPEN8b
+    """
+    sess = env.c1.new_client_session(env.testname(t))
+    acl = nfs4acl.make_test_acl()
+    mode = 0o400  # Read-only for owner
+
+    # Test UNCHECKED4
+    name1 = env.testname(t) + b"_unchecked"
+    attrs = {FATTR4_MODE: mode, FATTR4_ACL: acl}
+    res = create_file(sess, name1, attrs=attrs, mode=UNCHECKED4)
+    check(res)
+    fh1 = res.resarray[-1].object
+    stateid1 = res.resarray[-2].stateid
+
+    attrs_dict = do_getattrdict(sess, fh1, [FATTR4_ACL, FATTR4_MODE])
+    if FATTR4_ACL not in attrs_dict or FATTR4_MODE not in attrs_dict:
+        fail("ACL or MODE not returned after UNCHECKED4 CREATE")
+    try:
+        nfs4acl.verify_acl(attrs_dict[FATTR4_ACL], acl)
+    except AssertionError as e:
+        fail(str(e))
+
+    res = close_file(sess, fh1, stateid=stateid1)
+    check(res)
+
+    # Test GUARDED4
+    name2 = env.testname(t) + b"_guarded"
+    attrs = {FATTR4_MODE: mode, FATTR4_ACL: acl}
+    res = create_file(sess, name2, attrs=attrs, mode=GUARDED4)
+    check(res)
+    fh2 = res.resarray[-1].object
+    stateid2 = res.resarray[-2].stateid
+
+    attrs_dict = do_getattrdict(sess, fh2, [FATTR4_ACL, FATTR4_MODE])
+    if FATTR4_ACL not in attrs_dict or FATTR4_MODE not in attrs_dict:
+        fail("ACL or MODE not returned after GUARDED4 CREATE")
+    try:
+        nfs4acl.verify_acl(attrs_dict[FATTR4_ACL], acl)
+    except AssertionError as e:
+        fail(str(e))
+
+    res = close_file(sess, fh2, stateid=stateid2)
+    check(res)
+
+    # Test EXCLUSIVE4_1
+    name3 = env.testname(t) + b"_exclusive"
+    attrs = {FATTR4_MODE: mode, FATTR4_ACL: acl}
+    verifier = b"testverif"
+    res = create_file(sess, name3, attrs=attrs,
+                      mode=EXCLUSIVE4_1, verifier=verifier)
+    check(res)
+    fh3 = res.resarray[-1].object
+    stateid3 = res.resarray[-2].stateid
+
+    attrs_dict = do_getattrdict(sess, fh3, [FATTR4_ACL, FATTR4_MODE])
+    if FATTR4_ACL not in attrs_dict or FATTR4_MODE not in attrs_dict:
+        fail("ACL or MODE not returned after EXCLUSIVE4_1 CREATE")
+    try:
+        nfs4acl.verify_acl(attrs_dict[FATTR4_ACL], acl)
+    except AssertionError as e:
+        fail(str(e))
+
+    res = close_file(sess, fh3, stateid=stateid3)
+    check(res)
+
+#
+# Tests for OPEN CREATE with MODE and ACL per RFC 8881 §6.4.3
+#
+
+def testOpenCreateModeDerivation(t, env):
+    """OPEN CREATE with MODE should derive ACL from mode
+
+    Per RFC 8881 §6.4.3: If just mode is given, the mode MUST be
+    applied to the inherited/created ACL per §6.4.1.1. Test with
+    UNCHECKED4, GUARDED4, and EXCLUSIVE4_1.
+
+    FLAGS: open all
+    CODE: OPEN38
+    """
+    from nfs4acl import acl2mode_rfc8881
+
+    sess = env.c1.new_client_session(env.testname(t))
+
+    # Test with UNCHECKED4
+    mode = 0o640
+    attrs = {FATTR4_MODE: mode}
+    name1 = env.testname(t) + b"_unchecked"
+    res = create_file(sess, name1, attrs=attrs, mode=UNCHECKED4)
+    check(res)
+    fh1 = res.resarray[-1].object
+    stateid1 = res.resarray[-2].stateid
+
+    attrs_dict = do_getattrdict(sess, fh1, [FATTR4_MODE, FATTR4_ACL])
+    result_mode = attrs_dict[FATTR4_MODE] & 0o777
+    if result_mode != mode:
+        fail("UNCHECKED4: mode (0%o) doesn't match requested (0%o)" %
+             (result_mode, mode))
+
+    acl_derived_mode = acl2mode_rfc8881(attrs_dict[FATTR4_ACL])
+    if result_mode != acl_derived_mode:
+        fail("UNCHECKED4: mode (0%o) doesn't match ACL derivation (0%o)" %
+             (result_mode, acl_derived_mode))
+
+    res = close_file(sess, fh1, stateid=stateid1)
+    check(res)
+
+    # Test with GUARDED4
+    mode = 0o750
+    attrs = {FATTR4_MODE: mode}
+    name2 = env.testname(t) + b"_guarded"
+    res = create_file(sess, name2, attrs=attrs, mode=GUARDED4)
+    check(res)
+    fh2 = res.resarray[-1].object
+    stateid2 = res.resarray[-2].stateid
+
+    attrs_dict = do_getattrdict(sess, fh2, [FATTR4_MODE, FATTR4_ACL])
+    result_mode = attrs_dict[FATTR4_MODE] & 0o777
+    if result_mode != mode:
+        fail("GUARDED4: mode (0%o) doesn't match requested (0%o)" %
+             (result_mode, mode))
+
+    acl_derived_mode = acl2mode_rfc8881(attrs_dict[FATTR4_ACL])
+    if result_mode != acl_derived_mode:
+        fail("GUARDED4: mode (0%o) doesn't match ACL derivation (0%o)" %
+             (result_mode, acl_derived_mode))
+
+    res = close_file(sess, fh2, stateid=stateid2)
+    check(res)
+
+    # Test with EXCLUSIVE4_1
+    mode = 0o755
+    attrs = {FATTR4_MODE: mode}
+    name3 = env.testname(t) + b"_exclusive"
+    verifier = b"testverif"
+    res = create_file(sess, name3, attrs=attrs,
+                      mode=EXCLUSIVE4_1, verifier=verifier)
+    check(res)
+    fh3 = res.resarray[-1].object
+    stateid3 = res.resarray[-2].stateid
+
+    attrs_dict = do_getattrdict(sess, fh3, [FATTR4_MODE, FATTR4_ACL])
+    result_mode = attrs_dict[FATTR4_MODE] & 0o777
+    if result_mode != mode:
+        fail("EXCLUSIVE4_1: mode (0%o) doesn't match requested (0%o)" %
+             (result_mode, mode))
+
+    acl_derived_mode = acl2mode_rfc8881(attrs_dict[FATTR4_ACL])
+    if result_mode != acl_derived_mode:
+        fail("EXCLUSIVE4_1: mode (0%o) doesn't match ACL derivation (0%o)" %
+             (result_mode, acl_derived_mode))
+
+    res = close_file(sess, fh3, stateid=stateid3)
+    check(res)
+
+def testOpenCreateACLDerivation(t, env):
+    """OPEN CREATE with ACL should derive mode from ACL
+
+    Per RFC 8881 §6.4.3: If just ACL is given, inheritance SHOULD NOT
+    take place, ACL should be set as given, and mode modified per §6.4.1.2.
+
+    FLAGS: open all
+    CODE: OPEN39
+    """
+    from nfs4acl import acl2mode_rfc8881
+
+    sess = env.c1.new_client_session(env.testname(t))
+
+    # Test with UNCHECKED4
+    acl = [
+        nfsace4(ACE4_ACCESS_ALLOWED_ACE_TYPE, 0,
+                ACE4_READ_DATA | ACE4_WRITE_DATA | ACE4_APPEND_DATA,
+                b"OWNER@"),
+        nfsace4(ACE4_ACCESS_ALLOWED_ACE_TYPE, 0,
+                ACE4_READ_DATA,
+                b"GROUP@")
+    ]
+    attrs = {FATTR4_ACL: acl}
+    name1 = env.testname(t) + b"_unchecked"
+    res = create_file(sess, name1, attrs=attrs, mode=UNCHECKED4)
+    check(res)
+    fh1 = res.resarray[-1].object
+    stateid1 = res.resarray[-2].stateid
+
+    attrs_dict = do_getattrdict(sess, fh1, [FATTR4_MODE, FATTR4_ACL])
+    result_mode = attrs_dict[FATTR4_MODE] & 0o777
+    expected_mode = acl2mode_rfc8881(attrs_dict[FATTR4_ACL])
+
+    if result_mode != expected_mode:
+        fail("UNCHECKED4: mode (0%o) doesn't match ACL derivation (0%o)" %
+             (result_mode, expected_mode))
+
+    res = close_file(sess, fh1, stateid=stateid1)
+    check(res)
+
+    # Test with GUARDED4
+    acl = [
+        nfsace4(ACE4_ACCESS_ALLOWED_ACE_TYPE, 0,
+                ACE4_READ_DATA | ACE4_EXECUTE,
+                b"OWNER@")
+    ]
+    attrs = {FATTR4_ACL: acl}
+    name2 = env.testname(t) + b"_guarded"
+    res = create_file(sess, name2, attrs=attrs, mode=GUARDED4)
+    check(res)
+    fh2 = res.resarray[-1].object
+    stateid2 = res.resarray[-2].stateid
+
+    attrs_dict = do_getattrdict(sess, fh2, [FATTR4_MODE, FATTR4_ACL])
+    result_mode = attrs_dict[FATTR4_MODE] & 0o777
+    expected_mode = acl2mode_rfc8881(attrs_dict[FATTR4_ACL])
+
+    if result_mode != expected_mode:
+        fail("GUARDED4: mode (0%o) doesn't match ACL derivation (0%o)" %
+             (result_mode, expected_mode))
+
+    res = close_file(sess, fh2, stateid=stateid2)
+    check(res)
+
+    # Test with EXCLUSIVE4_1
+    acl = [
+        nfsace4(ACE4_ACCESS_ALLOWED_ACE_TYPE, 0,
+                ACE4_READ_DATA | ACE4_WRITE_DATA | ACE4_APPEND_DATA | ACE4_EXECUTE,
+                b"OWNER@"),
+        nfsace4(ACE4_ACCESS_ALLOWED_ACE_TYPE, 0,
+                ACE4_READ_DATA | ACE4_EXECUTE,
+                b"GROUP@"),
+        nfsace4(ACE4_ACCESS_ALLOWED_ACE_TYPE, 0,
+                ACE4_READ_DATA,
+                b"EVERYONE@")
+    ]
+    attrs = {FATTR4_ACL: acl}
+    name3 = env.testname(t) + b"_exclusive"
+    verifier = b"testverif"
+    res = create_file(sess, name3, attrs=attrs,
+                      mode=EXCLUSIVE4_1, verifier=verifier)
+    check(res)
+    fh3 = res.resarray[-1].object
+    stateid3 = res.resarray[-2].stateid
+
+    attrs_dict = do_getattrdict(sess, fh3, [FATTR4_MODE, FATTR4_ACL])
+    result_mode = attrs_dict[FATTR4_MODE] & 0o777
+    expected_mode = acl2mode_rfc8881(attrs_dict[FATTR4_ACL])
+
+    if result_mode != expected_mode:
+        fail("EXCLUSIVE4_1: mode (0%o) doesn't match ACL derivation (0%o)" %
+             (result_mode, expected_mode))
+
+    res = close_file(sess, fh3, stateid=stateid3)
+    check(res)
+
+def testOpenCreateModeACLConflict(t, env):
+    """OPEN CREATE with both MODE and ACL - ACL should win
+
+    Per RFC 8881 §6.4.3: If both mode and ACL are given, both attributes
+    will be set per §6.4.1.3 (MODE first, then ACL, with ACL modifying
+    the final mode).
+
+    FLAGS: open all
+    CODE: OPEN40
+    """
+    from nfs4acl import acl2mode_rfc8881
+
+    sess = env.c1.new_client_session(env.testname(t))
+
+    # Test with UNCHECKED4: ACL would derive to 0754, request MODE 0640
+    acl = [
+        nfsace4(ACE4_ACCESS_ALLOWED_ACE_TYPE, 0,
+                ACE4_READ_DATA | ACE4_WRITE_DATA | ACE4_APPEND_DATA | ACE4_EXECUTE,
+                b"OWNER@"),
+        nfsace4(ACE4_ACCESS_ALLOWED_ACE_TYPE, 0,
+                ACE4_READ_DATA | ACE4_EXECUTE,
+                b"GROUP@"),
+        nfsace4(ACE4_ACCESS_ALLOWED_ACE_TYPE, 0,
+                ACE4_READ_DATA,
+                b"EVERYONE@")
+    ]
+    mode = 0o640
+    attrs = {FATTR4_MODE: mode, FATTR4_ACL: acl}
+    name1 = env.testname(t) + b"_unchecked"
+    res = create_file(sess, name1, attrs=attrs, mode=UNCHECKED4)
+    check(res)
+    fh1 = res.resarray[-1].object
+    stateid1 = res.resarray[-2].stateid
+
+    attrs_dict = do_getattrdict(sess, fh1, [FATTR4_MODE, FATTR4_ACL])
+    result_mode = attrs_dict[FATTR4_MODE] & 0o777
+    expected_mode = acl2mode_rfc8881(attrs_dict[FATTR4_ACL])
+
+    if result_mode != expected_mode:
+        fail("UNCHECKED4: mode (0%o) doesn't match ACL derivation (0%o). "
+             "Per §6.4.3, should follow §6.4.1.3 (ACL wins)" %
+             (result_mode, expected_mode))
+
+    res = close_file(sess, fh1, stateid=stateid1)
+    check(res)
+
+    # Test with GUARDED4
+    acl = [
+        nfsace4(ACE4_ACCESS_ALLOWED_ACE_TYPE, 0,
+                ACE4_READ_DATA | ACE4_WRITE_DATA | ACE4_APPEND_DATA,
+                b"OWNER@")
+    ]
+    mode = 0o755
+    attrs = {FATTR4_MODE: mode, FATTR4_ACL: acl}
+    name2 = env.testname(t) + b"_guarded"
+    res = create_file(sess, name2, attrs=attrs, mode=GUARDED4)
+    check(res)
+    fh2 = res.resarray[-1].object
+    stateid2 = res.resarray[-2].stateid
+
+    attrs_dict = do_getattrdict(sess, fh2, [FATTR4_MODE, FATTR4_ACL])
+    result_mode = attrs_dict[FATTR4_MODE] & 0o777
+    expected_mode = acl2mode_rfc8881(attrs_dict[FATTR4_ACL])
+
+    if result_mode != expected_mode:
+        fail("GUARDED4: mode (0%o) doesn't match ACL derivation (0%o). "
+             "Per §6.4.3, should follow §6.4.1.3" %
+             (result_mode, expected_mode))
+
+    res = close_file(sess, fh2, stateid=stateid2)
+    check(res)
+
+    # Test with EXCLUSIVE4_1
+    acl = [
+        nfsace4(ACE4_ACCESS_ALLOWED_ACE_TYPE, 0,
+                ACE4_EXECUTE,
+                b"OWNER@")
+    ]
+    mode = 0o644
+    attrs = {FATTR4_MODE: mode, FATTR4_ACL: acl}
+    name3 = env.testname(t) + b"_exclusive"
+    verifier = b"testverif"
+    res = create_file(sess, name3, attrs=attrs,
+                      mode=EXCLUSIVE4_1, verifier=verifier)
+    check(res)
+    fh3 = res.resarray[-1].object
+    stateid3 = res.resarray[-2].stateid
+
+    attrs_dict = do_getattrdict(sess, fh3, [FATTR4_MODE, FATTR4_ACL])
+    result_mode = attrs_dict[FATTR4_MODE] & 0o777
+    expected_mode = acl2mode_rfc8881(attrs_dict[FATTR4_ACL])
+
+    if result_mode != expected_mode:
+        fail("EXCLUSIVE4_1: mode (0%o) doesn't match ACL derivation (0%o). "
+             "Per §6.4.3, should follow §6.4.1.3" %
+             (result_mode, expected_mode))
+
+    res = close_file(sess, fh3, stateid=stateid3)
+    check(res)
-- 
2.51.1


^ permalink raw reply related	[flat|nested] 10+ messages in thread

end of thread, other threads:[~2025-11-25 19:49 UTC | newest]

Thread overview: 10+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2025-11-25 19:49 [PATCH v2 0/9] pynfs tests for setting ACL+MODE Chuck Lever
2025-11-25 19:49 ` [PATCH v2 1/9] Add helper to report unsupported protocol features Chuck Lever
2025-11-25 19:49 ` [PATCH v2 2/9] Add helper to format attribute bitmaps Chuck Lever
2025-11-25 19:49 ` [PATCH v2 3/9] Add a helper to compute POSIX mode bits from NFSv4 ACLs Chuck Lever
2025-11-25 19:49 ` [PATCH v2 4/9] Add access_mask_to_str() helper to nfs4.0/nfs4acl.py Chuck Lever
2025-11-25 19:49 ` [PATCH v2 5/9] Add make_test_acl() helper to nfs4acl modules Chuck Lever
2025-11-25 19:49 ` [PATCH v2 6/9] Add verify_acl() " Chuck Lever
2025-11-25 19:49 ` [PATCH v2 7/9] Add verify_mode_and_acl() " Chuck Lever
2025-11-25 19:49 ` [PATCH v2 8/9] Add tests for SETATTR with MODE and ACL Chuck Lever
2025-11-25 19:49 ` [PATCH v2 9/9] Add tests for OPEN(create) with ACLs Chuck Lever

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox;
as well as URLs for NNTP newsgroup(s).