All of lore.kernel.org
 help / color / mirror / Atom feed
* [PATCH v1 0/8] Test the WANT_*_DELEG OPEN flags
@ 2026-10-08 15:47 Chuck Lever
  2026-10-08 15:47 ` [PATCH v1 1/8] pynfs: add test for OPEN(CLAIM_FH) with WANT_READ_DELEG Chuck Lever
                   ` (7 more replies)
  0 siblings, 8 replies; 10+ messages in thread
From: Chuck Lever @ 2026-10-08 15:47 UTC (permalink / raw)
  To: Calum Mackay; +Cc: linux-nfs

Add specific compliance tests for requesting delegations on
already-open files, as a pre-requisite to finishing the Linux
NFSD implementation of this feature.

Chuck Lever (8):
  pynfs: add test for OPEN(CLAIM_FH) with WANT_READ_DELEG
  pynfs: add test for OPEN(CLAIM_FH) delegation request on an open file
  pynfs: add test for a delegation request after a recall
  pynfs: add tests for SETATTR by a delegation holder
  pynfs: add test for WANT_OPEN_XOR_DELEGATION through CLAIM_FH
  pynfs: add test for a CLAIM_FH delegation that outlives its open
  pynfs: add tests for delegation upgrade and downgrade wants
  pynfs: add test for OPEN with WANT_CANCEL

 nfs4.1/server41tests/st_delegation.py | 405 ++++++++++++++++++++++++++
 1 file changed, 405 insertions(+)

-- 
2.55.0


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

* [PATCH v1 1/8] pynfs: add test for OPEN(CLAIM_FH) with WANT_READ_DELEG
  2026-10-08 15:47 [PATCH v1 0/8] Test the WANT_*_DELEG OPEN flags Chuck Lever
@ 2026-10-08 15:47 ` Chuck Lever
  2026-10-09  7:58   ` Substituting WANT_DELEGATION with OPEN(CLAIM_FH)? " Lionel Cons
  2026-10-08 15:47 ` [PATCH v1 2/8] pynfs: add test for OPEN(CLAIM_FH) delegation request on an open file Chuck Lever
                   ` (6 subsequent siblings)
  7 siblings, 1 reply; 10+ messages in thread
From: Chuck Lever @ 2026-10-08 15:47 UTC (permalink / raw)
  To: Calum Mackay; +Cc: linux-nfs

The delegation tests request a delegation only through CLAIM_NULL,
and OPEN7 pairs CLAIM_FH with WANT_NO_DELEG. No test covers a client
that already holds a filehandle and asks for a delegation with
OPEN(CLAIM_FH). On a server that does not implement WANT_DELEGATION,
that OPEN is the only way to ask.

Add DELEG27. It creates a file, closes it, then sends OPEN(CLAIM_FH)
with WANT_READ_DELEG and expects a read delegation. A failure reports
the delegation type and ond_why that the server returned instead.

Signed-off-by: Chuck Lever <cel@kernel.org>
---
 nfs4.1/server41tests/st_delegation.py | 61 +++++++++++++++++++++++++++
 1 file changed, 61 insertions(+)

diff --git a/nfs4.1/server41tests/st_delegation.py b/nfs4.1/server41tests/st_delegation.py
index 6ae89d223c7f..c35ce8f5ec3b 100644
--- a/nfs4.1/server41tests/st_delegation.py
+++ b/nfs4.1/server41tests/st_delegation.py
@@ -625,3 +625,64 @@ def testCbGetattrAfterSyncWrite(t, env):
     if compareTimes(attrs2[FATTR4_TIME_MODIFY], attrs4[FATTR4_TIME_MODIFY]) != 0:
         fail(f"mtime after write ({attrs2[FATTR4_TIME_MODIFY]}) != "
              f"mtime from delegreturn ({attrs4[FATTR4_TIME_MODIFY]})")
