diff mbox series

[v1,08/14] cifs: Add DFS cache routines

Message ID 20181115142103.24617-9-aaptel@suse.com (mailing list archive)
State New, archived
Headers show
Series DFS failover | expand

Commit Message

Aurélien Aptel Nov. 15, 2018, 2:20 p.m. UTC
From: Paulo Alcantara <paulo@paulo.ac>

* Add new dfs_cache.[ch] files

* Add new /proc/fs/cifs/dfscache file
  - dump current cache when read
  - clear current cache when writing "0" to it

* Add delayed_work to periodically refresh cache entries

The new interface will be used for caching DFS referrals, as well as
supporting client target failover.

The DFS cache is a hashtable that maps UNC paths to cache entries.

A cache entry contains:
- the UNC path it is mapped on
- how much the the UNC path the entry consumes
- flags
- a Time-To-Live after which the entry expires
- a list of possible targets (linked lists of UNC paths)
- a "hint target" pointing the last known working target or the first
  target if none were tried. This hint lets cifs.ko remember and try
  working targets first.

* Looking for an entry in the cache is done with dfs_cache_find()
  - if no valid entries are found, a DFS query is made, stored in the
    cache and returned
  - the full target list can be copied and returned to avoid race
    conditions and looped on with the help with the
    dfs_cache_tgt_iterator

* Updating the target hint to the next target is done with
  dfs_cache_update_tgthint()

These functions have a dfs_cache_noreq_XXX() version that doesn't
fetches referrals if no entries are found. These versions don't
require the tcp/ses/tcon/cifs_sb parameters as a result.

Expired entries cannot be used and since they have a pretty short TTL
[1] in order for them to be useful for failover the DFS cache adds a
delayed work called periodically to keep them fresh.

Since we might not have available connections to issue the referral
request when refreshing we need to store volume_info structs with
credentials and other needed info to be able to connect to the right
server.

1: Windows defaults: 5mn for domain-based referrals, 30mn for regular
links

Signed-off-by: Paulo Alcantara <palcantara@suse.de>
Signed-off-by: Aurelien Aptel <aaptel@suse.com>
---
 fs/cifs/Makefile     |    2 +-
 fs/cifs/cifs_debug.c |   12 +
 fs/cifs/cifsglob.h   |    5 +
 fs/cifs/cifsproto.h  |    3 +
 fs/cifs/connect.c    |    2 +-
 fs/cifs/dfs_cache.c  | 1379 ++++++++++++++++++++++++++++++++++++++++++++++++++
 fs/cifs/dfs_cache.h  |   97 ++++
 7 files changed, 1498 insertions(+), 2 deletions(-)
 create mode 100644 fs/cifs/dfs_cache.c
 create mode 100644 fs/cifs/dfs_cache.h

Comments

Steve French Dec. 24, 2018, 5:39 a.m. UTC | #1
merged into cifs-2.6.git for-next with minor changes. Let me know if
objections to the fix of the return code (ENOSYS is for syscalls only)

$ diff ~/save-patch1/0019-cifs-Add-DFS-cache-routines.patch
0019-cifs-Add-DFS-cache-routines.patch
834c834
< +            ce = ERR_PTR(-ENOSYS);
---
> +            ce = ERR_PTR(-EOPNOTSUPP);
1481c1481
< +        rc = -ENOSYS;
---
> +        rc = -EOPNOTSUPP;




