* [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* 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
* [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