+
+def _open_claim_fh(sess, fh, owner, access):
+    """OPEN by filehandle. No GETFH follows, so OPEN is the last result."""
+    open_op = op.open(0, access, OPEN4_SHARE_DENY_NONE, open_owner4(0, owner),
+                      openflag4(OPEN4_NOCREATE), open_claim4(CLAIM_FH))
+    return sess.compound([op.putfh(fh), open_op])
+
+def _open_claim_fh_retry(sess, fh, owner, access):
+    """Send OPEN(CLAIM_FH) again if the first reply carries no delegation
+
+    The first OPEN after CREATE_SESSION can race the server's backchannel
+    probe.
+    """
+    res = _open_claim_fh(sess, fh, owner, access)
+    check(res)
+    if not _got_deleg(res.resarray[-1].delegation):
+        res = _open_claim_fh(sess, fh, owner, access)
+        check(res)
+    return res
+
+def _delegreturn(sess, fh, deleg):
+    """Return the delegation, if OPEN granted one"""
+    if _got_deleg(deleg):
+        res = sess.compound([op.putfh(fh), op.delegreturn(deleg.stateid)])
+        check(res)
+
+def _deleg_desc(deleg):
+    desc = "%s" % open_delegation_type4.get(deleg.delegation_type,
+                                            deleg.delegation_type)
+    if deleg.delegation_type == OPEN_DELEGATE_NONE_EXT:
+        desc += ", ond_why %s" % why_no_delegation4.get(deleg.ond_why,
+                                                        deleg.ond_why)
+    return desc
+
+def testClaimFHReadDeleg(t, env):
+    """OPEN(CLAIM_FH) with WANT_READ_DELEG on a file with no open state
+
+    FLAGS: open deleg all
+    CODE: DELEG28
+    """
+    name = env.testname(t)
+    sess1 = env.c1.new_client_session(b"%s_1" % name)
+    res = create_file(sess1, name, access=OPEN4_SHARE_ACCESS_BOTH |
+                      OPEN4_SHARE_ACCESS_WANT_NO_DELEG)
+    check(res)
+    fh = res.resarray[-1].object
+    res = close_file(sess1, fh, stateid=res.resarray[-2].stateid)
+    check(res)
+
+    res = _open_claim_fh_retry(sess1, fh, name, OPEN4_SHARE_ACCESS_READ |
+                               OPEN4_SHARE_ACCESS_WANT_READ_DELEG)
+    stateid = res.resarray[-1].stateid
+    deleg = res.resarray[-1].delegation
+    if deleg.delegation_type != OPEN_DELEGATE_READ:
+        _delegreturn(sess1, fh, deleg)
+        fail("Expected a read delegation, got %s" % _deleg_desc(deleg))
+
+    res = sess1.compound([op.putfh(fh), op.delegreturn(deleg.read.stateid)])
+    check(res)
+    res = close_file(sess1, fh, stateid=stateid)
+    check(res)
-- 
2.55.0


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

* [PATCH v1 2/8] pynfs: add test for OPEN(CLAIM_FH) delegation request on an open file
  2026-10-08 15:47 [PATCH v1 0/8] Test the WANT_*_DELEG OPEN flags Chuck Lever
  2026-10-08 15:47 ` [PATCH v1 1/8] pynfs: add test for OPEN(CLAIM_FH) with WANT_READ_DELEG Chuck Lever
@ 2026-10-08 15:47 ` Chuck Lever
  2026-10-08 15:47 ` [PATCH v1 3/8] pynfs: add test for a delegation request after a recall Chuck Lever
                   ` (5 subsequent siblings)
  7 siblings, 0 replies; 10+ messages in thread
From: Chuck Lever @ 2026-10-08 15:47 UTC (permalink / raw)
  To: Calum Mackay; +Cc: linux-nfs

A client that wants a delegation on a file it already has open can
repeat the OPEN with CLAIM_FH, the same open-owner, and a want bit.
The server treats that OPEN as an upgrade of the existing open, and
no test checks that a delegation can come back from one.

Add DELEG28. It keeps the file open with WANT_NO_DELEG, then sends
OPEN(CLAIM_FH) with WANT_READ_DELEG from the same open-owner. The
reply has to carry a read delegation and the original open stateid
with its seqid incremented by one.

Signed-off-by: Chuck Lever <cel@kernel.org>
---
 nfs4.1/server41tests/st_delegation.py | 41 +++++++++++++++++++++++++++
 1 file changed, 41 insertions(+)

diff --git a/nfs4.1/server41tests/st_delegation.py b/nfs4.1/server41tests/st_delegation.py
index c35ce8f5ec3b..56d227127a15 100644
--- a/nfs4.1/server41tests/st_delegation.py
+++ b/nfs4.1/server41tests/st_delegation.py
@@ -686,3 +686,44 @@ def testClaimFHReadDeleg(t, env):
     check(res)
     res = close_file(sess1, fh, stateid=stateid)
     check(res)
+
+def testClaimFHUpgradeReadDeleg(t, env):
+    """OPEN(CLAIM_FH) with WANT_READ_DELEG from an open-owner that
+       already has the file open
+
+    FLAGS: open deleg all
+    CODE: DELEG29
+    """
+    name = env.testname(t)
+    sess1 = env.c1.new_client_session(b"%s_1" % name)
+    res = create_file(sess1, name, access=OPEN4_SHARE_ACCESS_READ |
+                      OPEN4_SHARE_ACCESS_WANT_NO_DELEG)
+    check(res)
+    fh = res.resarray[-1].object
+    stateid = res.resarray[-2].stateid
+
+    # The second pass is the retry that _open_claim_fh_retry() makes.
+    # Each OPEN bumps the seqid, so count them here.
+    seqid = stateid.seqid
+    for attempt in range(2):
+        res = _open_claim_fh(sess1, fh, name, OPEN4_SHARE_ACCESS_READ |
+                             OPEN4_SHARE_ACCESS_WANT_READ_DELEG)
+        check(res)
+        seqid += 1
+        upgraded = res.resarray[-1].stateid
+        if upgraded.other != stateid.other:
+            fail("OPEN by the same open-owner returned a different stateid")
+        if upgraded.seqid != seqid:
+            fail("Expected open stateid seqid %i, got %i" %
+                 (seqid, upgraded.seqid))
+        deleg = res.resarray[-1].delegation
+        if _got_deleg(deleg):
+            break
+    if deleg.delegation_type != OPEN_DELEGATE_READ:
+        _delegreturn(sess1, fh, deleg)
+        fail("Expected a read delegation, got %s" % _deleg_desc(deleg))
+
+    res = sess1.compound([op.putfh(fh), op.delegreturn(deleg.read.stateid)])
+    check(res)
+    res = close_file(sess1, fh, stateid=upgraded)
+    check(res)
-- 
2.55.0


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