On Thu, Nov 15, 2018 at 8:22 AM Aurelien Aptel <aaptel@suse.com> wrote:
>
> From: Paulo Alcantara <paulo@paulo.ac>
>
> * Add new dfs_cache.[ch] files
>
> * Add new /proc/fs/cifs/dfscache file
>   - dump current cache when read
>   - clear current cache when writing "0" to it
>
> * Add delayed_work to periodically refresh cache entries
>
> The new interface will be used for caching DFS referrals, as well as
> supporting client target failover.
>
> The DFS cache is a hashtable that maps UNC paths to cache entries.
>
> A cache entry contains:
> - the UNC path it is mapped on
> - how much the the UNC path the entry consumes
> - flags
> - a Time-To-Live after which the entry expires
> - a list of possible targets (linked lists of UNC paths)
> - a "hint target" pointing the last known working target or the first
>   target if none were tried. This hint lets cifs.ko remember and try
>   working targets first.
>
> * Looking for an entry in the cache is done with dfs_cache_find()
>   - if no valid entries are found, a DFS query is made, stored in the
>     cache and returned
>   - the full target list can be copied and returned to avoid race
>     conditions and looped on with the help with the
>     dfs_cache_tgt_iterator
>
> * Updating the target hint to the next target is done with
>   dfs_cache_update_tgthint()
>
> These functions have a dfs_cache_noreq_XXX() version that doesn't
> fetches referrals if no entries are found. These versions don't
> require the tcp/ses/tcon/cifs_sb parameters as a result.
>
> Expired entries cannot be used and since they have a pretty short TTL
> [1] in order for them to be useful for failover the DFS cache adds a
> delayed work called periodically to keep them fresh.
>
> Since we might not have available connections to issue the referral
> request when refreshing we need to store volume_info structs with
> credentials and other needed info to be able to connect to the right
> server.
>
> 1: Windows defaults: 5mn for domain-based referrals, 30mn for regular
> links
>
> Signed-off-by: Paulo Alcantara <palcantara@suse.de>
> Signed-off-by: Aurelien Aptel <aaptel@suse.com>
> ---
>  fs/cifs/Makefile     |    2 +-
>  fs/cifs/cifs_debug.c |   12 +
>  fs/cifs/cifsglob.h   |    5 +
>  fs/cifs/cifsproto.h  |    3 +
>  fs/cifs/connect.c    |    2 +-
>  fs/cifs/dfs_cache.c  | 1379 ++++++++++++++++++++++++++++++++++++++++++++++++++
>  fs/cifs/dfs_cache.h  |   97 ++++
>  7 files changed, 1498 insertions(+), 2 deletions(-)
>  create mode 100644 fs/cifs/dfs_cache.c
>  create mode 100644 fs/cifs/dfs_cache.h
>
> diff --git a/fs/cifs/Makefile b/fs/cifs/Makefile
> index 85817991ee68..51af69a1a328 100644
> --- a/fs/cifs/Makefile
> +++ b/fs/cifs/Makefile
> @@ -17,7 +17,7 @@ cifs-$(CONFIG_CIFS_ACL) += cifsacl.o
>
>  cifs-$(CONFIG_CIFS_UPCALL) += cifs_spnego.o
>
> -cifs-$(CONFIG_CIFS_DFS_UPCALL) += dns_resolve.o cifs_dfs_ref.o
> +cifs-$(CONFIG_CIFS_DFS_UPCALL) += dns_resolve.o cifs_dfs_ref.o dfs_cache.o
>
>  cifs-$(CONFIG_CIFS_FSCACHE) += fscache.o cache.o
>
> diff --git a/fs/cifs/cifs_debug.c b/fs/cifs/cifs_debug.c
> index ba178b09de0b..593fb422d0f3 100644
> --- a/fs/cifs/cifs_debug.c
> +++ b/fs/cifs/cifs_debug.c
> @@ -30,6 +30,9 @@
>  #include "cifsproto.h"
>  #include "cifs_debug.h"
>  #include "cifsfs.h"
> +#ifdef CONFIG_CIFS_DFS_UPCALL
> +#include "dfs_cache.h"
> +#endif
>  #ifdef CONFIG_CIFS_SMB_DIRECT
>  #include "smbdirect.h"
>  #endif
> @@ -629,6 +632,11 @@ cifs_proc_init(void)
>                     &cifs_security_flags_proc_fops);
>         proc_create("LookupCacheEnabled", 0644, proc_fs_cifs,
>                     &cifs_lookup_cache_proc_fops);
> +
> +#ifdef CONFIG_CIFS_DFS_UPCALL
> +       proc_create("dfscache", 0644, proc_fs_cifs, &dfscache_proc_fops);
> +#endif
> +
>  #ifdef CONFIG_CIFS_SMB_DIRECT
>         proc_create("rdma_readwrite_threshold", 0644, proc_fs_cifs,
>                 &cifs_rdma_readwrite_threshold_proc_fops);
> @@ -663,6 +671,10 @@ cifs_proc_clean(void)
>         remove_proc_entry("SecurityFlags", proc_fs_cifs);
>         remove_proc_entry("LinuxExtensionsEnabled", proc_fs_cifs);
>         remove_proc_entry("LookupCacheEnabled", proc_fs_cifs);
> +
> +#ifdef CONFIG_CIFS_DFS_UPCALL
> +       remove_proc_entry("dfscache", proc_fs_cifs);
> +#endif
>  #ifdef CONFIG_CIFS_SMB_DIRECT
>         remove_proc_entry("rdma_readwrite_threshold", proc_fs_cifs);
>         remove_proc_entry("smbd_max_frmr_depth", proc_fs_cifs);
> diff --git a/fs/cifs/cifsglob.h b/fs/cifs/cifsglob.h
> index 9534f46e6ad2..bd183ec17066 100644
> --- a/fs/cifs/cifsglob.h
> +++ b/fs/cifs/cifsglob.h
> @@ -1014,6 +1014,11 @@ struct cifs_tcon {
>         struct list_head pending_opens; /* list of incomplete opens */
>         struct cached_fid crfid; /* Cached root fid */
>         /* BB add field for back pointer to sb struct(s)? */
> +#ifdef CONFIG_CIFS_DFS_UPCALL
> +       char *dfs_path;
> +       int remap:2;
> +       struct list_head ulist; /* cache update list */
> +#endif
>  };
>
>  /*
> diff --git a/fs/cifs/cifsproto.h b/fs/cifs/cifsproto.h
> index f4dd2a3795dd..efa5e36b3762 100644
> --- a/fs/cifs/cifsproto.h
> +++ b/fs/cifs/cifsproto.h
> @@ -527,6 +527,9 @@ extern int SMBencrypt(unsigned char *passwd, const unsigned char *c8,
>  extern void
>  cifs_cleanup_volume_info_contents(struct smb_vol *volume_info);
>
> +extern struct TCP_Server_Info *
> +cifs_find_tcp_session(struct smb_vol *vol);
> +
>  void cifs_readdata_release(struct kref *refcount);
>  int cifs_async_readv(struct cifs_readdata *rdata);
>  int cifs_readv_receive(struct TCP_Server_Info *server, struct mid_q_entry *mid);
> diff --git a/fs/cifs/connect.c b/fs/cifs/connect.c
> index 86649230b4f2..b4fed69add86 100644
> --- a/fs/cifs/connect.c
> +++ b/fs/cifs/connect.c
> @@ -2294,7 +2294,7 @@ static int match_server(struct TCP_Server_Info *server, struct smb_vol *vol)
>         return 1;
>  }
>
> -static struct TCP_Server_Info *
> +struct TCP_Server_Info *
>  cifs_find_tcp_session(struct smb_vol *vol)
>  {
>         struct TCP_Server_Info *server;
> diff --git a/fs/cifs/dfs_cache.c b/fs/cifs/dfs_cache.c
> new file mode 100644
> index 000000000000..18da279e9354
> --- /dev/null
> +++ b/fs/cifs/dfs_cache.c
> @@ -0,0 +1,1379 @@
> +// SPDX-License-Identifier: GPL-2.0
> +/*
> + * DFS referral cache routines
> + *
> + * Copyright (c) 2018 Paulo Alcantara <palcantara@suse.de>
> + */
> +
> +#include <linux/rcupdate.h>
> +#include <linux/rculist.h>
> +#include <linux/jhash.h>
> +#include <linux/ktime.h>
> +#include <linux/slab.h>
> +#include <linux/nls.h>
> +#include <linux/workqueue.h>
> +#include "cifsglob.h"
> +#include "smb2pdu.h"
> +#include "smb2proto.h"
> +#include "cifsproto.h"
> +#include "cifs_debug.h"
> +#include "cifs_unicode.h"
> +#include "smb2glob.h"
> +
> +#include "dfs_cache.h"
> +
> +#define DFS_CACHE_HTABLE_SIZE 32
> +#define DFS_CACHE_MAX_ENTRIES 64
> +
> +#define IS_INTERLINK_SET(v) ((v) & (DFSREF_REFERRAL_SERVER | \
> +                                   DFSREF_STORAGE_SERVER))
> +
> +struct dfs_cache_tgt {
> +       char *t_name;
> +       struct list_head t_list;
> +};
> +
> +struct dfs_cache_entry {
> +       struct hlist_node ce_hlist;
> +       const char *ce_path;
> +       int ce_ttl;
> +       int ce_srvtype;
> +       int ce_flags;
> +       struct timespec64 ce_etime;
> +       int ce_path_consumed;
> +       int ce_numtgts;
> +       struct list_head ce_tlist;
> +       struct dfs_cache_tgt *ce_tgthint;
> +       struct rcu_head ce_rcu;
> +};
> +
> +static struct kmem_cache *dfs_cache_slab __read_mostly;
> +
> +struct dfs_cache_vol_info {
> +       char *vi_fullpath;
> +       struct smb_vol vi_vol;
> +       struct list_head vi_list;
> +};
> +
> +struct dfs_cache {
> +       struct mutex dc_lock;
> +       struct nls_table *dc_nlsc;
> +       struct list_head dc_vol_list;
> +       int dc_ttl;
> +       struct delayed_work dc_refresh;
> +};
> +
> +static struct dfs_cache dfs_cache;
> +
> +/*
> + * Number of entries in the cache
> + */
> +static size_t dfs_cache_count;
> +
> +static DEFINE_MUTEX(dfs_cache_list_lock);
> +static struct hlist_head dfs_cache_htable[DFS_CACHE_HTABLE_SIZE];
> +
> +static void refresh_cache_worker(struct work_struct *work);
> +
> +static inline bool is_path_valid(const char *path)
> +{
> +       return path && (strchr(path + 1, '\\') || strchr(path + 1, '/'));
> +}
> +
> +static inline int get_normalized_path(const char *path, char **npath)
> +{
> +       if (*path == '\\') {
> +               *npath = (char *)path;
> +       } else {
> +               *npath = kstrndup(path, strlen(path), GFP_KERNEL);
> +               if (!*npath)
> +                       return -ENOMEM;
> +               convert_delimiter(*npath, '\\');
> +       }
> +       return 0;
> +}
> +
> +static inline void free_normalized_path(const char *path, char *npath)
> +{
> +       if (path != npath)
> +               kfree(npath);
> +}
> +
> +static inline bool cache_entry_expired(const struct dfs_cache_entry *ce)
> +{
> +       struct timespec64 ts;
> +
> +       ts = current_kernel_time64();
> +       return timespec64_compare(&ts, &ce->ce_etime) >= 0;
> +}
> +
> +static inline void free_tgts(struct dfs_cache_entry *ce)
> +{
> +       struct dfs_cache_tgt *t, *n;
> +
> +       list_for_each_entry_safe(t, n, &ce->ce_tlist, t_list) {
> +               list_del(&t->t_list);
> +               kfree(t->t_name);
> +               kfree(t);
> +       }
> +}
> +
> +static void free_cache_entry(struct rcu_head *rcu)
> +{
> +       struct dfs_cache_entry *ce = container_of(rcu, struct dfs_cache_entry,
> +                                                 ce_rcu);
> +       kmem_cache_free(dfs_cache_slab, ce);
> +}
> +
> +static inline void flush_cache_ent(struct dfs_cache_entry *ce)
> +{
> +       if (hlist_unhashed(&ce->ce_hlist))
> +               return;
> +
> +       hlist_del_init_rcu(&ce->ce_hlist);
> +       kfree(ce->ce_path);
> +       free_tgts(ce);
> +       dfs_cache_count--;
> +       call_rcu(&ce->ce_rcu, free_cache_entry);
> +}
> +
> +static void flush_cache_ents(void)
> +{
> +       int i;
> +
> +       rcu_read_lock();
> +       for (i = 0; i < DFS_CACHE_HTABLE_SIZE; i++) {
> +               struct hlist_head *l = &dfs_cache_htable[i];
> +               struct dfs_cache_entry *ce;
> +
> +               hlist_for_each_entry_rcu(ce, l, ce_hlist)
> +                       flush_cache_ent(ce);
> +       }
> +       rcu_read_unlock();
> +}
> +
> +/*
> + * dfs cache /proc file
> + */
> +static int dfscache_proc_show(struct seq_file *m, void *v)
> +{
> +       int bucket;
> +       struct dfs_cache_entry *ce;
> +       struct dfs_cache_tgt *t;
> +
> +       seq_puts(m, "DFS cache\n---------\n");
> +
> +       mutex_lock(&dfs_cache_list_lock);
> +
> +       rcu_read_lock();
> +       hash_for_each_rcu(dfs_cache_htable, bucket, ce, ce_hlist) {
> +               seq_printf(m,
> +                          "cache entry: path=%s,type=%s,ttl=%d,etime=%ld,"
> +                          "interlink=%s,path_consumed=%d,expired=%s\n",
> +                          ce->ce_path,
> +                          ce->ce_srvtype == DFS_TYPE_ROOT ? "root" : "link",
> +                          ce->ce_ttl, ce->ce_etime.tv_nsec,
> +                          IS_INTERLINK_SET(ce->ce_flags) ? "yes" : "no",
> +                          ce->ce_path_consumed,
> +                          cache_entry_expired(ce) ? "yes" : "no");
> +
> +               list_for_each_entry(t, &ce->ce_tlist, t_list) {
> +                       seq_printf(m, "  %s%s\n",
> +                                  t->t_name,
> +                                  ce->ce_tgthint == t ? " (target hint)" : "");
> +               }
> +
> +       }
> +       rcu_read_unlock();
> +
> +       mutex_unlock(&dfs_cache_list_lock);
> +       return 0;
> +}
> +
> +static ssize_t dfscache_proc_write(struct file *file, const char __user *buffer,
> +                                  size_t count, loff_t *ppos)
> +{
> +       char c;
> +       int rc;
> +
> +       rc = get_user(c, buffer);
> +       if (rc)
> +               return rc;
> +
> +       if (c != '0')
> +               return -EINVAL;
> +
> +       cifs_dbg(FYI, "clearing dfs cache");
> +       mutex_lock(&dfs_cache_list_lock);
> +       flush_cache_ents();
> +       mutex_unlock(&dfs_cache_list_lock);
> +
> +       return count;
> +}
> +
> +static int dfscache_proc_open(struct inode *inode, struct file *file)
> +{
> +       return single_open(file, dfscache_proc_show, NULL);
> +}
> +
> +const struct file_operations dfscache_proc_fops = {
> +       .open           = dfscache_proc_open,
> +       .read           = seq_read,
> +       .llseek         = seq_lseek,
> +       .release        = single_release,
> +       .write          = dfscache_proc_write,
> +};
> +
> +#ifdef CONFIG_CIFS_DEBUG2
> +static inline void dump_tgts(const struct dfs_cache_entry *ce)
> +{
> +       struct dfs_cache_tgt *t;
> +
> +       cifs_dbg(FYI, "target list:\n");
> +       list_for_each_entry(t, &ce->ce_tlist, t_list) {
> +               cifs_dbg(FYI, "  %s%s\n", t->t_name,
> +                        ce->ce_tgthint == t ? " (target hint)" : "");
> +       }
> +}
> +
> +static inline void dump_ce(const struct dfs_cache_entry *ce)
> +{
> +       cifs_dbg(FYI, "cache entry: path=%s,type=%s,ttl=%d,etime=%ld,"
> +                "interlink=%s,path_consumed=%d,expired=%s\n", ce->ce_path,
> +                ce->ce_srvtype == DFS_TYPE_ROOT ? "root" : "link", ce->ce_ttl,
> +                ce->ce_etime.tv_nsec,
> +                IS_INTERLINK_SET(ce->ce_flags) ? "yes" : "no",
> +                ce->ce_path_consumed,
> +                cache_entry_expired(ce) ? "yes" : "no");
> +       dump_tgts(ce);
> +}
> +
> +static inline void dump_refs(const struct dfs_info3_param *refs, int numrefs)
> +{
> +       int i;
> +
> +       cifs_dbg(FYI, "DFS referrals returned by the server:\n");
> +       for (i = 0; i < numrefs; i++) {
> +               const struct dfs_info3_param *ref = &refs[i];
> +
> +               cifs_dbg(FYI,
> +                        "\n"
> +                        "flags:         0x%x\n"
> +                        "path_consumed: %d\n"
> +                        "server_type:   0x%x\n"
> +                        "ref_flag:      0x%x\n"
> +                        "path_name:     %s\n"
> +                        "node_name:     %s\n"
> +                        "ttl:           %d (%dm)\n",
> +                        ref->flags, ref->path_consumed, ref->server_type,
> +                        ref->ref_flag, ref->path_name, ref->node_name,
> +                        ref->ttl, ref->ttl / 60);
> +       }
> +}
> +#else
> +#define dump_tgts(e)
> +#define dump_ce(e)
> +#define dump_refs(r, n)
> +#endif
> +
> +/**
> + * dfs_cache_init - Initialize DFS referral cache.
> + *
> + * Return zero if initialized successfully, otherwise non-zero.
> + */
> +int dfs_cache_init(void)
> +{
> +       int i;
> +
> +       dfs_cache_slab = kmem_cache_create("cifs_dfs_cache",
> +                                          sizeof(struct dfs_cache_entry), 0,
> +                                          SLAB_HWCACHE_ALIGN, NULL);
> +       if (!dfs_cache_slab)
> +               return -ENOMEM;
> +
> +       for (i = 0; i < DFS_CACHE_HTABLE_SIZE; i++)
> +               INIT_HLIST_HEAD(&dfs_cache_htable[i]);
> +
> +       INIT_LIST_HEAD(&dfs_cache.dc_vol_list);
> +       mutex_init(&dfs_cache.dc_lock);
> +       INIT_DELAYED_WORK(&dfs_cache.dc_refresh, refresh_cache_worker);
> +       dfs_cache.dc_ttl = -1;
> +       dfs_cache.dc_nlsc = load_nls_default();
> +
> +       cifs_dbg(FYI, "%s: initialized DFS referral cache\n", __func__);
> +       return 0;
> +}
> +
> +static inline unsigned int cache_entry_hash(const void *data, int size)
> +{
> +       unsigned int h;
> +
> +       h = jhash(data, size, 0);
> +       return h & (DFS_CACHE_HTABLE_SIZE - 1);
> +}
> +
> +/* Check whether second path component of @path is SYSVOL or NETLOGON */
> +static inline bool is_sysvol_or_netlogon(const char *path)
> +{
> +       const char *s;
> +       char sep = path[0];
> +
> +       s = strchr(path + 1, sep) + 1;
> +       return !strncasecmp(s, "sysvol", strlen("sysvol")) ||
> +               !strncasecmp(s, "netlogon", strlen("netlogon"));
> +}
> +
> +/* Return target hint of a DFS cache entry */
> +static inline char *get_tgt_name(const struct dfs_cache_entry *ce)
> +{
> +       struct dfs_cache_tgt *t = ce->ce_tgthint;
> +
> +       return t ? t->t_name : ERR_PTR(-ENOENT);
> +}
> +
> +/* Return expire time out of a new entry's TTL */
> +static inline struct timespec64 get_expire_time(int ttl)
> +{
> +       struct timespec64 ts = {
> +               .tv_sec = ttl,
> +               .tv_nsec = 0,
> +       };
> +
> +       return timespec64_add(current_kernel_time64(), ts);
> +}
> +
> +/* Allocate a new DFS target */
> +static inline struct dfs_cache_tgt *alloc_tgt(const char *name)
> +{
> +       struct dfs_cache_tgt *t;
> +
> +       t = kmalloc(sizeof(*t), GFP_KERNEL);
> +       if (!t)
> +               return ERR_PTR(-ENOMEM);
> +       t->t_name = kstrndup(name, strlen(name), GFP_KERNEL);
> +       if (!t->t_name) {
> +               kfree(t);
> +               return ERR_PTR(-ENOMEM);
> +       }
> +       INIT_LIST_HEAD(&t->t_list);
> +       return t;
> +}
> +
> +/*
> + * Copy DFS referral information to a cache entry and conditionally update
> + * target hint.
> + */
> +static int copy_ref_data(const struct dfs_info3_param *refs, int numrefs,
> +                        struct dfs_cache_entry *ce, const char *tgthint)
> +{
> +       int i;
> +
> +       ce->ce_ttl = refs[0].ttl;
> +       ce->ce_etime = get_expire_time(ce->ce_ttl);
> +       ce->ce_srvtype = refs[0].server_type;
> +       ce->ce_flags = refs[0].ref_flag;
> +       ce->ce_path_consumed = refs[0].path_consumed;
> +
> +       for (i = 0; i < numrefs; i++) {
> +               struct dfs_cache_tgt *t;
> +
> +               t = alloc_tgt(refs[i].node_name);
> +               if (IS_ERR(t)) {
> +                       free_tgts(ce);
> +                       return PTR_ERR(t);
> +               }
> +               if (tgthint && !strncasecmp(t->t_name, tgthint,
> +                                           strlen(tgthint))) {
> +                       list_add(&t->t_list, &ce->ce_tlist);
> +                       tgthint = NULL;
> +               } else {
> +                       list_add_tail(&t->t_list, &ce->ce_tlist);
> +               }
> +               ce->ce_numtgts++;
> +       }
> +
> +       ce->ce_tgthint = list_first_entry_or_null(&ce->ce_tlist,
> +                                                 struct dfs_cache_tgt, t_list);
> +
> +       return 0;
> +}
> +
> +/* Allocate a new cache entry */
> +static struct dfs_cache_entry *
> +alloc_cache_entry(const char *path, const struct dfs_info3_param *refs,
> +                 int numrefs)
> +{
> +       struct dfs_cache_entry *ce;
> +       int rc;
> +
> +       ce = kmem_cache_zalloc(dfs_cache_slab, GFP_KERNEL);
> +       if (!ce)
> +               return ERR_PTR(-ENOMEM);
> +
> +       ce->ce_path = kstrdup_const(path, GFP_KERNEL);
> +       if (!ce->ce_path) {
> +               kfree(ce);
> +               return ERR_PTR(-ENOMEM);
> +       }
> +       INIT_HLIST_NODE(&ce->ce_hlist);
> +       INIT_LIST_HEAD(&ce->ce_tlist);
> +
> +       rc = copy_ref_data(refs, numrefs, ce, NULL);
> +       if (rc) {
> +               kfree(ce->ce_path);
> +               kfree(ce);
> +               ce = ERR_PTR(rc);
> +       }
> +       return ce;
> +}
> +
> +static void remove_oldest_entry(void)
> +{
> +       int bucket;
> +       struct dfs_cache_entry *ce;
> +       struct dfs_cache_entry *to_del = NULL;
> +
> +       rcu_read_lock();
> +       hash_for_each_rcu(dfs_cache_htable, bucket, ce, ce_hlist) {
> +               if (!to_del || timespec64_compare(&ce->ce_etime,
> +                                                 &to_del->ce_etime) < 0)
> +                       to_del = ce;
> +       }
> +       if (!to_del) {
> +               cifs_dbg(FYI, "%s: no entry to remove", __func__);
> +               goto out;
> +       }
> +       cifs_dbg(FYI, "%s: removing entry", __func__);
> +       dump_ce(to_del);
> +       flush_cache_ent(to_del);
> +out:
> +       rcu_read_unlock();
> +}
> +
> +/* Add a new DFS cache entry */
> +static inline struct dfs_cache_entry *
> +add_cache_entry(unsigned int hash, const char *path,
> +               const struct dfs_info3_param *refs, int numrefs)
> +{
> +       struct dfs_cache_entry *ce;
> +
> +       ce = alloc_cache_entry(path, refs, numrefs);
> +       if (IS_ERR(ce))
> +               return ce;
> +
> +       hlist_add_head_rcu(&ce->ce_hlist, &dfs_cache_htable[hash]);
> +
> +       mutex_lock(&dfs_cache.dc_lock);
> +       if (dfs_cache.dc_ttl < 0) {
> +               dfs_cache.dc_ttl = ce->ce_ttl;
> +               queue_delayed_work(cifsiod_wq, &dfs_cache.dc_refresh,
> +                                  dfs_cache.dc_ttl * HZ);
> +       } else {
> +               dfs_cache.dc_ttl = min_t(int, dfs_cache.dc_ttl, ce->ce_ttl);
> +               mod_delayed_work(cifsiod_wq, &dfs_cache.dc_refresh,
> +                                dfs_cache.dc_ttl * HZ);
> +       }
> +       mutex_unlock(&dfs_cache.dc_lock);
> +
> +       return ce;
> +}
> +
> +static struct dfs_cache_entry *__find_cache_entry(unsigned int hash,
> +                                                 const char *path, int len)
> +{
> +       struct dfs_cache_entry *ce;
> +       bool found = false;
> +
> +       rcu_read_lock();
> +       hlist_for_each_entry_rcu(ce, &dfs_cache_htable[hash], ce_hlist) {
> +               if (!strncasecmp(ce->ce_path, path, len)) {
> +#ifdef CONFIG_CIFS_DEBUG2
> +                       char *name = get_tgt_name(ce);
> +
> +                       if (unlikely(IS_ERR(name))) {
> +                               rcu_read_unlock();
> +                               return ERR_CAST(name);
> +                       }
> +                       cifs_dbg(FYI, "%s: cache hit\n", __func__);
> +                       cifs_dbg(FYI, "%s: target hint: %s\n", __func__, name);
> +#endif
> +                       found = true;
> +                       break;
> +               }
> +       }
> +       rcu_read_unlock();
> +       return found ? ce : ERR_PTR(-ENOENT);
> +}
> +
> +/*
> + * Find a DFS cache entry in hash table and optionally check prefix path against
> + * @path.
> + * Use whole path components in the match.
> + * Return ERR_PTR(-ENOENT) if the entry is not found.
> + */
> +static inline struct dfs_cache_entry *find_cache_entry(const char *path,
> +                                                      unsigned int *hash)
> +{
> +       int len = strlen(path);
> +
> +       *hash = cache_entry_hash(path, len);
> +       return __find_cache_entry(*hash, path, len);
> +}
> +
> +static inline void destroy_slab_cache(void)
> +{
> +       rcu_barrier();
> +       kmem_cache_destroy(dfs_cache_slab);
> +}
> +
> +static inline void free_vol(struct dfs_cache_vol_info *vi)
> +{
> +       list_del(&vi->vi_list);
> +       kfree(vi->vi_fullpath);
> +       cifs_cleanup_volume_info_contents(&vi->vi_vol);
> +       kfree(vi);
> +}
> +
> +static inline void free_vol_list(void)
> +{
> +       struct dfs_cache_vol_info *vi, *nvi;
> +
> +       list_for_each_entry_safe(vi, nvi, &dfs_cache.dc_vol_list, vi_list)
> +               free_vol(vi);
> +}
> +
> +/**
> + * dfs_cache_destroy - destroy DFS referral cache
> + */
> +void dfs_cache_destroy(void)
> +{
> +       cancel_delayed_work_sync(&dfs_cache.dc_refresh);
> +       unload_nls(dfs_cache.dc_nlsc);
> +       free_vol_list();
> +       mutex_destroy(&dfs_cache.dc_lock);
> +
> +       flush_cache_ents();
> +       destroy_slab_cache();
> +       mutex_destroy(&dfs_cache_list_lock);
> +
> +       cifs_dbg(FYI, "%s: destroyed DFS referral cache\n", __func__);
> +}
> +
> +static inline struct dfs_cache_entry *
> +__update_cache_entry(const char *path, const struct dfs_info3_param *refs,
> +                    int numrefs)
> +{
> +       int rc;
> +       unsigned int h;
> +       struct dfs_cache_entry *ce;
> +       char *s, *th = NULL;
> +
> +       ce = find_cache_entry(path, &h);
> +       if (IS_ERR(ce))
> +               return ce;
> +
> +       if (ce->ce_tgthint) {
> +               s = ce->ce_tgthint->t_name;
> +               th = kstrndup(s, strlen(s), GFP_KERNEL);
> +               if (!th)
> +                       return ERR_PTR(-ENOMEM);
> +       }
> +
> +       free_tgts(ce);
> +       ce->ce_numtgts = 0;
> +
> +       rc = copy_ref_data(refs, numrefs, ce, th);
> +       kfree(th);
> +
> +       if (rc)
> +               ce = ERR_PTR(rc);
> +
> +       return ce;
> +}
> +
> +/* Update an expired cache entry by getting a new DFS referral from server */
> +static struct dfs_cache_entry *
> +update_cache_entry(const unsigned int xid, struct cifs_ses *ses,
> +                  const struct nls_table *nls_codepage, int remap,
> +                  const char *path, struct dfs_cache_entry *ce)
> +{
> +       int rc;
> +       struct dfs_info3_param *refs = NULL;
> +       int numrefs = 0;
> +
> +       cifs_dbg(FYI, "%s: update expired cache entry\n", __func__);
> +       /*
> +        * Check if caller provided enough parameters to update an expired
> +        * entry.
> +        */
> +       if (!ses || !ses->server || !ses->server->ops->get_dfs_refer)
> +               return ERR_PTR(-ETIME);
> +       if (unlikely(!nls_codepage))
> +               return ERR_PTR(-ETIME);
> +
> +       cifs_dbg(FYI, "%s: DFS referral request for %s\n", __func__, path);
> +
> +       rc = ses->server->ops->get_dfs_refer(xid, ses, path, &refs, &numrefs,
> +                                            nls_codepage, remap);
> +       if (rc)
> +               ce = ERR_PTR(rc);
> +       else
> +               ce = __update_cache_entry(path, refs, numrefs);
> +
> +       dump_refs(refs, numrefs);
> +       free_dfs_info_array(refs, numrefs);
> +
> +       return ce;
> +}
> +
> +/*
> + * Find, create or update a DFS cache entry.
> + *
> + * If the entry wasn't found, it will create a new one. Or if it was found but
> + * expired, then it will update the entry accordingly.
> + *
> + * For interlinks, __cifs_dfs_mount() and expand_dfs_referral() are supposed to
> + * handle them properly.
> + */
> +static struct dfs_cache_entry *
> +do_dfs_cache_find(const unsigned int xid, struct cifs_ses *ses,
> +                 const struct nls_table *nls_codepage, int remap,
> +                 const char *path, bool noreq)
> +{
> +       int rc;
> +       unsigned int h;
> +       struct dfs_cache_entry *ce;
> +       struct dfs_info3_param *nrefs;
> +       int numnrefs;
> +
> +       cifs_dbg(FYI, "%s: search path: %s\n", __func__, path);
> +
> +       ce = find_cache_entry(path, &h);
> +       if (IS_ERR(ce)) {
> +               cifs_dbg(FYI, "%s: cache miss\n", __func__);
> +               /*
> +                * If @noreq is set, no requests will be sent to the server for
> +                * either updating or getting a new DFS referral.
> +                */
> +               if (noreq)
> +                       return ce;
> +               /*
> +                * No cache entry was found, so check for valid parameters that
> +                * will be required to get a new DFS referral and then create a
> +                * new cache entry.
> +                */
> +               if (!ses || !ses->server || !ses->server->ops->get_dfs_refer) {
> +                       ce = ERR_PTR(-ENOSYS);
> +                       return ce;
> +               }
> +               if (unlikely(!nls_codepage)) {
> +                       ce = ERR_PTR(-EINVAL);
> +                       return ce;
> +               }
> +
> +               nrefs = NULL;
> +               numnrefs = 0;
> +
> +               cifs_dbg(FYI, "%s: DFS referral request for %s\n", __func__,
> +                        path);
> +
> +               rc = ses->server->ops->get_dfs_refer(xid, ses, path, &nrefs,
> +                                                    &numnrefs, nls_codepage,
> +                                                    remap);
> +               if (rc) {
> +                       ce = ERR_PTR(rc);
> +                       return ce;
> +               }
> +
> +               dump_refs(nrefs, numnrefs);
> +
> +               cifs_dbg(FYI, "%s: new cache entry\n", __func__);
> +
> +               if (dfs_cache_count >= DFS_CACHE_MAX_ENTRIES) {
> +                       cifs_dbg(FYI, "%s: reached max cache size (%d)",
> +                                __func__, DFS_CACHE_MAX_ENTRIES);
> +                       remove_oldest_entry();
> +               }
> +               ce = add_cache_entry(h, path, nrefs, numnrefs);
> +               free_dfs_info_array(nrefs, numnrefs);
> +
> +               if (IS_ERR(ce))
> +                       return ce;
> +
> +               dfs_cache_count++;
> +       }
> +
> +       dump_ce(ce);
> +
> +       /* Just return the found cache entry in case @noreq is set */
> +       if (noreq)
> +               return ce;
> +
> +       if (cache_entry_expired(ce)) {
> +               cifs_dbg(FYI, "%s: expired cache entry\n", __func__);
> +               ce = update_cache_entry(xid, ses, nls_codepage, remap, path,
> +                                       ce);
> +               if (IS_ERR(ce)) {
> +                       cifs_dbg(FYI, "%s: failed to update expired entry\n",
> +                                __func__);
> +               }
> +       }
> +       return ce;
> +}
> +
> +/* Set up a new DFS referral from a given cache entry */
> +static int setup_ref(const char *path, const struct dfs_cache_entry *ce,
> +                    struct dfs_info3_param *ref, const char *tgt)
> +{
> +       int rc;
> +
> +       cifs_dbg(FYI, "%s: set up new ref\n", __func__);
> +
> +       memset(ref, 0, sizeof(*ref));
> +
> +       ref->path_name = kstrndup(path, strlen(path), GFP_KERNEL);
> +       if (!ref->path_name)
> +               return -ENOMEM;
> +
> +       ref->path_consumed = ce->ce_path_consumed;
> +
> +       ref->node_name = kstrndup(tgt, strlen(tgt), GFP_KERNEL);
> +       if (!ref->node_name) {
> +               rc = -ENOMEM;
> +               goto err_free_path;
> +       }
> +
> +       ref->ttl = ce->ce_ttl;
> +       ref->server_type = ce->ce_srvtype;
> +       ref->ref_flag = ce->ce_flags;
> +
> +       return 0;
> +
> +err_free_path:
> +       kfree(ref->path_name);
> +       ref->path_name = NULL;
> +       return rc;
> +}
> +
> +/* Return target list of a DFS cache entry */
> +static int get_tgt_list(const struct dfs_cache_entry *ce,
> +                       struct dfs_cache_tgt_list *tl)
> +{
> +       int rc;
> +       struct list_head *head = &tl->tl_list;
> +       struct dfs_cache_tgt *t;
> +       struct dfs_cache_tgt_iterator *it, *nit;
> +
> +       memset(tl, 0, sizeof(*tl));
> +       INIT_LIST_HEAD(head);
> +
> +       list_for_each_entry(t, &ce->ce_tlist, t_list) {
> +               it = kzalloc(sizeof(*it), GFP_KERNEL);
> +               if (!it) {
> +                       rc = -ENOMEM;
> +                       goto err_free_it;
> +               }
> +
> +               it->it_name = kstrndup(t->t_name, strlen(t->t_name),
> +                                      GFP_KERNEL);
> +               if (!it->it_name) {
> +                       rc = -ENOMEM;
> +                       goto err_free_it;
> +               }
> +
> +               if (ce->ce_tgthint == t)
> +                       list_add(&it->it_list, head);
> +               else
> +                       list_add_tail(&it->it_list, head);
> +       }
> +       tl->tl_numtgts = ce->ce_numtgts;
> +
> +       return 0;
> +
> +err_free_it:
> +       list_for_each_entry_safe(it, nit, head, it_list) {
> +               kfree(it->it_name);
> +               kfree(it);
> +       }
> +       return rc;
> +}
> +
> +/**
> + * dfs_cache_find - find a DFS cache entry
> + *
> + * If it doesn't find the cache entry, then it will get a DFS referral
> + * for @path and create a new entry.
> + *
> + * In case the cache entry exists but expired, it will get a DFS referral
> + * for @path and then update the respective cache entry.
> + *
> + * These parameters are passed down to the get_dfs_refer() call if it
> + * needs to be issued:
> + * @xid: syscall xid
> + * @ses: smb session to issue the request on
> + * @nls_codepage: charset conversion
> + * @remap: path character remapping type
> + * @path: path to lookup in DFS referral cache.
> + *
> + * @ref: when non-NULL, store single DFS referral result in it.
> + * @tgt_list: when non-NULL, store complete DFS target list in it.
> + *
> + * Return zero if the target was found, otherwise non-zero.
> + */
> +int dfs_cache_find(const unsigned int xid, struct cifs_ses *ses,
> +                  const struct nls_table *nls_codepage, int remap,
> +                  const char *path, struct dfs_info3_param *ref,
> +                  struct dfs_cache_tgt_list *tgt_list)
> +{
> +       int rc;
> +       char *npath;
> +       struct dfs_cache_entry *ce;
> +
> +       if (unlikely(!is_path_valid(path)))
> +               return -EINVAL;
> +
> +       rc = get_normalized_path(path, &npath);
> +       if (rc)
> +               return rc;
> +
> +       mutex_lock(&dfs_cache_list_lock);
> +       ce = do_dfs_cache_find(xid, ses, nls_codepage, remap, npath, false);
> +       if (!IS_ERR(ce)) {
> +               if (ref)
> +                       rc = setup_ref(path, ce, ref, get_tgt_name(ce));
> +               else
> +                       rc = 0;
> +               if (!rc && tgt_list)
> +                       rc = get_tgt_list(ce, tgt_list);
> +       } else {
> +               rc = PTR_ERR(ce);
> +       }
> +       mutex_unlock(&dfs_cache_list_lock);
> +       free_normalized_path(path, npath);
> +       return rc;
> +}
> +
> +/**
> + * dfs_cache_noreq_find - find a DFS cache entry without sending any requests to
> + * the currently connected server.
> + *
> + * NOTE: This function will neither update a cache entry in case it was
> + * expired, nor create a new cache entry if @path hasn't been found. It heavily
> + * relies on an existing cache entry.
> + *
> + * @path: path to lookup in the DFS referral cache.
> + * @ref: when non-NULL, store single DFS referral result in it.
> + * @tgt_list: when non-NULL, store complete DFS target list in it.
> + *
> + * Return 0 if successful.
> + * Return -ENOENT if the entry was not found.
> + * Return non-zero for other errors.
> + */
> +int dfs_cache_noreq_find(const char *path, struct dfs_info3_param *ref,
> +                        struct dfs_cache_tgt_list *tgt_list)
> +{
> +       int rc;
> +       char *npath;
> +       struct dfs_cache_entry *ce;
> +
> +       if (unlikely(!is_path_valid(path)))
> +               return -EINVAL;
> +
> +       rc = get_normalized_path(path, &npath);
> +       if (rc)
> +               return rc;
> +
> +       mutex_lock(&dfs_cache_list_lock);
> +       ce = do_dfs_cache_find(0, NULL, NULL, 0, npath, true);
> +       if (IS_ERR(ce)) {
> +               rc = PTR_ERR(ce);
> +               goto out;
> +       }
> +
> +       if (ref)
> +               rc = setup_ref(path, ce, ref, get_tgt_name(ce));
> +       else
> +               rc = 0;
> +       if (!rc && tgt_list)
> +               rc = get_tgt_list(ce, tgt_list);
> +out:
> +       mutex_unlock(&dfs_cache_list_lock);
> +       free_normalized_path(path, npath);
> +       return rc;
> +}
> +
> +/**
> + * dfs_cache_update_tgthint - update target hint of a DFS cache entry
> + *
> + * If it doesn't find the cache entry, then it will get a DFS referral for @path
> + * and create a new entry.
> + *
> + * In case the cache entry exists but expired, it will get a DFS referral
> + * for @path and then update the respective cache entry.
> + *
> + * @xid: syscall id
> + * @ses: smb session
> + * @nls_codepage: charset conversion
> + * @remap: type of character remapping for paths
> + * @path: path to lookup in DFS referral cache.
> + * @it: DFS target iterator
> + *
> + * Return zero if the target hint was updated successfully, otherwise non-zero.
> + */
> +int dfs_cache_update_tgthint(const unsigned int xid, struct cifs_ses *ses,
> +                            const struct nls_table *nls_codepage, int remap,
> +                            const char *path,
> +                            const struct dfs_cache_tgt_iterator *it)
> +{
> +       int rc;
> +       char *npath;
> +       struct dfs_cache_entry *ce;
> +       struct dfs_cache_tgt *t;
> +
> +       if (unlikely(!is_path_valid(path)))
> +               return -EINVAL;
> +
> +       rc = get_normalized_path(path, &npath);
> +       if (rc)
> +               return rc;
> +
> +       cifs_dbg(FYI, "%s: path: %s\n", __func__, npath);
> +
> +       mutex_lock(&dfs_cache_list_lock);
> +       ce = do_dfs_cache_find(xid, ses, nls_codepage, remap, npath, false);
> +       if (IS_ERR(ce)) {
> +               rc = PTR_ERR(ce);
> +               goto out;
> +       }
> +
> +       rc = 0;
> +
> +       t = ce->ce_tgthint;
> +
> +       if (likely(!strncasecmp(it->it_name, t->t_name, strlen(t->t_name))))
> +               goto out;
> +
> +       list_for_each_entry(t, &ce->ce_tlist, t_list) {
> +               if (!strncasecmp(t->t_name, it->it_name, strlen(it->it_name))) {
> +                       ce->ce_tgthint = t;
> +                       cifs_dbg(FYI, "%s: new target hint: %s\n", __func__,
> +                                it->it_name);
> +                       break;
> +               }
> +       }
> +
> +out:
> +       mutex_unlock(&dfs_cache_list_lock);
> +       free_normalized_path(path, npath);
> +       return rc;
> +}
> +
> +/**
> + * dfs_cache_noreq_update_tgthint - update target hint of a DFS cache entry
> + * without sending any requests to the currently connected server.
> + *
> + * NOTE: This function will neither update a cache entry in case it was
> + * expired, nor create a new cache entry if @path hasn't been found. It heavily
> + * relies on an existing cache entry.
> + *
> + * @path: path to lookup in DFS referral cache.
> + * @it: target iterator which contains the target hint to update the cache
> + * entry with.
> + *
> + * Return zero if the target hint was updated successfully, otherwise non-zero.
> + */
> +int dfs_cache_noreq_update_tgthint(const char *path,
> +                                  const struct dfs_cache_tgt_iterator *it)
> +{
> +       int rc;
> +       char *npath;
> +       struct dfs_cache_entry *ce;
> +       struct dfs_cache_tgt *t;
> +
> +       if (unlikely(!is_path_valid(path)) || !it)
> +               return -EINVAL;
> +
> +       rc = get_normalized_path(path, &npath);
> +       if (rc)
> +               return rc;
> +
> +       cifs_dbg(FYI, "%s: path: %s\n", __func__, npath);
> +
> +       mutex_lock(&dfs_cache_list_lock);
> +
> +       ce = do_dfs_cache_find(0, NULL, NULL, 0, npath, true);
> +       if (IS_ERR(ce)) {
> +               rc = PTR_ERR(ce);
> +               goto out;
> +       }
> +
> +       rc = 0;
> +
> +       t = ce->ce_tgthint;
> +
> +       if (unlikely(!strncasecmp(it->it_name, t->t_name, strlen(t->t_name))))
> +               goto out;
> +
> +       list_for_each_entry(t, &ce->ce_tlist, t_list) {
> +               if (!strncasecmp(t->t_name, it->it_name, strlen(it->it_name))) {
> +                       ce->ce_tgthint = t;
> +                       cifs_dbg(FYI, "%s: new target hint: %s\n", __func__,
> +                                it->it_name);
> +                       break;
> +               }
> +       }
> +
> +out:
> +       mutex_unlock(&dfs_cache_list_lock);
> +       free_normalized_path(path, npath);
> +       return rc;
> +}
> +
> +/**
> + * dfs_cache_get_tgt_referral - returns a DFS referral (@ref) from a given
> + * target iterator (@it).
> + *
> + * @path: path to lookup in DFS referral cache.
> + * @it: DFS target iterator.
> + * @ref: DFS referral pointer to set up the gathered information.
> + *
> + * Return zero if the DFS referral was set up correctly, otherwise non-zero.
> + */
> +int dfs_cache_get_tgt_referral(const char *path,
> +                              const struct dfs_cache_tgt_iterator *it,
> +                              struct dfs_info3_param *ref)
> +{
> +       int rc;
> +       char *npath;
> +       struct dfs_cache_entry *ce;
> +       unsigned int h;
> +
> +       if (!it || !ref)
> +               return -EINVAL;
> +       if (unlikely(!is_path_valid(path)))
> +               return -EINVAL;
> +
> +       rc = get_normalized_path(path, &npath);
> +       if (rc)
> +               return rc;
> +
> +       cifs_dbg(FYI, "%s: path: %s\n", __func__, npath);
> +
> +       mutex_lock(&dfs_cache_list_lock);
> +
> +       ce = find_cache_entry(npath, &h);
> +       if (IS_ERR(ce)) {
> +               rc = PTR_ERR(ce);
> +               goto out;
> +       }
> +
> +       cifs_dbg(FYI, "%s: target name: %s\n", __func__, it->it_name);
> +
> +       rc = setup_ref(path, ce, ref, it->it_name);
> +
> +out:
> +       mutex_unlock(&dfs_cache_list_lock);
> +       free_normalized_path(path, npath);
> +       return rc;
> +}
> +
> +static int dup_vol(struct smb_vol *vol, struct smb_vol *new)
> +{
> +       memcpy(new, vol, sizeof(*new));
> +
> +       if (vol->username) {
> +               new->username = kstrndup(vol->username, strlen(vol->username),
> +                                       GFP_KERNEL);
> +               if (!new->username)
> +                       return -ENOMEM;
> +       }
> +       if (vol->password) {
> +               new->password = kstrndup(vol->password, strlen(vol->password),
> +                                        GFP_KERNEL);
> +               if (!new->password)
> +                       goto err_free_username;
> +       }
> +       if (vol->UNC) {
> +               cifs_dbg(FYI, "%s: vol->UNC: %s\n", __func__, vol->UNC);
> +               new->UNC = kstrndup(vol->UNC, strlen(vol->UNC), GFP_KERNEL);
> +               if (!new->UNC)
> +                       goto err_free_password;
> +       }
> +       if (vol->domainname) {
> +               new->domainname = kstrndup(vol->domainname,
> +                                         strlen(vol->domainname), GFP_KERNEL);
> +               if (!new->domainname)
> +                       goto err_free_unc;
> +       }
> +       if (vol->iocharset) {
> +               new->iocharset = kstrndup(vol->iocharset,
> +                                         strlen(vol->iocharset), GFP_KERNEL);
> +               if (!new->iocharset)
> +                       goto err_free_domainname;
> +       }
> +       if (vol->prepath) {
> +               cifs_dbg(FYI, "%s: vol->prepath: %s\n", __func__, vol->prepath);
> +               new->prepath = kstrndup(vol->prepath, strlen(vol->prepath),
> +                                       GFP_KERNEL);
> +               if (!new->prepath)
> +                       goto err_free_iocharset;
> +       }
> +
> +       return 0;
> +
> +err_free_iocharset:
> +       kfree(new->iocharset);
> +err_free_domainname:
> +       kfree(new->domainname);
> +err_free_unc:
> +       kfree(new->UNC);
> +err_free_password:
> +       kfree(new->password);
> +err_free_username:
> +       kfree(new->username);
> +       kfree(new);
> +       return -ENOMEM;
> +}
> +
> +/**
> + * dfs_cache_add_vol - add a cifs volume during mount() that will be handled by
> + * DFS cache refresh worker.
> + *
> + * @vol: cifs volume.
> + * @fullpath: origin full path.
> + *
> + * Return zero if volume was set up correctly, otherwise non-zero.
> + */
> +int dfs_cache_add_vol(struct smb_vol *vol, const char *fullpath)
> +{
> +       int rc;
> +       struct dfs_cache_vol_info *vi;
> +
> +       if (!vol || !fullpath)
> +               return -EINVAL;
> +
> +       cifs_dbg(FYI, "%s: fullpath: %s\n", __func__, fullpath);
> +
> +       vi = kzalloc(sizeof(*vi), GFP_KERNEL);
> +       if (!vi)
> +               return -ENOMEM;
> +
> +       vi->vi_fullpath = kstrndup(fullpath, strlen(fullpath), GFP_KERNEL);
> +       if (!vi->vi_fullpath) {
> +               rc = -ENOMEM;
> +               goto err_free_vi;
> +       }
> +
> +       rc = dup_vol(vol, &vi->vi_vol);
> +       if (rc)
> +               goto err_free_fullpath;
> +
> +       mutex_lock(&dfs_cache.dc_lock);
> +       list_add_tail(&vi->vi_list, &dfs_cache.dc_vol_list);
> +       mutex_unlock(&dfs_cache.dc_lock);
> +       return 0;
> +
> +err_free_fullpath:
> +       kfree(vi->vi_fullpath);
> +err_free_vi:
> +       kfree(vi);
> +       return rc;
> +}
> +
> +static inline struct dfs_cache_vol_info *find_vol(const char *fullpath)
> +{
> +       struct dfs_cache_vol_info *vi;
> +
> +       list_for_each_entry(vi, &dfs_cache.dc_vol_list, vi_list) {
> +               cifs_dbg(FYI, "%s: vi->vi_fullpath: %s\n", __func__,
> +                        vi->vi_fullpath);
> +               if (!strncasecmp(vi->vi_fullpath, fullpath,
> +                                strlen(vi->vi_fullpath)))
> +                       return vi;
> +       }
> +       return ERR_PTR(-ENOENT);
> +}
> +
> +/**
> + * dfs_cache_update_vol - update vol info in DFS cache after failover
> + *
> + * @fullpath: fullpath to look up in volume list.
> + * @server: TCP ses pointer.
> + *
> + * Return zero if volume was updated, otherwise non-zero.
> + */
> +int dfs_cache_update_vol(const char *fullpath, struct TCP_Server_Info *server)
> +{
> +       int rc;
> +       struct dfs_cache_vol_info *vi;
> +
> +       if (!fullpath || !server)
> +               return -EINVAL;
> +
> +       cifs_dbg(FYI, "%s: fullpath: %s\n", __func__, fullpath);
> +
> +       mutex_lock(&dfs_cache.dc_lock);
> +
> +       vi = find_vol(fullpath);
> +       if (IS_ERR(vi)) {
> +               rc = PTR_ERR(vi);
> +               goto out;
> +       }
> +
> +       cifs_dbg(FYI, "%s: updating volume info\n", __func__);
> +       memcpy(&vi->vi_vol.dstaddr, &server->dstaddr,
> +              sizeof(vi->vi_vol.dstaddr));
> +       rc = 0;
> +
> +out:
> +       mutex_unlock(&dfs_cache.dc_lock);
> +       return rc;
> +}
> +
> +/**
> + * dfs_cache_del_vol - remove volume info in DFS cache during umount()
> + *
> + * @fullpath: fullpath to look up in volume list.
> + *
> + * Return zero if volume was deleted, otherwise non-zero.
> + */
> +void dfs_cache_del_vol(const char *fullpath)
> +{
> +       struct dfs_cache_vol_info *vi;
> +
> +       if (!fullpath || !*fullpath)
> +               return;
> +
> +       cifs_dbg(FYI, "%s: fullpath: %s\n", __func__, fullpath);
> +
> +       mutex_lock(&dfs_cache.dc_lock);
> +       vi = find_vol(fullpath);
> +       if (!IS_ERR(vi))
> +               free_vol(vi);
> +       mutex_unlock(&dfs_cache.dc_lock);
> +}
> +
> +/* Get all tcons that are within a DFS namespace and can be refreshed */
> +static void get_tcons(struct TCP_Server_Info *server, struct list_head *head)
> +{
> +       struct cifs_ses *ses;
> +       struct cifs_tcon *tcon;
> +
> +       INIT_LIST_HEAD(head);
> +
> +       spin_lock(&cifs_tcp_ses_lock);
> +       list_for_each_entry(ses, &server->smb_ses_list, smb_ses_list) {
> +               list_for_each_entry(tcon, &ses->tcon_list, tcon_list) {
> +                       if (!tcon->need_reconnect && !tcon->need_reopen_files &&
> +                           tcon->dfs_path) {
> +                               tcon->tc_count++;
> +                               list_add_tail(&tcon->ulist, head);
> +                       }
> +               }
> +               if (ses->tcon_ipc && !ses->tcon_ipc->need_reconnect &&
> +                   ses->tcon_ipc->dfs_path) {
> +                       list_add_tail(&ses->tcon_ipc->ulist, head);
> +               }
> +       }
> +       spin_unlock(&cifs_tcp_ses_lock);
> +}
> +
> +/* Refresh DFS cache entry from a given tcon */
> +static void do_refresh_tcon(struct dfs_cache *dc, struct cifs_tcon *tcon)
> +{
> +       int rc = 0;
> +       unsigned int xid;
> +       char *path, *npath;
> +       unsigned int h;
> +       struct dfs_cache_entry *ce;
> +       struct dfs_info3_param *refs = NULL;
> +       int numrefs = 0;
> +
> +       xid = get_xid();
> +
> +       path = tcon->dfs_path + 1;
> +
> +       cifs_dbg(FYI, "%s: dfs path: %s\n", __func__, path);
> +
> +       rc = get_normalized_path(path, &npath);
> +       if (rc)
> +               goto out;
> +
> +       mutex_lock(&dfs_cache_list_lock);
> +       ce = find_cache_entry(npath, &h);
> +       mutex_unlock(&dfs_cache_list_lock);
> +
> +       if (IS_ERR(ce)) {
> +               rc = PTR_ERR(ce);
> +               goto out;
> +       }
> +
> +       if (!cache_entry_expired(ce))
> +               goto out;
> +
> +       if (unlikely(!tcon->ses->server->ops->get_dfs_refer)) {
> +               rc = -ENOSYS;
> +       } else {
> +               rc = tcon->ses->server->ops->get_dfs_refer(xid, tcon->ses, path,
> +                                                          &refs, &numrefs,
> +                                                          dc->dc_nlsc,
> +                                                          tcon->remap);
> +               if (!rc) {
> +                       mutex_lock(&dfs_cache_list_lock);
> +                       ce = __update_cache_entry(npath, refs, numrefs);
> +                       mutex_unlock(&dfs_cache_list_lock);
> +                       dump_refs(refs, numrefs);
> +                       free_dfs_info_array(refs, numrefs);
> +                       if (IS_ERR(ce))
> +                               rc = PTR_ERR(ce);
> +               }
> +       }
> +       if (rc)
> +               cifs_dbg(FYI, "%s: failed to update expired entry\n", __func__);
> +out:
> +       free_xid(xid);
> +       free_normalized_path(path, npath);
> +}
> +
> +/*
> + * Worker that will refresh DFS cache based on lowest TTL value from a DFS
> + * referral.
> + *
> + * FIXME: ensure that all requests are sent to DFS root for refreshing the
> + * cache.
> + */
> +static void refresh_cache_worker(struct work_struct *work)
> +{
> +       struct dfs_cache *dc = container_of(work, struct dfs_cache,
> +                                           dc_refresh.work);
> +       struct dfs_cache_vol_info *vi;
> +       struct TCP_Server_Info *server;
> +       LIST_HEAD(list);
> +       struct cifs_tcon *tcon, *ntcon;
> +
> +       cifs_dbg(FYI, "%s: refreshing DFS referral cache\n", __func__);
> +
> +       mutex_lock(&dc->dc_lock);
> +
> +       list_for_each_entry(vi, &dc->dc_vol_list, vi_list) {
> +               cifs_dbg(FYI, "%s: vol path: %s\n", __func__, vi->vi_fullpath);
> +               server = cifs_find_tcp_session(&vi->vi_vol);
> +               if (!server)
> +                       continue;
> +               cifs_dbg(FYI, "%s: found a tcp ses: %p\n", __func__, server);
> +               if (server->tcpStatus != CifsGood)
> +                       goto next;
> +               get_tcons(server, &list);
> +               list_for_each_entry_safe(tcon, ntcon, &list, ulist) {
> +                       do_refresh_tcon(dc, tcon);
> +                       list_del_init(&tcon->ulist);
> +                       cifs_put_tcon(tcon);
> +               }
> +next:
> +               cifs_put_tcp_session(server, 0);
> +       }
> +       queue_delayed_work(cifsiod_wq, &dc->dc_refresh, dc->dc_ttl * HZ);
> +       mutex_unlock(&dc->dc_lock);
> +
> +       cifs_dbg(FYI, "%s: finished refreshing DFS referral cache\n", __func__);
> +}
> diff --git a/fs/cifs/dfs_cache.h b/fs/cifs/dfs_cache.h
> new file mode 100644
> index 000000000000..22f366514f3a
> --- /dev/null
> +++ b/fs/cifs/dfs_cache.h
> @@ -0,0 +1,97 @@
> +/* SPDX-License-Identifier: GPL-2.0 */
> +/*
> + * DFS referral cache routines
> + *
> + * Copyright (c) 2018 Paulo Alcantara <palcantara@suse.de>
> + */
> +
> +#ifndef _CIFS_DFS_CACHE_H
> +#define _CIFS_DFS_CACHE_H
> +
> +#include <linux/nls.h>
> +#include <linux/list.h>
> +#include "cifsglob.h"
> +
> +struct dfs_cache_tgt_list {
> +       int tl_numtgts;
> +       struct list_head tl_list;
> +};
> +
> +struct dfs_cache_tgt_iterator {
> +       char *it_name;
> +       struct list_head it_list;
> +};
> +
> +extern int dfs_cache_init(void);
> +extern void dfs_cache_destroy(void);
> +extern const struct file_operations dfscache_proc_fops;
> +
> +extern int dfs_cache_find(const unsigned int xid, struct cifs_ses *ses,
> +                         const struct nls_table *nls_codepage, int remap,
> +                         const char *path, struct dfs_info3_param *ref,
> +                         struct dfs_cache_tgt_list *tgt_list);
> +extern int dfs_cache_noreq_find(const char *path, struct dfs_info3_param *ref,
> +                               struct dfs_cache_tgt_list *tgt_list);
> +extern int dfs_cache_update_tgthint(const unsigned int xid,
> +                                   struct cifs_ses *ses,
> +                                   const struct nls_table *nls_codepage,
> +                                   int remap, const char *path,
> +                                   const struct dfs_cache_tgt_iterator *it);
> +extern int
> +dfs_cache_noreq_update_tgthint(const char *path,
> +                              const struct dfs_cache_tgt_iterator *it);
> +extern int dfs_cache_get_tgt_referral(const char *path,
> +                                     const struct dfs_cache_tgt_iterator *it,
> +                                     struct dfs_info3_param *ref);
> +extern int dfs_cache_add_vol(struct smb_vol *vol, const char *fullpath);
> +extern int dfs_cache_update_vol(const char *fullpath,
> +                               struct TCP_Server_Info *server);
> +extern void dfs_cache_del_vol(const char *fullpath);
> +
> +static inline struct dfs_cache_tgt_iterator *
> +dfs_cache_get_next_tgt(struct dfs_cache_tgt_list *tl,
> +                      struct dfs_cache_tgt_iterator *it)
> +{
> +       if (!tl || list_empty(&tl->tl_list) || !it ||
> +           list_is_last(&it->it_list, &tl->tl_list))
> +               return NULL;
> +       return list_next_entry(it, it_list);
> +}
> +
> +static inline struct dfs_cache_tgt_iterator *
> +dfs_cache_get_tgt_iterator(struct dfs_cache_tgt_list *tl)
> +{
> +       if (!tl)
> +               return NULL;
> +       return list_first_entry_or_null(&tl->tl_list,
> +                                       struct dfs_cache_tgt_iterator,
> +                                       it_list);
> +}
> +
> +static inline void dfs_cache_free_tgts(struct dfs_cache_tgt_list *tl)
> +{
> +       struct dfs_cache_tgt_iterator *it, *nit;
> +
> +       if (!tl || list_empty(&tl->tl_list))
> +               return;
> +       list_for_each_entry_safe(it, nit, &tl->tl_list, it_list) {
> +               list_del(&it->it_list);
> +               kfree(it->it_name);
> +               kfree(it);
> +       }
> +       tl->tl_numtgts = 0;
> +}
> +
> +static inline const char *
> +dfs_cache_get_tgt_name(const struct dfs_cache_tgt_iterator *it)
> +{
> +       return it ? it->it_name : NULL;
> +}
> +
> +static inline int
> +dfs_cache_get_nr_tgts(const struct dfs_cache_tgt_list *tl)
> +{
> +       return tl ? tl->tl_numtgts : 0;
> +}
> +
> +#endif /* _CIFS_DFS_CACHE_H */
> --
> 2.13.7
>
diff mbox series

