NFSv4/pnfs: Confirm a deviceID delete via GETDEVICEINFO

RFC 8881 Section 18.40.4: if TEST_STATEID says at least one layout
referring to the deleted deviceID is still valid, the delete cannot
be trusted -- verify it with GETDEVICEINFO.  The device really being
gone while the server also considers a referring layout valid means
the server is faulty; recover by re-establishing the client ID and
drop the cached device.  Any other answer -- including the device
existing, i.e. an erroneous DELETE -- keeps the cached device and
the layout intact.

Re-establishing the client ID is nfs4_reset_all_state(), which sets
NFS4CLNT_PURGE_STATE so the state manager runs nfs4_purge_lease():
a fresh EXCHANGE_ID, then state reclaim with no grace period.  The
grace-less reclaim is the point -- the server has not rebooted, so
there is nothing to reclaim under CLAIM_PREVIOUS, and the new client
ID orphans the state held under the old one.  The obvious-looking
nfs4_schedule_lease_recovery() is not the right call here: it sets
NFS4CLNT_CHECK_LEASE, which the state manager turns into a lease
renewal, and on a healthy session -- which this one is, the server
having just answered TEST_STATEID and GETDEVICEINFO on it -- that
renewal succeeds and no EXCHANGE_ID is ever sent.

This is the only path on which a device notification can escalate to
a full client-ID reset, and every open, lock and delegation on the
client is reclaimed as a result.  From userspace that is
indistinguishable from a spontaneous lease expiry, so the escalation
is announced with a rate-limited warning naming the server.

The raw-status probe calls nfs4_proc_getdeviceinfo() directly
because nfs4_get_device_info() swallows the RPC status and cannot
distinguish NFS4ERR_NOENT from a transient failure.  A one-page
reply buffer is enough: a device too large for it fails with
something other than -ENOENT, which still proves existence.

Still nothing enqueues suspects; no behavior change.

Assisted-by: Claude:claude-fable-5
Signed-off-by: Benjamin Coddington <bcodding@hammerspace.com>
Signed-off-by: Anna Schumaker <anna.schumaker@hammerspace.com>
This commit is contained in:
Benjamin Coddington 2026-09-15 08:22:21 -04:00 committed by Anna Schumaker
parent 59b065e645
commit 14286f49f0
3 changed files with 54 additions and 8 deletions

View File

@ -511,6 +511,7 @@ extern void nfs_inode_find_state_and_recover(struct inode *inode,
const nfs4_stateid *stateid);
extern int nfs4_state_mark_reclaim_nograce(struct nfs_client *, struct nfs4_state *);
extern void nfs4_schedule_lease_recovery(struct nfs_client *);
extern void nfs4_reset_all_state(struct nfs_client *);
extern int nfs4_wait_clnt_recover(struct nfs_client *clp);
extern int nfs4_client_recover_expired_lease(struct nfs_client *clp);
extern void nfs4_schedule_state_manager(struct nfs_client *);

View File

@ -10506,19 +10506,50 @@ static int nfs41_free_stateid(struct nfs_server *server,
return ret;
}
/*
* GETDEVICEINFO surfacing the raw status; nfs4_get_device_info()
* swallows it. A device too large for one page fails with something
* other than -ENOENT, which still proves existence.
*/
static int nfs4_deviceid_validate(struct nfs_server *server,
const struct pnfs_layoutdriver_type *ld,
const struct nfs4_deviceid *id, const struct cred *cred)
{
struct pnfs_device pdev;
struct page *page;
int status;
page = alloc_page(GFP_KERNEL);
if (!page)
return -ENOMEM;
memset(&pdev, 0, sizeof(pdev));
memcpy(&pdev.dev_id, id, sizeof(pdev.dev_id));
pdev.layout_type = ld->id;
pdev.pages = &page;
pdev.pglen = PAGE_SIZE;
pdev.maxcount = PAGE_SIZE - nfs41_maxgetdevinfo_overhead;
status = nfs4_proc_getdeviceinfo(server, &pdev, cred);
__free_page(page);
return status;
}
/*
* A DELETE for a deviceID we still hold layouts on implies the server
* revoked them: run the RFC 8881 Section 18.40.4 recovery.
* revoked them: run the RFC 8881 Section 18.40.4 recovery. A layout the
* server still calls valid leaves the revocations unable to confirm the
* delete, so verify it with GETDEVICEINFO.
*/
static void nfs4_deviceid_delete_recover(struct nfs_client *clp,
const struct pnfs_layoutdriver_type *ld,
const struct nfs4_deviceid *id)
{
LIST_HEAD(layouts);
struct nfs4_deviceid_ref *ref;
struct nfs4_deviceid_ref *ref, *confirm = NULL;
bool revoked = false;
bool referenced = false;
bool inconclusive = false;
int status;
if (pnfs_layout_collect_deviceid_refs(clp, ld, id, &layouts)) {
/* Only a partial list -- an allocation failed, or an inode is
@ -10539,14 +10570,14 @@ static void nfs4_deviceid_delete_recover(struct nfs_client *clp,
struct inode *inode = ref->inode;
bool invalidated = false;
LIST_HEAD(head);
int status;
status = nfs41_test_stateid(NFS_SERVER(inode), &ref->stateid,
ref->cred);
switch (status) {
case NFS_OK:
case -NFS4ERR_OLD_STATEID:
referenced = true;
if (!confirm)
confirm = ref;
break;
case -NFS4ERR_ADMIN_REVOKED:
case -NFS4ERR_DELEG_REVOKED:
@ -10572,10 +10603,24 @@ static void nfs4_deviceid_delete_recover(struct nfs_client *clp,
break;
}
}
pnfs_layout_put_deviceid_refs(&layouts);
if (revoked && !referenced && !inconclusive)
if (confirm) {
status = nfs4_deviceid_validate(NFS_SERVER(confirm->inode),
ld, id, confirm->cred);
if (status == -ENOENT) {
/* Section 18.40.4 prescribes EXCHANGE_ID here;
* nfs4_schedule_lease_recovery() would only renew
* the existing lease.
*/
pr_warn_ratelimited("NFS: server %s deleted a deviceID referred to by a layout it still considers valid; re-establishing the client ID\n",
clp->cl_hostname);
nfs4_reset_all_state(clp);
nfs4_delete_deviceid(ld, clp, id);
}
} else if (revoked && !inconclusive) {
nfs4_delete_deviceid(ld, clp, id);
}
pnfs_layout_put_deviceid_refs(&layouts);
}
void nfs4_deviceid_delete_recover_run(struct nfs_client *clp)

View File

@ -2354,7 +2354,7 @@ void nfs41_notify_server(struct nfs_client *clp)
nfs4_schedule_state_manager(clp);
}
static void nfs4_reset_all_state(struct nfs_client *clp)
void nfs4_reset_all_state(struct nfs_client *clp)
{
if (test_and_set_bit(NFS4CLNT_LEASE_EXPIRED, &clp->cl_state) == 0) {
set_bit(NFS4CLNT_PURGE_STATE, &clp->cl_state);