* [PATCH v1 3/8] pynfs: add test for a delegation request after a recall
  2026-10-08 15:47 [PATCH v1 0/8] Test the WANT_*_DELEG OPEN flags Chuck Lever
  2026-10-08 15:47 ` [PATCH v1 1/8] pynfs: add test for OPEN(CLAIM_FH) with WANT_READ_DELEG Chuck Lever
  2026-10-08 15:47 ` [PATCH v1 2/8] pynfs: add test for OPEN(CLAIM_FH) delegation request on an open file Chuck Lever
@ 2026-10-08 15:47 ` Chuck Lever
  2026-10-08 15:47 ` [PATCH v1 4/8] pynfs: add tests for SETATTR by a delegation holder Chuck Lever
                   ` (4 subsequent siblings)
  7 siblings, 0 replies; 10+ messages in thread
From: Chuck Lever @ 2026-10-08 15:47 UTC (permalink / raw)
  To: Calum Mackay; +Cc: linux-nfs

RFC 8881 Section 10.4.7 names re-establishing a recalled delegation
as a use for a delegation want. No test asks for a delegation again
once the conflicting open is gone, so a server that never grants one
on that file again, or that misreports why, goes unnoticed.

Add DELEG29. It has a second client recall a read delegation and
then close its open. The first client then sends OPEN(CLAIM_FH) with
WANT_READ_DELEG three times, at 0, 65, and 100 seconds, and the
failure message gives the outcome of each. The test accepts a
delegation or OPEN_DELEGATE_NONE_EXT with WND4_CONTENTION. A reply
that names another reason, or that is a bare OPEN_DELEGATE_NONE,
fails it. The delays separate a refusal that ends from one that does
not. NFSD, for one, blocks a recalled file for at least 30 seconds
and ages the block only when a delegation request arrives, so the
block can outlast the second request.

Signed-off-by: Chuck Lever <cel@kernel.org>
---
 nfs4.1/server41tests/st_delegation.py | 73 +++++++++++++++++++++++++++
 1 file changed, 73 insertions(+)

diff --git a/nfs4.1/server41tests/st_delegation.py b/nfs4.1/server41tests/st_delegation.py
index 56d227127a15..8addde6c4fbf 100644
--- a/nfs4.1/server41tests/st_delegation.py
+++ b/nfs4.1/server41tests/st_delegation.py
@@ -727,3 +727,76 @@ def testClaimFHUpgradeReadDeleg(t, env):
     check(res)
     res = close_file(sess1, fh, stateid=upgraded)
     check(res)
+
+def testClaimFHAfterRecall(t, env):
+    """OPEN(CLAIM_FH) with WANT_READ_DELEG after the delegation was
+       recalled and the conflicting open has been closed
+
+    The request is made three times: right after the conflicting open
+    is closed, 65 seconds later, and 100 seconds later. A server can
+    refuse a delegation on a recently recalled file for a while, and
+    it can age that refusal only when a request arrives. The last
+    request shows whether the refusal ever ends. Any reply may carry
+    a delegation or WND4_CONTENTION.
+
+    FLAGS: open deleg
+    CODE: DELEG30
+    """
+    recall = threading.Event()
+    def pre_hook(arg, env):
+        env.notify = recall.set
+    def post_hook(arg, env, res):
+        return res
+    name = env.testname(t)
+    sess1 = env.c1.new_client_session(b"%s_1" % name)
+    sess1.client.cb_pre_hook(OP_CB_RECALL, pre_hook)
+    sess1.client.cb_post_hook(OP_CB_RECALL, post_hook)
+    access = OPEN4_SHARE_ACCESS_READ | OPEN4_SHARE_ACCESS_WANT_READ_DELEG
+    fh, stateid, deleg = __create_file_with_deleg(sess1, name, access)
+
+    sess2 = env.c1.new_client_session(b"%s_2" % name)
+    open_op = op.open(0, OPEN4_SHARE_ACCESS_BOTH, OPEN4_SHARE_DENY_NONE,
+                      open_owner4(0, b"My Open Owner 2"),
+                      openflag4(OPEN4_NOCREATE), open_claim4(CLAIM_NULL, name))
+    slot = sess2.compound_async(env.home + [open_op])
+    completed = recall.wait(2)
+    env.sleep(.1)
+    res = sess1.compound([op.putfh(fh), op.delegreturn(deleg.read.stateid)])
+    check(res)
+    res = sess2.listen(slot)
+    if not completed:
+        fail("delegation break not received")
+    for i in range(5):
+        if res.status != NFS4ERR_DELAY:
+            break
+        env.sleep(1)
+        res = sess2.compound(env.home + [open_op])
+    check(res)
+    res = close_file(sess2, fh, stateid=res.resarray[-1].stateid)
+    check(res)
+
+    passed = True
+    results = []
+    elapsed = 0
+    for delay in (0, 65, 100):
+        # Sleep in short steps so that the lease does not expire
+        while elapsed < delay:
+            env.sleep(5)
+            elapsed += 5
+            check(sess1.compound([]))
+        res = _open_claim_fh(sess1, fh, name, access)
+        check(res)
+        stateid = res.resarray[-1].stateid
+        deleg = res.resarray[-1].delegation
+        _delegreturn(sess1, fh, deleg)
+        if (not _got_deleg(deleg) and
+            (deleg.delegation_type != OPEN_DELEGATE_NONE_EXT or
+             deleg.ond_why != WND4_CONTENTION)):
+            passed = False
+        results.append("after %i seconds: %s" % (delay, _deleg_desc(deleg)))
+    results = "; ".join(results)
+    res = close_file(sess1, fh, stateid=stateid)
+    check(res)
+    if not passed:
+        fail(results)
+    print(results)
-- 
2.55.0


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

* [PATCH v1 4/8] pynfs: add tests for SETATTR by a delegation holder
  2026-10-08 15:47 [PATCH v1 0/8] Test the WANT_*_DELEG OPEN flags Chuck Lever
                   ` (2 preceding siblings ...)
  2026-10-08 15:47 ` [PATCH v1 3/8] pynfs: add test for a delegation request after a recall Chuck Lever
@ 2026-10-08 15:47 ` Chuck Lever
  2026-10-08 15:47 ` [PATCH v1 5/8] pynfs: add test for WANT_OPEN_XOR_DELEGATION through CLAIM_FH Chuck Lever
                   ` (3 subsequent siblings)
  7 siblings, 0 replies; 10+ messages in thread