Patch

diff --git a/fs/cifs/Makefile b/fs/cifs/Makefile
index 85817991ee68..51af69a1a328 100644
--- a/fs/cifs/Makefile
+++ b/fs/cifs/Makefile
@@ -17,7 +17,7 @@  cifs-$(CONFIG_CIFS_ACL) += cifsacl.o
 
 cifs-$(CONFIG_CIFS_UPCALL) += cifs_spnego.o
 
-cifs-$(CONFIG_CIFS_DFS_UPCALL) += dns_resolve.o cifs_dfs_ref.o
+cifs-$(CONFIG_CIFS_DFS_UPCALL) += dns_resolve.o cifs_dfs_ref.o dfs_cache.o
 
 cifs-$(CONFIG_CIFS_FSCACHE) += fscache.o cache.o
 
diff --git a/fs/cifs/cifs_debug.c b/fs/cifs/cifs_debug.c
index ba178b09de0b..593fb422d0f3 100644
--- a/fs/cifs/cifs_debug.c
+++ b/fs/cifs/cifs_debug.c
@@ -30,6 +30,9 @@ 
 #include "cifsproto.h"
 #include "cifs_debug.h"
 #include "cifsfs.h"
+#ifdef CONFIG_CIFS_DFS_UPCALL
+#include "dfs_cache.h"
+#endif
 #ifdef CONFIG_CIFS_SMB_DIRECT
 #include "smbdirect.h"
 #endif
