root/fs/nfs/nfs40client.c
/* SPDX-License-Identifier: GPL-2.0 */
#include <linux/nfs_fs.h>
#include "nfs4_fs.h"
#include "nfs4session.h"
#include "callback.h"
#include "delegation.h"
#include "internal.h"
#include "netns.h"
#include "nfs40.h"

#define NFSDBG_FACILITY         NFSDBG_CLIENT

/*
 * SETCLIENTID just did a callback update with the callback ident in
 * "drop," but server trunking discovery claims "drop" and "keep" are
 * actually the same server.  Swap the callback IDs so that "keep"
 * will continue to use the callback ident the server now knows about,
 * and so that "keep"'s original callback ident is destroyed when
 * "drop" is freed.
 */
static void nfs4_swap_callback_idents(struct nfs_client *keep,
                                      struct nfs_client *drop)
{
        struct nfs_net *nn = net_generic(keep->cl_net, nfs_net_id);
        unsigned int save = keep->cl_cb_ident;

        if (keep->cl_cb_ident == drop->cl_cb_ident)
                return;

        dprintk("%s: keeping callback ident %u and dropping ident %u\n",
                __func__, keep->cl_cb_ident, drop->cl_cb_ident);

        spin_lock(&nn->nfs_client_lock);

        idr_replace(&nn->cb_ident_idr, keep, drop->cl_cb_ident);
        keep->cl_cb_ident = drop->cl_cb_ident;

        idr_replace(&nn->cb_ident_idr, drop, save);
        drop->cl_cb_ident = save;

        spin_unlock(&nn->nfs_client_lock);
}

static bool nfs4_same_verifier(nfs4_verifier *v1, nfs4_verifier *v2)
{
        return memcmp(v1->data, v2->data, sizeof(v1->data)) == 0;
}

void nfs40_shutdown_client(struct nfs_client *clp)
{
        if (clp->cl_slot_tbl) {
                nfs4_shutdown_slot_table(clp->cl_slot_tbl);
                kfree(clp->cl_slot_tbl);
        }
}

/**
 * nfs40_init_client - nfs_client initialization tasks for NFSv4.0
 * @clp: nfs_client to initialize
 *
 * Returns zero on success, or a negative errno if some error occurred.
 */
int nfs40_init_client(struct nfs_client *clp)
{
        struct nfs4_slot_table *tbl;
        int ret;

        tbl = kzalloc_obj(*tbl, GFP_NOFS);
        if (tbl == NULL)
                return -ENOMEM;

        ret = nfs4_setup_slot_table(tbl, NFS4_MAX_SLOT_TABLE,
                                        "NFSv4.0 transport Slot table");
        if (ret) {
                nfs4_shutdown_slot_table(tbl);
                kfree(tbl);
                return ret;
        }

        clp->cl_slot_tbl = tbl;
        return 0;
}

/*
 * nfs40_handle_cb_pathdown - return all delegations after NFS4ERR_CB_PATH_DOWN
 * @clp: client to process
 *
 * Set the NFS4CLNT_LEASE_EXPIRED state in order to force a
 * resend of the SETCLIENTID and hence re-establish the
 * callback channel. Then return all existing delegations.
 */
void nfs40_handle_cb_pathdown(struct nfs_client *clp)
{
        set_bit(NFS4CLNT_LEASE_EXPIRED, &clp->cl_state);
        nfs_expire_all_delegations(clp);
        dprintk("%s: handling CB_PATHDOWN recovery for server %s\n", __func__,
                        clp->cl_hostname);
}

void nfs4_schedule_path_down_recovery(struct nfs_client *clp)
{
        nfs40_handle_cb_pathdown(clp);
        nfs4_schedule_state_manager(clp);
}

/**
 * nfs40_walk_client_list - Find server that recognizes a client ID
 *
 * @new: nfs_client with client ID to test
 * @result: OUT: found nfs_client, or new
 * @cred: credential to use for trunking test
 *
 * Returns zero, a negative errno, or a negative NFS4ERR status.
 * If zero is returned, an nfs_client pointer is planted in "result."
 *
 * NB: nfs40_walk_client_list() relies on the new nfs_client being
 *     the last nfs_client on the list.
 */