From: Chuck Lever @ 2026-10-08 15:47 UTC (permalink / raw)
  To: Calum Mackay; +Cc: linux-nfs

A client that holds a delegation and then changes the file's mode
does not conflict with itself, so the server has no reason to recall
that delegation. A server that recalls it anyway costs the client
its delegation on every chmod, and no test checks for that.

Add DELEG30 and DELEG31, which take a write and a read delegation
and send a mode-changing SETATTR from the same session. Each fails
if a CB_RECALL arrives within two seconds or if TEST_STATEID no
longer accepts the delegation stateid. A server that does not grant
the delegation type the test asks for gets UNSUPPORTED, not a
failure.

Signed-off-by: Chuck Lever <cel@kernel.org>
---
 nfs4.1/server41tests/st_delegation.py | 59 +++++++++++++++++++++++++++
 1 file changed, 59 insertions(+)

diff --git a/nfs4.1/server41tests/st_delegation.py b/nfs4.1/server41tests/st_delegation.py
index 8addde6c4fbf..198555ce9b9d 100644
--- a/nfs4.1/server41tests/st_delegation.py
+++ b/nfs4.1/server41tests/st_delegation.py
@@ -800,3 +800,62 @@ def testClaimFHAfterRecall(t, env):
     if not passed:
         fail(results)
     print(results)
+
+def _check_deleg_stateid(sess, stateid):
+    res = sess.compound([op.test_stateid([stateid])])
+    check(res)
+    status = res.resarray[0].tsr_status_codes[0]
+    if status != NFS4_OK:
+        fail("TEST_STATEID on the delegation stateid returned %s" %
+             nfsstat4[status])
+
+def _testHolderSetattr(t, env, access, deleg_type):
+    recall = threading.Event()
+    def pre_hook(arg, env):
+        env.notify = recall.set
+    def post_hook(arg, env, res):
+        return res
+    name = env.testname(t)
+    sess1 = env.c1.new_client_session(b"%s_1" % name)
+    sess1.client.cb_pre_hook(OP_CB_RECALL, pre_hook)
+    sess1.client.cb_post_hook(OP_CB_RECALL, post_hook)
+
+    fh, stateid, deleg = __create_file_with_deleg(sess1, name, access)
+    if deleg.delegation_type != deleg_type:
+        _delegreturn(sess1, fh, deleg)
+        t.fail_support("Wanted %s, got %s" %
+                       (open_delegation_type4[deleg_type], _deleg_desc(deleg)))
+
+    res = sess1.compound([op.putfh(fh),
+                          op.setattr(nfs4lib.state00, {FATTR4_MODE: 0o600})])
+    recalled = recall.wait(2)
+    if recalled or res.status != NFS4_OK:
+        _delegreturn(sess1, fh, deleg)
+    if recalled:
+        fail("SETATTR by the delegation holder recalled its delegation")
+    check(res)
+    _check_deleg_stateid(sess1, deleg.stateid)
+
+    _delegreturn(sess1, fh, deleg)
+    res = close_file(sess1, fh, stateid=stateid)
+    check(res)
+
+def testWriteDelegHolderSetattr(t, env):
+    """SETATTR by the holder of a write delegation must not recall it
+
+    FLAGS: writedelegations deleg all
+    CODE: DELEG31
+    """
+    _testHolderSetattr(t, env, OPEN4_SHARE_ACCESS_BOTH |
+                       OPEN4_SHARE_ACCESS_WANT_WRITE_DELEG,
+                       OPEN_DELEGATE_WRITE)
+
+def testReadDelegHolderSetattr(t, env):
+    """SETATTR by the holder of a read delegation must not recall it
+
+    FLAGS: deleg all
+    CODE: DELEG32
+    """
+    _testHolderSetattr(t, env, OPEN4_SHARE_ACCESS_READ |
+                       OPEN4_SHARE_ACCESS_WANT_READ_DELEG,
+                       OPEN_DELEGATE_READ)
-- 
2.55.0


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