@@ -629,6 +632,11 @@  cifs_proc_init(void)
 		    &cifs_security_flags_proc_fops);
 	proc_create("LookupCacheEnabled", 0644, proc_fs_cifs,
 		    &cifs_lookup_cache_proc_fops);
+
+#ifdef CONFIG_CIFS_DFS_UPCALL
+	proc_create("dfscache", 0644, proc_fs_cifs, &dfscache_proc_fops);
+#endif
+
 #ifdef CONFIG_CIFS_SMB_DIRECT
 	proc_create("rdma_readwrite_threshold", 0644, proc_fs_cifs,
 		&cifs_rdma_readwrite_threshold_proc_fops);
@@ -663,6 +671,10 @@  cifs_proc_clean(void)
 	remove_proc_entry("SecurityFlags", proc_fs_cifs);
 	remove_proc_entry("LinuxExtensionsEnabled", proc_fs_cifs);
 	remove_proc_entry("LookupCacheEnabled", proc_fs_cifs);
+
+#ifdef CONFIG_CIFS_DFS_UPCALL
+	remove_proc_entry("dfscache", proc_fs_cifs);
+#endif
 #ifdef CONFIG_CIFS_SMB_DIRECT
 	remove_proc_entry("rdma_readwrite_threshold", proc_fs_cifs);
 	remove_proc_entry("smbd_max_frmr_depth", proc_fs_cifs);