static int nfs40_walk_client_list(struct nfs_client *new,
                                  struct nfs_client **result,
                                  const struct cred *cred)
{
        struct nfs_net *nn = net_generic(new->cl_net, nfs_net_id);
        struct nfs_client *pos, *prev = NULL;
        struct nfs4_setclientid_res clid = {
                .clientid       = new->cl_clientid,
                .confirm        = new->cl_confirm,
        };
        int status = -NFS4ERR_STALE_CLIENTID;

        spin_lock(&nn->nfs_client_lock);
        list_for_each_entry(pos, &nn->nfs_client_list, cl_share_link) {

                if (pos == new)
                        goto found;

                status = nfs4_match_client(pos, new, &prev, nn);
                if (status < 0)
                        goto out_unlock;
                if (status != 0)
                        continue;
                /*
                 * We just sent a new SETCLIENTID, which should have
                 * caused the server to return a new cl_confirm.  So if
                 * cl_confirm is the same, then this is a different
                 * server that just returned the same cl_confirm by
                 * coincidence:
                 */
                if ((new != pos) && nfs4_same_verifier(&pos->cl_confirm,
                                                       &new->cl_confirm))
                        continue;
                /*
                 * But if the cl_confirm's are different, then the only
                 * way that a SETCLIENTID_CONFIRM to pos can succeed is
                 * if new and pos point to the same server:
                 */
found:
                refcount_inc(&pos->cl_count);
                spin_unlock(&nn->nfs_client_lock);

                nfs_put_client(prev);
                prev = pos;

                status = nfs4_proc_setclientid_confirm(pos, &clid, cred);
                switch (status) {
                case -NFS4ERR_STALE_CLIENTID:
                        break;
                case 0:
                        nfs4_swap_callback_idents(pos, new);
                        pos->cl_confirm = new->cl_confirm;
                        nfs_mark_client_ready(pos, NFS_CS_READY);

                        prev = NULL;
                        *result = pos;
                        goto out;
                case -ERESTARTSYS:
                case -ETIMEDOUT:
                        /* The callback path may have been inadvertently
                         * changed. Schedule recovery!
                         */
                        nfs4_schedule_path_down_recovery(pos);
                        goto out;
                default:
                        goto out;
                }

                spin_lock(&nn->nfs_client_lock);
        }
out_unlock:
        spin_unlock(&nn->nfs_client_lock);

        /* No match found. The server lost our clientid */
out:
        nfs_put_client(prev);
        return status;
}

/**
 * nfs40_discover_server_trunking - Detect server IP address trunking (mv0)
 *
 * @clp: nfs_client under test
 * @result: OUT: found nfs_client, or clp
 * @cred: credential to use for trunking test
 *
 * Returns zero, a negative errno, or a negative NFS4ERR status.
 * If zero is returned, an nfs_client pointer is planted in
 * "result".
 *
 * Note: The returned client may not yet be marked ready.
 */
int nfs40_discover_server_trunking(struct nfs_client *clp,
                                   struct nfs_client **result,
                                   const struct cred *cred)
{
        struct nfs4_setclientid_res clid = {
                .clientid = clp->cl_clientid,
                .confirm = clp->cl_confirm,
        };
        struct nfs_net *nn = net_generic(clp->cl_net, nfs_net_id);
        unsigned short port;
        int status;

        port = nn->nfs_callback_tcpport;
        if (clp->cl_addr.ss_family == AF_INET6)
                port = nn->nfs_callback_tcpport6;

        status = nfs4_proc_setclientid(clp, NFS4_CALLBACK, port, cred, &clid);
        if (status != 0)
                goto out;
        clp->cl_clientid = clid.clientid;
        clp->cl_confirm = clid.confirm;

        status = nfs40_walk_client_list(clp, result, cred);
        if (status == 0) {
                /* Sustain the lease, even if it's empty.  If the clientid4
                 * goes stale it's of no use for trunking discovery. */
                nfs4_schedule_state_renewal(*result);

                /* If the client state need to recover, do it. */
                if (clp->cl_state)
                        nfs4_schedule_state_manager(clp);
        }
out:
        return status;
}