* [PATCH v1 5/8] pynfs: add test for WANT_OPEN_XOR_DELEGATION through CLAIM_FH
  2026-10-08 15:47 [PATCH v1 0/8] Test the WANT_*_DELEG OPEN flags Chuck Lever
                   ` (3 preceding siblings ...)
  2026-10-08 15:47 ` [PATCH v1 4/8] pynfs: add tests for SETATTR by a delegation holder Chuck Lever
@ 2026-10-08 15:47 ` Chuck Lever
  2026-10-08 15:47 ` [PATCH v1 6/8] pynfs: add test for a CLAIM_FH delegation that outlives its open Chuck Lever
                   ` (2 subsequent siblings)
  7 siblings, 0 replies; 10+ messages in thread
From: Chuck Lever @ 2026-10-08 15:47 UTC (permalink / raw)
  To: Calum Mackay; +Cc: linux-nfs

An OPEN(CLAIM_FH) from the open-owner that already has the file open
bumps the seqid of that owner's open stateid, even when the client
only wants a delegation. RFC 9754 gives the client a way around
that: send the OPEN from a second open-owner with
OPEN4_SHARE_ACCESS_WANT_OPEN_XOR_DELEGATION, and the server returns
the delegation alone. No test exercises XOR.

Add DELEG32. It holds the file open under one open-owner and sends
the XOR request from another. The reply has to carry a read
delegation and OPEN4_RESULT_NO_OPEN_STATEID, and the first owner's
open stateid has to keep its seqid. The test reports UNSUPPORTED
when the open_arguments attribute does not advertise XOR.

Signed-off-by: Chuck Lever <cel@kernel.org>
---
 nfs4.1/server41tests/st_delegation.py | 59 +++++++++++++++++++++++++++
 1 file changed, 59 insertions(+)

diff --git a/nfs4.1/server41tests/st_delegation.py b/nfs4.1/server41tests/st_delegation.py
index 198555ce9b9d..a8b7ea54a772 100644
--- a/nfs4.1/server41tests/st_delegation.py
+++ b/nfs4.1/server41tests/st_delegation.py
@@ -859,3 +859,62 @@ def testReadDelegHolderSetattr(t, env):
     _testHolderSetattr(t, env, OPEN4_SHARE_ACCESS_READ |
                        OPEN4_SHARE_ACCESS_WANT_READ_DELEG,
                        OPEN_DELEGATE_READ)
+
+def testClaimFHXorDeleg(t, env):
+    """OPEN(CLAIM_FH) with WANT_OPEN_XOR_DELEGATION from a second
+       open-owner returns a delegation and no open stateid
+
+    FLAGS: open deleg all
+    CODE: DELEG33
+    """
+    name = env.testname(t)
+    sess1 = env.c1.new_client_session(b"%s_1" % name)
+    res = sess1.compound([op.putrootfh(),
+                          op.getattr(nfs4lib.list2bitmap([FATTR4_OPEN_ARGUMENTS]))])
+    check(res)
+    caps = res.resarray[-1].obj_attributes
+    xor = 1 << OPEN_ARGS_SHARE_ACCESS_WANT_OPEN_XOR_DELEGATION
+    if (FATTR4_OPEN_ARGUMENTS not in caps or
+        not caps[FATTR4_OPEN_ARGUMENTS].oa_share_access_want & xor):
+        t.fail_support("Server does not advertise WANT_OPEN_XOR_DELEGATION")
+
+    res = create_file(sess1, name, access=OPEN4_SHARE_ACCESS_READ |
+                      OPEN4_SHARE_ACCESS_WANT_NO_DELEG)
+    check(res)
+    fh = res.resarray[-1].object
+    stateid = res.resarray[-2].stateid
+
+    owner2 = b"%s_2" % name
+    access = (OPEN4_SHARE_ACCESS_READ | OPEN4_SHARE_ACCESS_WANT_READ_DELEG |
+              OPEN4_SHARE_ACCESS_WANT_OPEN_XOR_DELEGATION)
+    res = _open_claim_fh(sess1, fh, owner2, access)
+    check(res)
+    if not _got_deleg(res.resarray[-1].delegation):
+        # Close first, so that the retry is not an upgrade of this open
+        res = close_file(sess1, fh, stateid=res.resarray[-1].stateid)
+        check(res)
+        res = _open_claim_fh(sess1, fh, owner2, access)
+        check(res)
+    deleg = res.resarray[-1].delegation
+    if deleg.delegation_type != OPEN_DELEGATE_READ:
+        fail("Expected a read delegation, got %s" % _deleg_desc(deleg))
+    if not res.resarray[-1].rflags & OPEN4_RESULT_NO_OPEN_STATEID:
+        _delegreturn(sess1, fh, deleg)
+        fail("Got a delegation, but OPEN4_RESULT_NO_OPEN_STATEID is not set")
+
+    # An OPEN by the first open-owner bumps its seqid once. Any other
+    # seqid means the second open-owner's OPEN changed that stateid.
+    res = _open_claim_fh(sess1, fh, name, OPEN4_SHARE_ACCESS_READ |
+                         OPEN4_SHARE_ACCESS_WANT_NO_DELEG)
+    check(res)
+    upgraded = res.resarray[-1].stateid
+    if upgraded.other != stateid.other:
+        fail("OPEN by the same open-owner returned a different stateid")
+    if upgraded.seqid != stateid.seqid + 1:
+        fail("Expected open stateid seqid %i, got %i" %
+             (stateid.seqid + 1, upgraded.seqid))
+
+    res = sess1.compound([op.putfh(fh), op.delegreturn(deleg.read.stateid)])
+    check(res)
+    res = close_file(sess1, fh, stateid=upgraded)
+    check(res)
-- 
2.55.0


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

* [PATCH v1 6/8] pynfs: add test for a CLAIM_FH delegation that outlives its open
  2026-10-08 15:47 [PATCH v1 0/8] Test the WANT_*_DELEG OPEN flags Chuck Lever
                   ` (4 preceding siblings ...)
  2026-10-08 15:47 ` [PATCH v1 5/8] pynfs: add test for WANT_OPEN_XOR_DELEGATION through CLAIM_FH Chuck Lever
@ 2026-10-08 15:47 ` Chuck Lever
  2026-10-08 15:47 ` [PATCH v1 7/8] pynfs: add tests for delegation upgrade and downgrade wants Chuck Lever
  2026-10-08 15:47 ` [PATCH v1 8/8] pynfs: add test for OPEN with WANT_CANCEL Chuck Lever
  7 siblings, 0 replies; 10+ messages in thread