diff --git a/fs/cifs/cifsglob.h b/fs/cifs/cifsglob.h
index 9534f46e6ad2..bd183ec17066 100644
--- a/fs/cifs/cifsglob.h
+++ b/fs/cifs/cifsglob.h
@@ -1014,6 +1014,11 @@  struct cifs_tcon {
 	struct list_head pending_opens;	/* list of incomplete opens */
 	struct cached_fid crfid; /* Cached root fid */
 	/* BB add field for back pointer to sb struct(s)? */
+#ifdef CONFIG_CIFS_DFS_UPCALL
+	char *dfs_path;
+	int remap:2;
+	struct list_head ulist; /* cache update list */
+#endif
 };
 
 /*
diff --git a/fs/cifs/cifsproto.h b/fs/cifs/cifsproto.h
index f4dd2a3795dd..efa5e36b3762 100644
--- a/fs/cifs/cifsproto.h
+++ b/fs/cifs/cifsproto.h
@@ -527,6 +527,9 @@  extern int SMBencrypt(unsigned char *passwd, const unsigned char *c8,
 extern void
 cifs_cleanup_volume_info_contents(struct smb_vol *volume_info);
 
+extern struct TCP_Server_Info *
+cifs_find_tcp_session(struct smb_vol *vol);
+
 void cifs_readdata_release(struct kref *refcount);
 int cifs_async_readv(struct cifs_readdata *rdata);
 int cifs_readv_receive(struct TCP_Server_Info *server, struct mid_q_entry *mid);
diff --git a/fs/cifs/connect.c b/fs/cifs/connect.c
index 86649230b4f2..b4fed69add86 100644
--- a/fs/cifs/connect.c
+++ b/fs/cifs/connect.c
@@ -2294,7 +2294,7 @@  static int match_server(struct TCP_Server_Info *server, struct smb_vol *vol)
 	return 1;
 }
 
-static struct TCP_Server_Info *
+struct TCP_Server_Info *
 cifs_find_tcp_session(struct smb_vol *vol)
 {
 	struct TCP_Server_Info *server;
diff --git a/fs/cifs/dfs_cache.c b/fs/cifs/dfs_cache.c
new file mode 100644
index 000000000000..18da279e9354
--- /dev/null
+++ b/fs/cifs/dfs_cache.c
@@ -0,0 +1,1379 @@ 
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * DFS referral cache routines
+ *
+ * Copyright (c) 2018 Paulo Alcantara <palcantara@suse.de>
+ */
+
+#include <linux/rcupdate.h>
+#include <linux/rculist.h>
+#include <linux/jhash.h>
+#include <linux/ktime.h>
+#include <linux/slab.h>
+#include <linux/nls.h>
+#include <linux/workqueue.h>
+#include "cifsglob.h"
+#include "smb2pdu.h"
+#include "smb2proto.h"
+#include "cifsproto.h"
+#include "cifs_debug.h"
+#include "cifs_unicode.h"
+#include "smb2glob.h"
+
+#include "dfs_cache.h"
+
+#define DFS_CACHE_HTABLE_SIZE 32
+#define DFS_CACHE_MAX_ENTRIES 64
+
+#define IS_INTERLINK_SET(v) ((v) & (DFSREF_REFERRAL_SERVER | \
+				    DFSREF_STORAGE_SERVER))
+
+struct dfs_cache_tgt {
+	char *t_name;
+	struct list_head t_list;
+};
+
+struct dfs_cache_entry {
+	struct hlist_node ce_hlist;
+	const char *ce_path;
+	int ce_ttl;
+	int ce_srvtype;
+	int ce_flags;
+	struct timespec64 ce_etime;
+	int ce_path_consumed;
+	int ce_numtgts;
+	struct list_head ce_tlist;
+	struct dfs_cache_tgt *ce_tgthint;
+	struct rcu_head ce_rcu;
+};
+
+static struct kmem_cache *dfs_cache_slab __read_mostly;
+
+struct dfs_cache_vol_info {
+	char *vi_fullpath;
+	struct smb_vol vi_vol;
+	struct list_head vi_list;
+};
+
+struct dfs_cache {
+	struct mutex dc_lock;
+	struct nls_table *dc_nlsc;
+	struct list_head dc_vol_list;
+	int dc_ttl;
+	struct delayed_work dc_refresh;
+};
+
+static struct dfs_cache dfs_cache;
+
+/*
+ * Number of entries in the cache
+ */
+static size_t dfs_cache_count;
+
+static DEFINE_MUTEX(dfs_cache_list_lock);
+static struct hlist_head dfs_cache_htable[DFS_CACHE_HTABLE_SIZE];
+
+static void refresh_cache_worker(struct work_struct *work);
+
+static inline bool is_path_valid(const char *path)
+{
+	return path && (strchr(path + 1, '\\') || strchr(path + 1, '/'));
+}
+
+static inline int get_normalized_path(const char *path, char **npath)
+{
+	if (*path == '\\') {
+		*npath = (char *)path;
+	} else {
+		*npath = kstrndup(path, strlen(path), GFP_KERNEL);
+		if (!*npath)
+			return -ENOMEM;
+		convert_delimiter(*npath, '\\');
+	}
+	return 0;
+}
+
+static inline void free_normalized_path(const char *path, char *npath)
+{
+	if (path != npath)
+		kfree(npath);
+}
+
+static inline bool cache_entry_expired(const struct dfs_cache_entry *ce)
+{
+	struct timespec64 ts;
+
+	ts = current_kernel_time64();
+	return timespec64_compare(&ts, &ce->ce_etime) >= 0;
+}
+
+static inline void free_tgts(struct dfs_cache_entry *ce)
+{
+	struct dfs_cache_tgt *t, *n;
+
+	list_for_each_entry_safe(t, n, &ce->ce_tlist, t_list) {
+		list_del(&t->t_list);
+		kfree(t->t_name);
+		kfree(t);
+	}
+}
+
+static void free_cache_entry(struct rcu_head *rcu)
+{
+	struct dfs_cache_entry *ce = container_of(rcu, struct dfs_cache_entry,
+						  ce_rcu);
+	kmem_cache_free(dfs_cache_slab, ce);
+}
+
+static inline void flush_cache_ent(struct dfs_cache_entry *ce)
+{
+	if (hlist_unhashed(&ce->ce_hlist))
+		return;
+
+	hlist_del_init_rcu(&ce->ce_hlist);
+	kfree(ce->ce_path);
+	free_tgts(ce);
+	dfs_cache_count--;
+	call_rcu(&ce->ce_rcu, free_cache_entry);
+}
+
+static void flush_cache_ents(void)
+{
+	int i;
+
+	rcu_read_lock();
+	for (i = 0; i < DFS_CACHE_HTABLE_SIZE; i++) {
+		struct hlist_head *l = &dfs_cache_htable[i];
+		struct dfs_cache_entry *ce;
+
+		hlist_for_each_entry_rcu(ce, l, ce_hlist)
+			flush_cache_ent(ce);
+	}
+	rcu_read_unlock();
+}
+
+/*
+ * dfs cache /proc file
+ */
+static int dfscache_proc_show(struct seq_file *m, void *v)
+{
+	int bucket;
+	struct dfs_cache_entry *ce;
+	struct dfs_cache_tgt *t;
+
+	seq_puts(m, "DFS cache\n---------\n");
+
+	mutex_lock(&dfs_cache_list_lock);
+
+	rcu_read_lock();
+	hash_for_each_rcu(dfs_cache_htable, bucket, ce, ce_hlist) {
+		seq_printf(m,
+			   "cache entry: path=%s,type=%s,ttl=%d,etime=%ld,"
+			   "interlink=%s,path_consumed=%d,expired=%s\n",
+			   ce->ce_path,
+			   ce->ce_srvtype == DFS_TYPE_ROOT ? "root" : "link",
+			   ce->ce_ttl, ce->ce_etime.tv_nsec,
+			   IS_INTERLINK_SET(ce->ce_flags) ? "yes" : "no",
+			   ce->ce_path_consumed,
+			   cache_entry_expired(ce) ? "yes" : "no");
+
+		list_for_each_entry(t, &ce->ce_tlist, t_list) {
+			seq_printf(m, "  %s%s\n",
+				   t->t_name,
+				   ce->ce_tgthint == t ? " (target hint)" : "");
+		}
+
+	}
+	rcu_read_unlock();
+
+	mutex_unlock(&dfs_cache_list_lock);
+	return 0;
+}
+
+static ssize_t dfscache_proc_write(struct file *file, const char __user *buffer,
+				   size_t count, loff_t *ppos)
+{
+	char c;
+	int rc;
+
+	rc = get_user(c, buffer);
+	if (rc)
+		return rc;
+
+	if (c != '0')
+		return -EINVAL;
+
+	cifs_dbg(FYI, "clearing dfs cache");
+	mutex_lock(&dfs_cache_list_lock);
+	flush_cache_ents();
+	mutex_unlock(&dfs_cache_list_lock);
+
+	return count;
+}
+
+static int dfscache_proc_open(struct inode *inode, struct file *file)
+{
+	return single_open(file, dfscache_proc_show, NULL);
+}
+
+const struct file_operations dfscache_proc_fops = {
+	.open		= dfscache_proc_open,
+	.read		= seq_read,
+	.llseek		= seq_lseek,
+	.release	= single_release,
+	.write		= dfscache_proc_write,
+};
+
+#ifdef CONFIG_CIFS_DEBUG2
+static inline void dump_tgts(const struct dfs_cache_entry *ce)
+{
+	struct dfs_cache_tgt *t;
+
+	cifs_dbg(FYI, "target list:\n");
+	list_for_each_entry(t, &ce->ce_tlist, t_list) {
+		cifs_dbg(FYI, "  %s%s\n", t->t_name,
+			 ce->ce_tgthint == t ? " (target hint)" : "");
+	}
+}
+
+static inline void dump_ce(const struct dfs_cache_entry *ce)
+{
+	cifs_dbg(FYI, "cache entry: path=%s,type=%s,ttl=%d,etime=%ld,"
+		 "interlink=%s,path_consumed=%d,expired=%s\n", ce->ce_path,
+		 ce->ce_srvtype == DFS_TYPE_ROOT ? "root" : "link", ce->ce_ttl,
+		 ce->ce_etime.tv_nsec,
+		 IS_INTERLINK_SET(ce->ce_flags) ? "yes" : "no",
+		 ce->ce_path_consumed,
+		 cache_entry_expired(ce) ? "yes" : "no");
+	dump_tgts(ce);
+}
+
+static inline void dump_refs(const struct dfs_info3_param *refs, int numrefs)
+{
+	int i;
+
+	cifs_dbg(FYI, "DFS referrals returned by the server:\n");
+	for (i = 0; i < numrefs; i++) {
+		const struct dfs_info3_param *ref = &refs[i];
+
+		cifs_dbg(FYI,
+			 "\n"
+			 "flags:         0x%x\n"
+			 "path_consumed: %d\n"
+			 "server_type:   0x%x\n"
+			 "ref_flag:      0x%x\n"
+			 "path_name:     %s\n"
+			 "node_name:     %s\n"
+			 "ttl:           %d (%dm)\n",
+			 ref->flags, ref->path_consumed, ref->server_type,
+			 ref->ref_flag, ref->path_name, ref->node_name,
+			 ref->ttl, ref->ttl / 60);
+	}
+}
+#else
+#define dump_tgts(e)
+#define dump_ce(e)
+#define dump_refs(r, n)
+#endif
+
+/**
+ * dfs_cache_init - Initialize DFS referral cache.
+ *
+ * Return zero if initialized successfully, otherwise non-zero.
+ */
+int dfs_cache_init(void)
+{
+	int i;
+
+	dfs_cache_slab = kmem_cache_create("cifs_dfs_cache",
+					   sizeof(struct dfs_cache_entry), 0,
+					   SLAB_HWCACHE_ALIGN, NULL);
+	if (!dfs_cache_slab)
+		return -ENOMEM;
+
+	for (i = 0; i < DFS_CACHE_HTABLE_SIZE; i++)
+		INIT_HLIST_HEAD(&dfs_cache_htable[i]);
+
+	INIT_LIST_HEAD(&dfs_cache.dc_vol_list);
+	mutex_init(&dfs_cache.dc_lock);
+	INIT_DELAYED_WORK(&dfs_cache.dc_refresh, refresh_cache_worker);
+	dfs_cache.dc_ttl = -1;
+	dfs_cache.dc_nlsc = load_nls_default();
+
+	cifs_dbg(FYI, "%s: initialized DFS referral cache\n", __func__);
+	return 0;
+}
+
+static inline unsigned int cache_entry_hash(const void *data, int size)
+{
+	unsigned int h;
+
+	h = jhash(data, size, 0);
+	return h & (DFS_CACHE_HTABLE_SIZE - 1);
+}
+
+/* Check whether second path component of @path is SYSVOL or NETLOGON */
+static inline bool is_sysvol_or_netlogon(const char *path)
+{
+	const char *s;
+	char sep = path[0];
+
+	s = strchr(path + 1, sep) + 1;
+	return !strncasecmp(s, "sysvol", strlen("sysvol")) ||
+		!strncasecmp(s, "netlogon", strlen("netlogon"));
+}
+
+/* Return target hint of a DFS cache entry */
+static inline char *get_tgt_name(const struct dfs_cache_entry *ce)
+{
+	struct dfs_cache_tgt *t = ce->ce_tgthint;
+
+	return t ? t->t_name : ERR_PTR(-ENOENT);
+}
+
+/* Return expire time out of a new entry's TTL */
+static inline struct timespec64 get_expire_time(int ttl)
+{
+	struct timespec64 ts = {
+		.tv_sec = ttl,
+		.tv_nsec = 0,
+	};
+
+	return timespec64_add(current_kernel_time64(), ts);
+}
+
+/* Allocate a new DFS target */
+static inline struct dfs_cache_tgt *alloc_tgt(const char *name)
+{
+	struct dfs_cache_tgt *t;
+
+	t = kmalloc(sizeof(*t), GFP_KERNEL);
+	if (!t)
+		return ERR_PTR(-ENOMEM);
+	t->t_name = kstrndup(name, strlen(name), GFP_KERNEL);
+	if (!t->t_name) {
+		kfree(t);
+		return ERR_PTR(-ENOMEM);
+	}
+	INIT_LIST_HEAD(&t->t_list);
+	return t;
+}
+
+/*
+ * Copy DFS referral information to a cache entry and conditionally update
+ * target hint.
+ */
+static int copy_ref_data(const struct dfs_info3_param *refs, int numrefs,
+			 struct dfs_cache_entry *ce, const char *tgthint)
+{
+	int i;
+
+	ce->ce_ttl = refs[0].ttl;
+	ce->ce_etime = get_expire_time(ce->ce_ttl);
+	ce->ce_srvtype = refs[0].server_type;
+	ce->ce_flags = refs[0].ref_flag;
+	ce->ce_path_consumed = refs[0].path_consumed;
+
+	for (i = 0; i < numrefs; i++) {
+		struct dfs_cache_tgt *t;
+
+		t = alloc_tgt(refs[i].node_name);
+		if (IS_ERR(t)) {
+			free_tgts(ce);
+			return PTR_ERR(t);
+		}
+		if (tgthint && !strncasecmp(t->t_name, tgthint,
+					    strlen(tgthint))) {
+			list_add(&t->t_list, &ce->ce_tlist);
+			tgthint = NULL;
+		} else {
+			list_add_tail(&t->t_list, &ce->ce_tlist);
+		}
+		ce->ce_numtgts++;
+	}
+
+	ce->ce_tgthint = list_first_entry_or_null(&ce->ce_tlist,
+						  struct dfs_cache_tgt, t_list);
+
+	return 0;
+}
+
+/* Allocate a new cache entry */
+static struct dfs_cache_entry *
+alloc_cache_entry(const char *path, const struct dfs_info3_param *refs,
+		  int numrefs)
+{
+	struct dfs_cache_entry *ce;
+	int rc;
+
+	ce = kmem_cache_zalloc(dfs_cache_slab, GFP_KERNEL);
+	if (!ce)
+		return ERR_PTR(-ENOMEM);
+
+	ce->ce_path = kstrdup_const(path, GFP_KERNEL);
+	if (!ce->ce_path) {
+		kfree(ce);
+		return ERR_PTR(-ENOMEM);
+	}
+	INIT_HLIST_NODE(&ce->ce_hlist);
+	INIT_LIST_HEAD(&ce->ce_tlist);
+
+	rc = copy_ref_data(refs, numrefs, ce, NULL);
+	if (rc) {
+		kfree(ce->ce_path);
+		kfree(ce);
+		ce = ERR_PTR(rc);
+	}
+	return ce;
+}
+
+static void remove_oldest_entry(void)
+{
+	int bucket;
+	struct dfs_cache_entry *ce;
+	struct dfs_cache_entry *to_del = NULL;
+
+	rcu_read_lock();
+	hash_for_each_rcu(dfs_cache_htable, bucket, ce, ce_hlist) {
+		if (!to_del || timespec64_compare(&ce->ce_etime,
+						  &to_del->ce_etime) < 0)
+			to_del = ce;
+	}
+	if (!to_del) {
+		cifs_dbg(FYI, "%s: no entry to remove", __func__);
+		goto out;
+	}
+	cifs_dbg(FYI, "%s: removing entry", __func__);
+	dump_ce(to_del);
+	flush_cache_ent(to_del);
+out:
+	rcu_read_unlock();
+}
+
+/* Add a new DFS cache entry */
+static inline struct dfs_cache_entry *
+add_cache_entry(unsigned int hash, const char *path,
+		const struct dfs_info3_param *refs, int numrefs)
+{
+	struct dfs_cache_entry *ce;
+
+	ce = alloc_cache_entry(path, refs, numrefs);
+	if (IS_ERR(ce))
+		return ce;
+
+	hlist_add_head_rcu(&ce->ce_hlist, &dfs_cache_htable[hash]);
+
+	mutex_lock(&dfs_cache.dc_lock);
+	if (dfs_cache.dc_ttl < 0) {
+		dfs_cache.dc_ttl = ce->ce_ttl;
+		queue_delayed_work(cifsiod_wq, &dfs_cache.dc_refresh,
+				   dfs_cache.dc_ttl * HZ);
+	} else {
+		dfs_cache.dc_ttl = min_t(int, dfs_cache.dc_ttl, ce->ce_ttl);
+		mod_delayed_work(cifsiod_wq, &dfs_cache.dc_refresh,
+				 dfs_cache.dc_ttl * HZ);
+	}
+	mutex_unlock(&dfs_cache.dc_lock);
+
+	return ce;
+}
+
+static struct dfs_cache_entry *__find_cache_entry(unsigned int hash,
+						  const char *path, int len)
+{
+	struct dfs_cache_entry *ce;
+	bool found = false;
+
+	rcu_read_lock();
+	hlist_for_each_entry_rcu(ce, &dfs_cache_htable[hash], ce_hlist) {
+		if (!strncasecmp(ce->ce_path, path, len)) {
+#ifdef CONFIG_CIFS_DEBUG2
+			char *name = get_tgt_name(ce);
+
+			if (unlikely(IS_ERR(name))) {
+				rcu_read_unlock();
+				return ERR_CAST(name);
+			}
+			cifs_dbg(FYI, "%s: cache hit\n", __func__);
+			cifs_dbg(FYI, "%s: target hint: %s\n", __func__, name);
+#endif
+			found = true;
+			break;
+		}
+	}
+	rcu_read_unlock();
+	return found ? ce : ERR_PTR(-ENOENT);
+}
+
+/*
+ * Find a DFS cache entry in hash table and optionally check prefix path against
+ * @path.
+ * Use whole path components in the match.
+ * Return ERR_PTR(-ENOENT) if the entry is not found.
+ */
+static inline struct dfs_cache_entry *find_cache_entry(const char *path,
+						       unsigned int *hash)
+{
+	int len = strlen(path);
+
+	*hash = cache_entry_hash(path, len);
+	return __find_cache_entry(*hash, path, len);
+}
+
+static inline void destroy_slab_cache(void)
+{
+	rcu_barrier();
+	kmem_cache_destroy(dfs_cache_slab);
+}
+
+static inline void free_vol(struct dfs_cache_vol_info *vi)
+{
+	list_del(&vi->vi_list);
+	kfree(vi->vi_fullpath);
+	cifs_cleanup_volume_info_contents(&vi->vi_vol);
+	kfree(vi);
+}
+
+static inline void free_vol_list(void)
+{
+	struct dfs_cache_vol_info *vi, *nvi;
+
+	list_for_each_entry_safe(vi, nvi, &dfs_cache.dc_vol_list, vi_list)
+		free_vol(vi);
+}
+
+/**
+ * dfs_cache_destroy - destroy DFS referral cache
+ */
+void dfs_cache_destroy(void)
+{
+	cancel_delayed_work_sync(&dfs_cache.dc_refresh);
+	unload_nls(dfs_cache.dc_nlsc);
+	free_vol_list();
+	mutex_destroy(&dfs_cache.dc_lock);
+
+	flush_cache_ents();
+	destroy_slab_cache();
+	mutex_destroy(&dfs_cache_list_lock);
+
+	cifs_dbg(FYI, "%s: destroyed DFS referral cache\n", __func__);
+}
+
+static inline struct dfs_cache_entry *
+__update_cache_entry(const char *path, const struct dfs_info3_param *refs,
+		     int numrefs)
+{
+	int rc;
+	unsigned int h;
+	struct dfs_cache_entry *ce;
+	char *s, *th = NULL;
+
+	ce = find_cache_entry(path, &h);
+	if (IS_ERR(ce))
+		return ce;
+
+	if (ce->ce_tgthint) {
+		s = ce->ce_tgthint->t_name;
+		th = kstrndup(s, strlen(s), GFP_KERNEL);
+		if (!th)
+			return ERR_PTR(-ENOMEM);
+	}
+
+	free_tgts(ce);
+	ce->ce_numtgts = 0;
+
+	rc = copy_ref_data(refs, numrefs, ce, th);
+	kfree(th);
+
+	if (rc)
+		ce = ERR_PTR(rc);
+
+	return ce;
+}
+
+/* Update an expired cache entry by getting a new DFS referral from server */
+static struct dfs_cache_entry *
+update_cache_entry(const unsigned int xid, struct cifs_ses *ses,
+		   const struct nls_table *nls_codepage, int remap,
+		   const char *path, struct dfs_cache_entry *ce)
+{
+	int rc;
+	struct dfs_info3_param *refs = NULL;
+	int numrefs = 0;
+
+	cifs_dbg(FYI, "%s: update expired cache entry\n", __func__);
+	/*
+	 * Check if caller provided enough parameters to update an expired
+	 * entry.
+	 */
+	if (!ses || !ses->server || !ses->server->ops->get_dfs_refer)
+		return ERR_PTR(-ETIME);
+	if (unlikely(!nls_codepage))
+		return ERR_PTR(-ETIME);
+
+	cifs_dbg(FYI, "%s: DFS referral request for %s\n", __func__, path);
+
+	rc = ses->server->ops->get_dfs_refer(xid, ses, path, &refs, &numrefs,
+					     nls_codepage, remap);
+	if (rc)
+		ce = ERR_PTR(rc);
+	else
+		ce = __update_cache_entry(path, refs, numrefs);
+
+	dump_refs(refs, numrefs);
+	free_dfs_info_array(refs, numrefs);
+
+	return ce;
+}
+
+/*
+ * Find, create or update a DFS cache entry.
+ *
+ * If the entry wasn't found, it will create a new one. Or if it was found but
+ * expired, then it will update the entry accordingly.
+ *
+ * For interlinks, __cifs_dfs_mount() and expand_dfs_referral() are supposed to
+ * handle them properly.
+ */
+static struct dfs_cache_entry *
+do_dfs_cache_find(const unsigned int xid, struct cifs_ses *ses,
+		  const struct nls_table *nls_codepage, int remap,
+		  const char *path, bool noreq)
+{
+	int rc;
+	unsigned int h;
+	struct dfs_cache_entry *ce;
+	struct dfs_info3_param *nrefs;
+	int numnrefs;
+
+	cifs_dbg(FYI, "%s: search path: %s\n", __func__, path);
+
+	ce = find_cache_entry(path, &h);
+	if (IS_ERR(ce)) {
+		cifs_dbg(FYI, "%s: cache miss\n", __func__);
+		/*
+		 * If @noreq is set, no requests will be sent to the server for
+		 * either updating or getting a new DFS referral.
+		 */
+		if (noreq)
+			return ce;
+		/*
+		 * No cache entry was found, so check for valid parameters that
+		 * will be required to get a new DFS referral and then create a
+		 * new cache entry.
+		 */
+		if (!ses || !ses->server || !ses->server->ops->get_dfs_refer) {
+			ce = ERR_PTR(-ENOSYS);
+			return ce;
+		}
+		if (unlikely(!nls_codepage)) {
+			ce = ERR_PTR(-EINVAL);
+			return ce;
+		}
+
+		nrefs = NULL;
+		numnrefs = 0;
+
+		cifs_dbg(FYI, "%s: DFS referral request for %s\n", __func__,
+			 path);
+
+		rc = ses->server->ops->get_dfs_refer(xid, ses, path, &nrefs,
+						     &numnrefs, nls_codepage,
+						     remap);
+		if (rc) {
+			ce = ERR_PTR(rc);
+			return ce;
+		}
+
+		dump_refs(nrefs, numnrefs);
+
+		cifs_dbg(FYI, "%s: new cache entry\n", __func__);
+
+		if (dfs_cache_count >= DFS_CACHE_MAX_ENTRIES) {
+			cifs_dbg(FYI, "%s: reached max cache size (%d)",
+				 __func__, DFS_CACHE_MAX_ENTRIES);
+			remove_oldest_entry();
+		}
+		ce = add_cache_entry(h, path, nrefs, numnrefs);
+		free_dfs_info_array(nrefs, numnrefs);
+
+		if (IS_ERR(ce))
+			return ce;
+
+		dfs_cache_count++;
+	}
+
+	dump_ce(ce);
+
+	/* Just return the found cache entry in case @noreq is set */
+	if (noreq)
+		return ce;
+
+	if (cache_entry_expired(ce)) {
+		cifs_dbg(FYI, "%s: expired cache entry\n", __func__);
+		ce = update_cache_entry(xid, ses, nls_codepage, remap, path,
+					ce);
+		if (IS_ERR(ce)) {
+			cifs_dbg(FYI, "%s: failed to update expired entry\n",
+				 __func__);
+		}
+	}
+	return ce;
+}
+
+/* Set up a new DFS referral from a given cache entry */
+static int setup_ref(const char *path, const struct dfs_cache_entry *ce,
+		     struct dfs_info3_param *ref, const char *tgt)
+{
+	int rc;
+
+	cifs_dbg(FYI, "%s: set up new ref\n", __func__);
+
+	memset(ref, 0, sizeof(*ref));
+
+	ref->path_name = kstrndup(path, strlen(path), GFP_KERNEL);
+	if (!ref->path_name)
+		return -ENOMEM;
+
+	ref->path_consumed = ce->ce_path_consumed;
+
+	ref->node_name = kstrndup(tgt, strlen(tgt), GFP_KERNEL);
+	if (!ref->node_name) {
+		rc = -ENOMEM;
+		goto err_free_path;
+	}
+
+	ref->ttl = ce->ce_ttl;
+	ref->server_type = ce->ce_srvtype;
+	ref->ref_flag = ce->ce_flags;
+
+	return 0;
+
+err_free_path:
+	kfree(ref->path_name);
+	ref->path_name = NULL;
+	return rc;
+}
+
+/* Return target list of a DFS cache entry */
+static int get_tgt_list(const struct dfs_cache_entry *ce,
+			struct dfs_cache_tgt_list *tl)
+{
+	int rc;
+	struct list_head *head = &tl->tl_list;
+	struct dfs_cache_tgt *t;
+	struct dfs_cache_tgt_iterator *it, *nit;
+
+	memset(tl, 0, sizeof(*tl));
+	INIT_LIST_HEAD(head);
+
+	list_for_each_entry(t, &ce->ce_tlist, t_list) {
+		it = kzalloc(sizeof(*it), GFP_KERNEL);
+		if (!it) {
+			rc = -ENOMEM;
+			goto err_free_it;
+		}
+
+		it->it_name = kstrndup(t->t_name, strlen(t->t_name),
+				       GFP_KERNEL);
+		if (!it->it_name) {
+			rc = -ENOMEM;
+			goto err_free_it;
+		}
+
+		if (ce->ce_tgthint == t)
+			list_add(&it->it_list, head);
+		else
+			list_add_tail(&it->it_list, head);
+	}
+	tl->tl_numtgts = ce->ce_numtgts;
+
+	return 0;
+
+err_free_it:
+	list_for_each_entry_safe(it, nit, head, it_list) {
+		kfree(it->it_name);
+		kfree(it);
+	}
+	return rc;
+}
+
+/**
+ * dfs_cache_find - find a DFS cache entry
+ *
+ * If it doesn't find the cache entry, then it will get a DFS referral
+ * for @path and create a new entry.
+ *
+ * In case the cache entry exists but expired, it will get a DFS referral
+ * for @path and then update the respective cache entry.
+ *
+ * These parameters are passed down to the get_dfs_refer() call if it
+ * needs to be issued:
+ * @xid: syscall xid
+ * @ses: smb session to issue the request on
+ * @nls_codepage: charset conversion
+ * @remap: path character remapping type
+ * @path: path to lookup in DFS referral cache.
+ *
+ * @ref: when non-NULL, store single DFS referral result in it.
+ * @tgt_list: when non-NULL, store complete DFS target list in it.
+ *
+ * Return zero if the target was found, otherwise non-zero.
+ */
+int dfs_cache_find(const unsigned int xid, struct cifs_ses *ses,
+		   const struct nls_table *nls_codepage, int remap,
+		   const char *path, struct dfs_info3_param *ref,
+		   struct dfs_cache_tgt_list *tgt_list)
+{
+	int rc;
+	char *npath;
+	struct dfs_cache_entry *ce;
+
+	if (unlikely(!is_path_valid(path)))
+		return -EINVAL;
+
+	rc = get_normalized_path(path, &npath);
+	if (rc)
+		return rc;
+
+	mutex_lock(&dfs_cache_list_lock);
+	ce = do_dfs_cache_find(xid, ses, nls_codepage, remap, npath, false);
+	if (!IS_ERR(ce)) {
+		if (ref)
+			rc = setup_ref(path, ce, ref, get_tgt_name(ce));
+		else
+			rc = 0;
+		if (!rc && tgt_list)
+			rc = get_tgt_list(ce, tgt_list);
+	} else {
+		rc = PTR_ERR(ce);
+	}
+	mutex_unlock(&dfs_cache_list_lock);
+	free_normalized_path(path, npath);
+	return rc;
+}
+
+/**
+ * dfs_cache_noreq_find - find a DFS cache entry without sending any requests to
+ * the currently connected server.
+ *
+ * NOTE: This function will neither update a cache entry in case it was
+ * expired, nor create a new cache entry if @path hasn't been found. It heavily
+ * relies on an existing cache entry.
+ *
+ * @path: path to lookup in the DFS referral cache.
+ * @ref: when non-NULL, store single DFS referral result in it.
+ * @tgt_list: when non-NULL, store complete DFS target list in it.
+ *
+ * Return 0 if successful.
+ * Return -ENOENT if the entry was not found.
+ * Return non-zero for other errors.
+ */
+int dfs_cache_noreq_find(const char *path, struct dfs_info3_param *ref,
+			 struct dfs_cache_tgt_list *tgt_list)
+{
+	int rc;
+	char *npath;
+	struct dfs_cache_entry *ce;
+
+	if (unlikely(!is_path_valid(path)))
+		return -EINVAL;
+
+	rc = get_normalized_path(path, &npath);
+	if (rc)
+		return rc;
+
+	mutex_lock(&dfs_cache_list_lock);
+	ce = do_dfs_cache_find(0, NULL, NULL, 0, npath, true);
+	if (IS_ERR(ce)) {
+		rc = PTR_ERR(ce);
+		goto out;
+	}
+
+	if (ref)
+		rc = setup_ref(path, ce, ref, get_tgt_name(ce));
+	else
+		rc = 0;
+	if (!rc && tgt_list)
+		rc = get_tgt_list(ce, tgt_list);
+out:
+	mutex_unlock(&dfs_cache_list_lock);
+	free_normalized_path(path, npath);
+	return rc;
+}
+
+/**
+ * dfs_cache_update_tgthint - update target hint of a DFS cache entry
+ *
+ * If it doesn't find the cache entry, then it will get a DFS referral for @path
+ * and create a new entry.
+ *
+ * In case the cache entry exists but expired, it will get a DFS referral
+ * for @path and then update the respective cache entry.
+ *
+ * @xid: syscall id
+ * @ses: smb session
+ * @nls_codepage: charset conversion
+ * @remap: type of character remapping for paths
+ * @path: path to lookup in DFS referral cache.
+ * @it: DFS target iterator
+ *
+ * Return zero if the target hint was updated successfully, otherwise non-zero.
+ */
+int dfs_cache_update_tgthint(const unsigned int xid, struct cifs_ses *ses,
+			     const struct nls_table *nls_codepage, int remap,
+			     const char *path,
+			     const struct dfs_cache_tgt_iterator *it)
+{
+	int rc;
+	char *npath;
+	struct dfs_cache_entry *ce;
+	struct dfs_cache_tgt *t;
+
+	if (unlikely(!is_path_valid(path)))
+		return -EINVAL;
+
+	rc = get_normalized_path(path, &npath);
+	if (rc)
+		return rc;
+
+	cifs_dbg(FYI, "%s: path: %s\n", __func__, npath);
+
+	mutex_lock(&dfs_cache_list_lock);
+	ce = do_dfs_cache_find(xid, ses, nls_codepage, remap, npath, false);
+	if (IS_ERR(ce)) {
+		rc = PTR_ERR(ce);
+		goto out;
+	}
+
+	rc = 0;
+
+	t = ce->ce_tgthint;
+
+	if (likely(!strncasecmp(it->it_name, t->t_name, strlen(t->t_name))))
+		goto out;
+
+	list_for_each_entry(t, &ce->ce_tlist, t_list) {
+		if (!strncasecmp(t->t_name, it->it_name, strlen(it->it_name))) {
+			ce->ce_tgthint = t;
+			cifs_dbg(FYI, "%s: new target hint: %s\n", __func__,
+				 it->it_name);
+			break;
+		}
+	}
+
+out:
+	mutex_unlock(&dfs_cache_list_lock);
+	free_normalized_path(path, npath);
+	return rc;
+}
+
+/**
+ * dfs_cache_noreq_update_tgthint - update target hint of a DFS cache entry
+ * without sending any requests to the currently connected server.
+ *
+ * NOTE: This function will neither update a cache entry in case it was
+ * expired, nor create a new cache entry if @path hasn't been found. It heavily
+ * relies on an existing cache entry.
+ *
+ * @path: path to lookup in DFS referral cache.
+ * @it: target iterator which contains the target hint to update the cache
+ * entry with.
+ *
+ * Return zero if the target hint was updated successfully, otherwise non-zero.
+ */
+int dfs_cache_noreq_update_tgthint(const char *path,
+				   const struct dfs_cache_tgt_iterator *it)
+{
+	int rc;
+	char *npath;
+	struct dfs_cache_entry *ce;
+	struct dfs_cache_tgt *t;
+
+	if (unlikely(!is_path_valid(path)) || !it)
+		return -EINVAL;
+
+	rc = get_normalized_path(path, &npath);
+	if (rc)
+		return rc;
+
+	cifs_dbg(FYI, "%s: path: %s\n", __func__, npath);
+
+	mutex_lock(&dfs_cache_list_lock);
+
+	ce = do_dfs_cache_find(0, NULL, NULL, 0, npath, true);
+	if (IS_ERR(ce)) {
+		rc = PTR_ERR(ce);
+		goto out;
+	}
+
+	rc = 0;
+
+	t = ce->ce_tgthint;
+
+	if (unlikely(!strncasecmp(it->it_name, t->t_name, strlen(t->t_name))))
+		goto out;
+
+	list_for_each_entry(t, &ce->ce_tlist, t_list) {
+		if (!strncasecmp(t->t_name, it->it_name, strlen(it->it_name))) {
+			ce->ce_tgthint = t;
+			cifs_dbg(FYI, "%s: new target hint: %s\n", __func__,
+				 it->it_name);
+			break;
+		}
+	}
+
+out:
+	mutex_unlock(&dfs_cache_list_lock);
+	free_normalized_path(path, npath);
+	return rc;
+}
+
+/**
+ * dfs_cache_get_tgt_referral - returns a DFS referral (@ref) from a given
+ * target iterator (@it).
+ *
+ * @path: path to lookup in DFS referral cache.
+ * @it: DFS target iterator.
+ * @ref: DFS referral pointer to set up the gathered information.
+ *
+ * Return zero if the DFS referral was set up correctly, otherwise non-zero.
+ */
+int dfs_cache_get_tgt_referral(const char *path,
+			       const struct dfs_cache_tgt_iterator *it,
+			       struct dfs_info3_param *ref)
+{
+	int rc;
+	char *npath;
+	struct dfs_cache_entry *ce;
+	unsigned int h;
+
+	if (!it || !ref)
+		return -EINVAL;
+	if (unlikely(!is_path_valid(path)))
+		return -EINVAL;
+
+	rc = get_normalized_path(path, &npath);
+	if (rc)
+		return rc;
+
+	cifs_dbg(FYI, "%s: path: %s\n", __func__, npath);
+
+	mutex_lock(&dfs_cache_list_lock);
+
+	ce = find_cache_entry(npath, &h);
+	if (IS_ERR(ce)) {
+		rc = PTR_ERR(ce);
+		goto out;
+	}
+
+	cifs_dbg(FYI, "%s: target name: %s\n", __func__, it->it_name);
+
+	rc = setup_ref(path, ce, ref, it->it_name);
+
+out:
+	mutex_unlock(&dfs_cache_list_lock);
+	free_normalized_path(path, npath);
+	return rc;
+}
+
+static int dup_vol(struct smb_vol *vol, struct smb_vol *new)
+{
+	memcpy(new, vol, sizeof(*new));
+
+	if (vol->username) {
+		new->username = kstrndup(vol->username, strlen(vol->username),
+					GFP_KERNEL);
+		if (!new->username)
+			return -ENOMEM;
+	}
+	if (vol->password) {
+		new->password = kstrndup(vol->password, strlen(vol->password),
+					 GFP_KERNEL);
+		if (!new->password)
+			goto err_free_username;
+	}
+	if (vol->UNC) {
+		cifs_dbg(FYI, "%s: vol->UNC: %s\n", __func__, vol->UNC);
+		new->UNC = kstrndup(vol->UNC, strlen(vol->UNC), GFP_KERNEL);
+		if (!new->UNC)
+			goto err_free_password;
+	}
+	if (vol->domainname) {
+		new->domainname = kstrndup(vol->domainname,
+					  strlen(vol->domainname), GFP_KERNEL);
+		if (!new->domainname)
+			goto err_free_unc;
+	}
+	if (vol->iocharset) {
+		new->iocharset = kstrndup(vol->iocharset,
+					  strlen(vol->iocharset), GFP_KERNEL);
+		if (!new->iocharset)
+			goto err_free_domainname;
+	}
+	if (vol->prepath) {
+		cifs_dbg(FYI, "%s: vol->prepath: %s\n", __func__, vol->prepath);
+		new->prepath = kstrndup(vol->prepath, strlen(vol->prepath),
+					GFP_KERNEL);
+		if (!new->prepath)
+			goto err_free_iocharset;
+	}
+
+	return 0;
+
+err_free_iocharset:
+	kfree(new->iocharset);
+err_free_domainname:
+	kfree(new->domainname);
+err_free_unc:
+	kfree(new->UNC);
+err_free_password:
+	kfree(new->password);
+err_free_username:
+	kfree(new->username);
+	kfree(new);
+	return -ENOMEM;
+}
+
+/**
+ * dfs_cache_add_vol - add a cifs volume during mount() that will be handled by
+ * DFS cache refresh worker.
+ *
+ * @vol: cifs volume.
+ * @fullpath: origin full path.
+ *
+ * Return zero if volume was set up correctly, otherwise non-zero.
+ */
+int dfs_cache_add_vol(struct smb_vol *vol, const char *fullpath)
+{
+	int rc;
+	struct dfs_cache_vol_info *vi;
+
+	if (!vol || !fullpath)
+		return -EINVAL;
+
+	cifs_dbg(FYI, "%s: fullpath: %s\n", __func__, fullpath);
+
+	vi = kzalloc(sizeof(*vi), GFP_KERNEL);
+	if (!vi)
+		return -ENOMEM;
+
+	vi->vi_fullpath = kstrndup(fullpath, strlen(fullpath), GFP_KERNEL);
+	if (!vi->vi_fullpath) {
+		rc = -ENOMEM;
+		goto err_free_vi;
+	}
+
+	rc = dup_vol(vol, &vi->vi_vol);
+	if (rc)
+		goto err_free_fullpath;
+
+	mutex_lock(&dfs_cache.dc_lock);
+	list_add_tail(&vi->vi_list, &dfs_cache.dc_vol_list);
+	mutex_unlock(&dfs_cache.dc_lock);
+	return 0;
+
+err_free_fullpath:
+	kfree(vi->vi_fullpath);
+err_free_vi:
+	kfree(vi);
+	return rc;
+}
+
+static inline struct dfs_cache_vol_info *find_vol(const char *fullpath)
+{
+	struct dfs_cache_vol_info *vi;
+
+	list_for_each_entry(vi, &dfs_cache.dc_vol_list, vi_list) {
+		cifs_dbg(FYI, "%s: vi->vi_fullpath: %s\n", __func__,
+			 vi->vi_fullpath);
+		if (!strncasecmp(vi->vi_fullpath, fullpath,
+				 strlen(vi->vi_fullpath)))
+			return vi;
+	}
+	return ERR_PTR(-ENOENT);
+}
+
+/**
+ * dfs_cache_update_vol - update vol info in DFS cache after failover
+ *
+ * @fullpath: fullpath to look up in volume list.
+ * @server: TCP ses pointer.
+ *
+ * Return zero if volume was updated, otherwise non-zero.
+ */
+int dfs_cache_update_vol(const char *fullpath, struct TCP_Server_Info *server)
+{
+	int rc;
+	struct dfs_cache_vol_info *vi;
+
+	if (!fullpath || !server)
+		return -EINVAL;
+
+	cifs_dbg(FYI, "%s: fullpath: %s\n", __func__, fullpath);
+
+	mutex_lock(&dfs_cache.dc_lock);
+
+	vi = find_vol(fullpath);
+	if (IS_ERR(vi)) {
+		rc = PTR_ERR(vi);
+		goto out;
+	}
+
+	cifs_dbg(FYI, "%s: updating volume info\n", __func__);
+	memcpy(&vi->vi_vol.dstaddr, &server->dstaddr,
+	       sizeof(vi->vi_vol.dstaddr));
+	rc = 0;
+
+out:
+	mutex_unlock(&dfs_cache.dc_lock);
+	return rc;
+}
+
+/**
+ * dfs_cache_del_vol - remove volume info in DFS cache during umount()
+ *
+ * @fullpath: fullpath to look up in volume list.
+ *
+ * Return zero if volume was deleted, otherwise non-zero.
+ */
+void dfs_cache_del_vol(const char *fullpath)
+{
+	struct dfs_cache_vol_info *vi;
+
+	if (!fullpath || !*fullpath)
+		return;
+
+	cifs_dbg(FYI, "%s: fullpath: %s\n", __func__, fullpath);
+
+	mutex_lock(&dfs_cache.dc_lock);
+	vi = find_vol(fullpath);
+	if (!IS_ERR(vi))
+		free_vol(vi);
+	mutex_unlock(&dfs_cache.dc_lock);
+}
+
+/* Get all tcons that are within a DFS namespace and can be refreshed */
+static void get_tcons(struct TCP_Server_Info *server, struct list_head *head)
+{
+	struct cifs_ses *ses;
+	struct cifs_tcon *tcon;
+
+	INIT_LIST_HEAD(head);
+
+	spin_lock(&cifs_tcp_ses_lock);
+	list_for_each_entry(ses, &server->smb_ses_list, smb_ses_list) {
+		list_for_each_entry(tcon, &ses->tcon_list, tcon_list) {
+			if (!tcon->need_reconnect && !tcon->need_reopen_files &&
+			    tcon->dfs_path) {
+				tcon->tc_count++;
+				list_add_tail(&tcon->ulist, head);
+			}
+		}
+		if (ses->tcon_ipc && !ses->tcon_ipc->need_reconnect &&
+		    ses->tcon_ipc->dfs_path) {
+			list_add_tail(&ses->tcon_ipc->ulist, head);
+		}
+	}
+	spin_unlock(&cifs_tcp_ses_lock);
+}
+
+/* Refresh DFS cache entry from a given tcon */
+static void do_refresh_tcon(struct dfs_cache *dc, struct cifs_tcon *tcon)
+{
+	int rc = 0;
+	unsigned int xid;
+	char *path, *npath;
+	unsigned int h;
+	struct dfs_cache_entry *ce;
+	struct dfs_info3_param *refs = NULL;
+	int numrefs = 0;
+
+	xid = get_xid();
+
+	path = tcon->dfs_path + 1;
+
+	cifs_dbg(FYI, "%s: dfs path: %s\n", __func__, path);
+
+	rc = get_normalized_path(path, &npath);
+	if (rc)
+		goto out;
+
+	mutex_lock(&dfs_cache_list_lock);
+	ce = find_cache_entry(npath, &h);
+	mutex_unlock(&dfs_cache_list_lock);
+
+	if (IS_ERR(ce)) {
+		rc = PTR_ERR(ce);
+		goto out;
+	}
+
+	if (!cache_entry_expired(ce))
+		goto out;
+
+	if (unlikely(!tcon->ses->server->ops->get_dfs_refer)) {
+		rc = -ENOSYS;
+	} else {
+		rc = tcon->ses->server->ops->get_dfs_refer(xid, tcon->ses, path,
+							   &refs, &numrefs,
+							   dc->dc_nlsc,
+							   tcon->remap);
+		if (!rc) {
+			mutex_lock(&dfs_cache_list_lock);
+			ce = __update_cache_entry(npath, refs, numrefs);
+			mutex_unlock(&dfs_cache_list_lock);
+			dump_refs(refs, numrefs);
+			free_dfs_info_array(refs, numrefs);
+			if (IS_ERR(ce))
+				rc = PTR_ERR(ce);
+		}
+	}
+	if (rc)
+		cifs_dbg(FYI, "%s: failed to update expired entry\n", __func__);
+out:
+	free_xid(xid);
+	free_normalized_path(path, npath);
+}
+
+/*
+ * Worker that will refresh DFS cache based on lowest TTL value from a DFS
+ * referral.
+ *
+ * FIXME: ensure that all requests are sent to DFS root for refreshing the
+ * cache.
+ */
+static void refresh_cache_worker(struct work_struct *work)
+{
+	struct dfs_cache *dc = container_of(work, struct dfs_cache,
+					    dc_refresh.work);
+	struct dfs_cache_vol_info *vi;
+	struct TCP_Server_Info *server;
+	LIST_HEAD(list);
+	struct cifs_tcon *tcon, *ntcon;
+
+	cifs_dbg(FYI, "%s: refreshing DFS referral cache\n", __func__);
+
+	mutex_lock(&dc->dc_lock);
+
+	list_for_each_entry(vi, &dc->dc_vol_list, vi_list) {
+		cifs_dbg(FYI, "%s: vol path: %s\n", __func__, vi->vi_fullpath);
+		server = cifs_find_tcp_session(&vi->vi_vol);
+		if (!server)
+			continue;
+		cifs_dbg(FYI, "%s: found a tcp ses: %p\n", __func__, server);
+		if (server->tcpStatus != CifsGood)
+			goto next;
+		get_tcons(server, &list);
+		list_for_each_entry_safe(tcon, ntcon, &list, ulist) {
+			do_refresh_tcon(dc, tcon);
+			list_del_init(&tcon->ulist);
+			cifs_put_tcon(tcon);
+		}
+next:
+		cifs_put_tcp_session(server, 0);
+	}
+	queue_delayed_work(cifsiod_wq, &dc->dc_refresh, dc->dc_ttl * HZ);
+	mutex_unlock(&dc->dc_lock);
+
+	cifs_dbg(FYI, "%s: finished refreshing DFS referral cache\n", __func__);
+}
diff --git a/fs/cifs/dfs_cache.h b/fs/cifs/dfs_cache.h
new file mode 100644
index 000000000000..22f366514f3a
--- /dev/null
+++ b/fs/cifs/dfs_cache.h
@@ -0,0 +1,97 @@ 
+/* SPDX-License-Identifier: GPL-2.0 */
+/*
+ * DFS referral cache routines
+ *
+ * Copyright (c) 2018 Paulo Alcantara <palcantara@suse.de>
+ */
+
+#ifndef _CIFS_DFS_CACHE_H
+#define _CIFS_DFS_CACHE_H
+
+#include <linux/nls.h>
+#include <linux/list.h>
+#include "cifsglob.h"
+
+struct dfs_cache_tgt_list {
+	int tl_numtgts;
+	struct list_head tl_list;
+};
+
+struct dfs_cache_tgt_iterator {
+	char *it_name;
+	struct list_head it_list;
+};
+
+extern int dfs_cache_init(void);
+extern void dfs_cache_destroy(void);
+extern const struct file_operations dfscache_proc_fops;
+
+extern int dfs_cache_find(const unsigned int xid, struct cifs_ses *ses,
+			  const struct nls_table *nls_codepage, int remap,
+			  const char *path, struct dfs_info3_param *ref,
+			  struct dfs_cache_tgt_list *tgt_list);
+extern int dfs_cache_noreq_find(const char *path, struct dfs_info3_param *ref,
+				struct dfs_cache_tgt_list *tgt_list);
+extern int dfs_cache_update_tgthint(const unsigned int xid,
+				    struct cifs_ses *ses,
+				    const struct nls_table *nls_codepage,
+				    int remap, const char *path,
+				    const struct dfs_cache_tgt_iterator *it);
+extern int
+dfs_cache_noreq_update_tgthint(const char *path,
+			       const struct dfs_cache_tgt_iterator *it);
+extern int dfs_cache_get_tgt_referral(const char *path,
+				      const struct dfs_cache_tgt_iterator *it,
+				      struct dfs_info3_param *ref);
+extern int dfs_cache_add_vol(struct smb_vol *vol, const char *fullpath);
+extern int dfs_cache_update_vol(const char *fullpath,
+				struct TCP_Server_Info *server);
+extern void dfs_cache_del_vol(const char *fullpath);
+
+static inline struct dfs_cache_tgt_iterator *
+dfs_cache_get_next_tgt(struct dfs_cache_tgt_list *tl,
+		       struct dfs_cache_tgt_iterator *it)
+{
+	if (!tl || list_empty(&tl->tl_list) || !it ||
+	    list_is_last(&it->it_list, &tl->tl_list))
+		return NULL;
+	return list_next_entry(it, it_list);
+}
+
+static inline struct dfs_cache_tgt_iterator *
+dfs_cache_get_tgt_iterator(struct dfs_cache_tgt_list *tl)
+{
+	if (!tl)
+		return NULL;
+	return list_first_entry_or_null(&tl->tl_list,
+					struct dfs_cache_tgt_iterator,
+					it_list);
+}
+
+static inline void dfs_cache_free_tgts(struct dfs_cache_tgt_list *tl)
+{
+	struct dfs_cache_tgt_iterator *it, *nit;
+
+	if (!tl || list_empty(&tl->tl_list))
+		return;
+	list_for_each_entry_safe(it, nit, &tl->tl_list, it_list) {
+		list_del(&it->it_list);
+		kfree(it->it_name);
+		kfree(it);
+	}
+	tl->tl_numtgts = 0;
+}
+
+static inline const char *
+dfs_cache_get_tgt_name(const struct dfs_cache_tgt_iterator *it)
+{
+	return it ? it->it_name : NULL;
+}
+
+static inline int
+dfs_cache_get_nr_tgts(const struct dfs_cache_tgt_list *tl)
+{
+	return tl ? tl->tl_numtgts : 0;
+}
+
+#endif /* _CIFS_DFS_CACHE_H */