From: Chuck Lever @ 2026-10-08 15:47 UTC (permalink / raw)
  To: Calum Mackay; +Cc: linux-nfs

On a server without WANT_OPEN_XOR_DELEGATION, a client can still get
a delegation without disturbing its existing open stateid. It sends
OPEN(CLAIM_FH) from a second open-owner and closes that open once
the delegation arrives. DELEG26 shows a delegation outliving the
CLOSE of a CLAIM_NULL open by the only open-owner. No test covers a
second open-owner or CLAIM_FH.

Add DELEG33. It holds the file open under one open-owner, obtains a
read delegation with OPEN(CLAIM_FH) from another, closes the second
owner's open, and expects TEST_STATEID to accept the delegation
stateid.

Signed-off-by: Chuck Lever <cel@kernel.org>
---
 nfs4.1/server41tests/st_delegation.py | 31 +++++++++++++++++++++++++++
 1 file changed, 31 insertions(+)

diff --git a/nfs4.1/server41tests/st_delegation.py b/nfs4.1/server41tests/st_delegation.py
index a8b7ea54a772..3b6ca1d3b005 100644
--- a/nfs4.1/server41tests/st_delegation.py
+++ b/nfs4.1/server41tests/st_delegation.py
@@ -918,3 +918,34 @@ def testClaimFHXorDeleg(t, env):
     check(res)
     res = close_file(sess1, fh, stateid=upgraded)
     check(res)
+
+def testClaimFHDelegAfterClose(t, env):
+    """A delegation from OPEN(CLAIM_FH) by a second open-owner outlives
+       the CLOSE of that owner's open
+
+    FLAGS: open deleg all
+    CODE: DELEG34
+    """
+    name = env.testname(t)
+    sess1 = env.c1.new_client_session(b"%s_1" % name)
+    res = create_file(sess1, name, access=OPEN4_SHARE_ACCESS_READ |
+                      OPEN4_SHARE_ACCESS_WANT_NO_DELEG)
+    check(res)
+    fh = res.resarray[-1].object
+    stateid = res.resarray[-2].stateid
+
+    res = _open_claim_fh_retry(sess1, fh, b"%s_2" % name,
+                               OPEN4_SHARE_ACCESS_READ |
+                               OPEN4_SHARE_ACCESS_WANT_READ_DELEG)
+    deleg = res.resarray[-1].delegation
+    if deleg.delegation_type != OPEN_DELEGATE_READ:
+        _delegreturn(sess1, fh, deleg)
+        fail("Expected a read delegation, got %s" % _deleg_desc(deleg))
+    res = close_file(sess1, fh, stateid=res.resarray[-1].stateid)
+    check(res)
+    _check_deleg_stateid(sess1, deleg.read.stateid)
+
+    res = sess1.compound([op.putfh(fh), op.delegreturn(deleg.read.stateid)])
+    check(res)
+    res = close_file(sess1, fh, stateid=stateid)
+    check(res)
-- 
2.55.0


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

* [PATCH v1 7/8] pynfs: add tests for delegation upgrade and downgrade wants
  2026-10-08 15:47 [PATCH v1 0/8] Test the WANT_*_DELEG OPEN flags Chuck Lever
                   ` (5 preceding siblings ...)
  2026-10-08 15:47 ` [PATCH v1 6/8] pynfs: add test for a CLAIM_FH delegation that outlives its open Chuck Lever
@ 2026-10-08 15:47 ` Chuck Lever
  2026-10-08 15:47 ` [PATCH v1 8/8] pynfs: add test for OPEN with WANT_CANCEL Chuck Lever
  7 siblings, 0 replies; 10+ messages in thread
From: Chuck Lever @ 2026-10-08 15:47 UTC (permalink / raw)
  To: Calum Mackay; +Cc: linux-nfs

RFC 8881 Section 18.16.3 treats a want for the other delegation
type, from a client that already holds a delegation on the file, as
a request to upgrade or downgrade it. A server that supports neither
has to say so with WND4_NOT_SUPP_UPGRADE or WND4_NOT_SUPP_DOWNGRADE.
No test sends a want while a delegation is held, so a server that
gives another reason goes unnoticed.

Add DELEG34 and DELEG35. Each takes a delegation, then sends
OPEN(CLAIM_FH) from a second open-owner with a want for the other
type. The reply has to carry either a delegation of the wanted type
or the matching reason. DELEG35 reports UNSUPPORTED when the server
grants no write delegation to start from.

Signed-off-by: Chuck Lever <cel@kernel.org>
---
 nfs4.1/server41tests/st_delegation.py | 57 +++++++++++++++++++++++++++
 1 file changed, 57 insertions(+)

diff --git a/nfs4.1/server41tests/st_delegation.py b/nfs4.1/server41tests/st_delegation.py
index 3b6ca1d3b005..29d8341762d8 100644
--- a/nfs4.1/server41tests/st_delegation.py
+++ b/nfs4.1/server41tests/st_delegation.py
@@ -949,3 +949,60 @@ def testClaimFHDelegAfterClose(t, env):
     check(res)
     res = close_file(sess1, fh, stateid=stateid)
     check(res)
+
+def _testDelegXgrade(t, env, access, held_type, want_access, want_type, why):
+    name = env.testname(t)
+    sess1 = env.c1.new_client_session(b"%s_1" % name)
+    fh, stateid, deleg = __create_file_with_deleg(sess1, name, access)
+    if deleg.delegation_type != held_type:
+        _delegreturn(sess1, fh, deleg)
+        t.fail_support("Wanted %s, got %s" %
+                       (open_delegation_type4[held_type], _deleg_desc(deleg)))
+
+    res = _open_claim_fh(sess1, fh, b"%s_2" % name, want_access)
+    check(res)
+    stateid2 = res.resarray[-1].stateid
+    xgrade = res.resarray[-1].delegation
+    if xgrade.delegation_type == want_type:
+        deleg = xgrade
+
+    # Return the delegation first, so that a failure does not leave it
+    # behind to block the removal of the test file
+    _delegreturn(sess1, fh, deleg)
+    res = close_file(sess1, fh, stateid=stateid2)
+    check(res)
+    res = close_file(sess1, fh, stateid=stateid)
+    check(res)
+
+    if (xgrade.delegation_type != want_type and
+        (xgrade.delegation_type != OPEN_DELEGATE_NONE_EXT or
+         xgrade.ond_why != why)):
+        fail("Expected %s or %s, got %s" %
+             (open_delegation_type4[want_type], why_no_delegation4[why],
+              _deleg_desc(xgrade)))
+
+def testReadDelegUpgrade(t, env):
+    """WANT_WRITE_DELEG from the holder of a read delegation gets a
+       write delegation or WND4_NOT_SUPP_UPGRADE
+
+    FLAGS: open deleg all
+    CODE: DELEG35
+    """
+    _testDelegXgrade(t, env, OPEN4_SHARE_ACCESS_READ |
+                     OPEN4_SHARE_ACCESS_WANT_READ_DELEG, OPEN_DELEGATE_READ,
+                     OPEN4_SHARE_ACCESS_BOTH |
+                     OPEN4_SHARE_ACCESS_WANT_WRITE_DELEG, OPEN_DELEGATE_WRITE,
+                     WND4_NOT_SUPP_UPGRADE)
+
+def testWriteDelegDowngrade(t, env):
+    """WANT_READ_DELEG from the holder of a write delegation gets a
+       read delegation or WND4_NOT_SUPP_DOWNGRADE
+
+    FLAGS: writedelegations deleg all
+    CODE: DELEG36
+    """
+    _testDelegXgrade(t, env, OPEN4_SHARE_ACCESS_BOTH |
+                     OPEN4_SHARE_ACCESS_WANT_WRITE_DELEG, OPEN_DELEGATE_WRITE,
+                     OPEN4_SHARE_ACCESS_READ |
+                     OPEN4_SHARE_ACCESS_WANT_READ_DELEG, OPEN_DELEGATE_READ,
+                     WND4_NOT_SUPP_DOWNGRADE)
-- 
2.55.0


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

* [PATCH v1 8/8] pynfs: add test for OPEN with WANT_CANCEL
  2026-10-08 15:47 [PATCH v1 0/8] Test the WANT_*_DELEG OPEN flags Chuck Lever
                   ` (6 preceding siblings ...)
  2026-10-08 15:47 ` [PATCH v1 7/8] pynfs: add tests for delegation upgrade and downgrade wants Chuck Lever
@ 2026-10-08 15:47 ` Chuck Lever
  7 siblings, 0 replies; 10+ messages in thread
From: Chuck Lever @ 2026-10-08 15:47 UTC (permalink / raw)
  To: Calum Mackay; +Cc: linux-nfs

RFC 8881 Section 18.16.3 defines WND4_CANCELLED as the reason a
server gives when an OPEN sets OPEN4_SHARE_ACCESS_WANT_CANCEL. No
test sends that want, so a server that answers with another reason
goes unnoticed. NFSD reports WND4_NOT_WANTED.

Add DELEG36. It creates a file with WANT_CANCEL and fails unless the
reply carries OPEN_DELEGATE_NONE_EXT with ond_why set to
WND4_CANCELLED.

Signed-off-by: Chuck Lever <cel@kernel.org>
---
 nfs4.1/server41tests/st_delegation.py | 24 ++++++++++++++++++++++++
 1 file changed, 24 insertions(+)

diff --git a/nfs4.1/server41tests/st_delegation.py b/nfs4.1/server41tests/st_delegation.py
index 29d8341762d8..5af83c8744c4 100644
--- a/nfs4.1/server41tests/st_delegation.py
+++ b/nfs4.1/server41tests/st_delegation.py
@@ -1006,3 +1006,27 @@ def testWriteDelegDowngrade(t, env):
                      OPEN4_SHARE_ACCESS_READ |
                      OPEN4_SHARE_ACCESS_WANT_READ_DELEG, OPEN_DELEGATE_READ,
                      WND4_NOT_SUPP_DOWNGRADE)
+
+def testWantCancel(t, env):
+    """OPEN with WANT_CANCEL gets no delegation and WND4_CANCELLED
+
+    FLAGS: open deleg all
+    CODE: DELEG37
+    """
+    sess1 = env.c1.new_client_session(b"%s_1" % env.testname(t))
+    res = create_file(sess1, env.testname(t),
+                      access=OPEN4_SHARE_ACCESS_READ |
+                      OPEN4_SHARE_ACCESS_WANT_CANCEL)
+    check(res)
+    fh = res.resarray[-1].object
+    deleg = res.resarray[-2].delegation
+    _delegreturn(sess1, fh, deleg)
+    res = close_file(sess1, fh, stateid=res.resarray[-2].stateid)
+    check(res)
+    if deleg.delegation_type == OPEN_DELEGATE_NONE:
+        fail("Got OPEN_DELEGATE_NONE, expected OPEN_DELEGATE_NONE_EXT")
+    if deleg.delegation_type != OPEN_DELEGATE_NONE_EXT:
+        fail("Got a delegation (type %d) despite WANT_CANCEL" %
+             deleg.delegation_type)
+    if deleg.ond_why != WND4_CANCELLED:
+        fail("Got ond_why %d, expected WND4_CANCELLED" % deleg.ond_why)
-- 
2.55.0


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

* Substituting WANT_DELEGATION with OPEN(CLAIM_FH)? Re: [PATCH v1 1/8] pynfs: add test for OPEN(CLAIM_FH) with WANT_READ_DELEG
  2026-10-08 15:47 ` [PATCH v1 1/8] pynfs: add test for OPEN(CLAIM_FH) with WANT_READ_DELEG Chuck Lever
@ 2026-10-09  7:58   ` Lionel Cons
  0 siblings, 0 replies; 10+ messages in thread
From: Lionel Cons @ 2026-10-09  7:58 UTC (permalink / raw)
  To: linux-nfs

On Thu, 8 Oct 2026 at 17:52, Chuck Lever <cel@kernel.org> wrote:
>
> The delegation tests request a delegation only through CLAIM_NULL,
> and OPEN7 pairs CLAIM_FH with WANT_NO_DELEG. No test covers a client
> that already holds a filehandle and asks for a delegation with
> OPEN(CLAIM_FH). On a server that does not implement WANT_DELEGATION,
> that OPEN is the only way to ask.

FYI substituting WANT_DELEGATION with OPEN(CLAIM_FH) turned out to be
a deadend, to due various factors, including:
- many nfsd implementations do not implement OPEN(CLAIM_FH) with
delegation "upgrade", and it depends on either delicate/fragile open
stateid updates and barriers to all worker threads in the client and
in-flight RPC requests, OR
- handling of NFS4ERR_OLD_STATEID handling in the client, which turns
out to be impossible because many nfsd implementations return random
NFS4ERR values instead of NFS4ERR_OLD_STATEID, or just  shutting down
the session altogether (NetApp).

Lionel

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

end of thread, other threads:[~2026-10-09  7:59 UTC | newest]

Thread overview: 10+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-10-08 15:47 [PATCH v1 0/8] Test the WANT_*_DELEG OPEN flags Chuck Lever
2026-10-08 15:47 ` [PATCH v1 1/8] pynfs: add test for OPEN(CLAIM_FH) with WANT_READ_DELEG Chuck Lever
2026-10-09  7:58   ` Substituting WANT_DELEGATION with OPEN(CLAIM_FH)? " Lionel Cons
2026-10-08 15:47 ` [PATCH v1 2/8] pynfs: add test for OPEN(CLAIM_FH) delegation request on an open file Chuck Lever
2026-10-08 15:47 ` [PATCH v1 3/8] pynfs: add test for a delegation request after a recall Chuck Lever
2026-10-08 15:47 ` [PATCH v1 4/8] pynfs: add tests for SETATTR by a delegation holder Chuck Lever
2026-10-08 15:47 ` [PATCH v1 5/8] pynfs: add test for WANT_OPEN_XOR_DELEGATION through CLAIM_FH Chuck Lever
2026-10-08 15:47 ` [PATCH v1 6/8] pynfs: add test for a CLAIM_FH delegation that outlives its open Chuck Lever
2026-10-08 15:47 ` [PATCH v1 7/8] pynfs: add tests for delegation upgrade and downgrade wants Chuck Lever
2026-10-08 15:47 ` [PATCH v1 8/8] pynfs: add test for OPEN with WANT_CANCEL Chuck Lever

This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.