| // SPDX-License-Identifier: GPL-2.0 | |
| /* | |
| * DFS referral cache routines | |
| * | |
| * Copyright (c) 2018-2019 Paulo Alcantara <palcantara@suse.de> | |
| */ | |
| #include <linux/jhash.h> | |
| #include <linux/ktime.h> | |
| #include <linux/slab.h> | |
| #include <linux/proc_fs.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 CACHE_HTABLE_SIZE 32 | |
| #define CACHE_MAX_ENTRIES 64 | |
| #define IS_INTERLINK_SET(v) ((v) & (DFSREF_REFERRAL_SERVER | \ | |
| DFSREF_STORAGE_SERVER)) | |
| struct cache_dfs_tgt { | |
| char *name; | |
| int path_consumed; | |
| struct list_head list; | |
| }; | |
| struct cache_entry { | |
| struct hlist_node hlist; | |
| const char *path; | |
| int ttl; | |
| int srvtype; | |
| int flags; | |
| struct timespec64 etime; | |
| int path_consumed; | |
| int numtgts; | |
| struct list_head tlist; | |
| struct cache_dfs_tgt *tgthint; | |
| }; | |
| struct vol_info { | |
| char *fullpath; | |
| spinlock_t smb_vol_lock; | |
| struct smb_vol smb_vol; | |
| char *mntdata; | |
| struct list_head list; | |
| struct list_head rlist; | |
| struct kref refcnt; | |
| }; | |
| static struct kmem_cache *cache_slab __read_mostly; | |
| static struct workqueue_struct *dfscache_wq __read_mostly; | |
| static int cache_ttl; | |
| static DEFINE_SPINLOCK(cache_ttl_lock); | |
| static struct nls_table *cache_nlsc; | |
| /* | |
| * Number of entries in the cache | |
| */ | |
| static atomic_t cache_count; | |
| static struct hlist_head cache_htable[CACHE_HTABLE_SIZE]; | |
| static DECLARE_RWSEM(htable_rw_lock); | |
| static LIST_HEAD(vol_list); | |
| static DEFINE_SPINLOCK(vol_list_lock); | |
| static void refresh_cache_worker(struct work_struct *work); | |
| static DECLARE_DELAYED_WORK(refresh_task, refresh_cache_worker); | |
| static int get_normalized_path(const char *path, char **npath) | |
| { | |
| if (!path || strlen(path) < 3 || (*path != '\\' && *path != '/')) | |
| return -EINVAL; | |
| 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 cache_entry *ce) | |
| { | |
| struct timespec64 ts; | |
| ktime_get_coarse_real_ts64(&ts); | |
| return timespec64_compare(&ts, &ce->etime) >= 0; | |
| } | |
| static inline void free_tgts(struct cache_entry *ce) | |
| { | |
| struct cache_dfs_tgt *t, *n; | |
| list_for_each_entry_safe(t, n, &ce->tlist, list) { | |
| list_del(&t->list); | |
| kfree(t->name); | |
| kfree(t); | |
| } | |
| } | |
| static inline void flush_cache_ent(struct cache_entry *ce) | |
| { | |
| hlist_del_init(&ce->hlist); | |
| kfree(ce->path); | |
| free_tgts(ce); | |
| atomic_dec(&cache_count); | |
| kmem_cache_free(cache_slab, ce); | |
| } | |
| static void flush_cache_ents(void) | |
| { | |
| int i; | |
| for (i = 0; i < CACHE_HTABLE_SIZE; i++) { | |
| struct hlist_head *l = &cache_htable[i]; | |
| struct hlist_node *n; | |
| struct cache_entry *ce; | |
| hlist_for_each_entry_safe(ce, n, l, hlist) { | |
| if (!hlist_unhashed(&ce->hlist)) | |
| flush_cache_ent(ce); | |
| } | |
| } | |
| } | |
| /* | |
| * dfs cache /proc file | |
| */ | |
| static int dfscache_proc_show(struct seq_file *m, void *v) | |
| { | |
| int i; | |
| struct cache_entry *ce; | |
| struct cache_dfs_tgt *t; | |
| seq_puts(m, "DFS cache\n---------\n"); | |
| down_read(&htable_rw_lock); | |
| for (i = 0; i < CACHE_HTABLE_SIZE; i++) { | |
| struct hlist_head *l = &cache_htable[i]; | |
| hlist_for_each_entry(ce, l, hlist) { | |
| if (hlist_unhashed(&ce->hlist)) | |
| continue; | |
| seq_printf(m, | |
| "cache entry: path=%s,type=%s,ttl=%d,etime=%ld," | |
| "interlink=%s,path_consumed=%d,expired=%s\n", | |
| ce->path, | |
| ce->srvtype == DFS_TYPE_ROOT ? "root" : "link", | |
| ce->ttl, ce->etime.tv_nsec, | |
| IS_INTERLINK_SET(ce->flags) ? "yes" : "no", | |
| ce->path_consumed, | |
| cache_entry_expired(ce) ? "yes" : "no"); | |
| list_for_each_entry(t, &ce->tlist, list) { | |
| seq_printf(m, " %s%s\n", | |
| t->name, | |
| ce->tgthint == t ? " (target hint)" : ""); | |
| } | |
| } | |
| } | |
| up_read(&htable_rw_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\n"); | |
| down_write(&htable_rw_lock); | |
| flush_cache_ents(); | |
| up_write(&htable_rw_lock); | |
| return count; | |
| } | |
| static int dfscache_proc_open(struct inode *inode, struct file *file) | |
| { | |
| return single_open(file, dfscache_proc_show, NULL); | |
| } | |
| const struct proc_ops dfscache_proc_ops = { | |
| .proc_open = dfscache_proc_open, | |
| .proc_read = seq_read, | |
| .proc_lseek = seq_lseek, | |
| .proc_release = single_release, | |
| .proc_write = dfscache_proc_write, | |
| }; | |
| #ifdef CONFIG_CIFS_DEBUG2 | |
| static inline void dump_tgts(const struct cache_entry *ce) | |
| { | |
| struct cache_dfs_tgt *t; | |
| cifs_dbg(FYI, "target list:\n"); | |
| list_for_each_entry(t, &ce->tlist, list) { | |
| cifs_dbg(FYI, " %s%s\n", t->name, | |
| ce->tgthint == t ? " (target hint)" : ""); | |
| } | |
| } | |
| static inline void dump_ce(const struct 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->path, | |
| ce->srvtype == DFS_TYPE_ROOT ? "root" : "link", ce->ttl, | |
| ce->etime.tv_nsec, | |
| IS_INTERLINK_SET(ce->flags) ? "yes" : "no", | |
| 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 rc; | |
| int i; | |
| dfscache_wq = alloc_workqueue("cifs-dfscache", | |
| WQ_FREEZABLE | WQ_MEM_RECLAIM, 1); | |
| if (!dfscache_wq) | |
| return -ENOMEM; | |
| cache_slab = kmem_cache_create("cifs_dfs_cache", | |
| sizeof(struct cache_entry), 0, | |
| SLAB_HWCACHE_ALIGN, NULL); | |
| if (!cache_slab) { | |
| rc = -ENOMEM; | |
| goto out_destroy_wq; | |
| } | |
| for (i = 0; i < CACHE_HTABLE_SIZE; i++) | |
| INIT_HLIST_HEAD(&cache_htable[i]); | |
| atomic_set(&cache_count, 0); | |
| cache_nlsc = load_nls_default(); | |
| cifs_dbg(FYI, "%s: initialized DFS referral cache\n", __func__); | |
| return 0; | |
| out_destroy_wq: | |
| destroy_workqueue(dfscache_wq); | |
| return rc; | |
| } | |
| static inline unsigned int cache_entry_hash(const void *data, int size) | |
| { | |
| unsigned int h; | |
| h = jhash(data, size, 0); | |
| return h & (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 cache_entry *ce) | |
| { | |
| struct cache_dfs_tgt *t = ce->tgthint; | |
| return 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, | |
| }; | |
| struct timespec64 now; | |
| ktime_get_coarse_real_ts64(&now); | |
| return timespec64_add(now, ts); | |
| } | |
| /* Allocate a new DFS target */ | |
| static struct cache_dfs_tgt *alloc_target(const char *name, int path_consumed) | |
| { | |
| struct cache_dfs_tgt *t; | |
| t = kmalloc(sizeof(*t), GFP_ATOMIC); | |
| if (!t) | |
| return ERR_PTR(-ENOMEM); | |
| t->name = kstrndup(name, strlen(name), GFP_ATOMIC); | |
| if (!t->name) { | |
| kfree(t); | |
| return ERR_PTR(-ENOMEM); | |
| } | |
| t->path_consumed = path_consumed; | |
| INIT_LIST_HEAD(&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 cache_entry *ce, const char *tgthint) | |
| { | |
| int i; | |
| ce->ttl = refs[0].ttl; | |
| ce->etime = get_expire_time(ce->ttl); | |
| ce->srvtype = refs[0].server_type; | |
| ce->flags = refs[0].ref_flag; | |
| ce->path_consumed = refs[0].path_consumed; | |
| for (i = 0; i < numrefs; i++) { | |
| struct cache_dfs_tgt *t; | |
| t = alloc_target(refs[i].node_name, refs[i].path_consumed); | |
| if (IS_ERR(t)) { | |
| free_tgts(ce); | |
| return PTR_ERR(t); | |
| } | |
| if (tgthint && !strcasecmp(t->name, tgthint)) { | |
| list_add(&t->list, &ce->tlist); | |
| tgthint = NULL; | |
| } else { | |
| list_add_tail(&t->list, &ce->tlist); | |
| } | |
| ce->numtgts++; | |
| } | |
| ce->tgthint = list_first_entry_or_null(&ce->tlist, | |
| struct cache_dfs_tgt, list); | |
| return 0; | |
| } | |
| /* Allocate a new cache entry */ | |
| static struct cache_entry *alloc_cache_entry(const char *path, | |
| const struct dfs_info3_param *refs, | |
| int numrefs) | |
| { | |
| struct cache_entry *ce; | |
| int rc; | |
| ce = kmem_cache_zalloc(cache_slab, GFP_KERNEL); | |
| if (!ce) | |
| return ERR_PTR(-ENOMEM); | |
| ce->path = kstrndup(path, strlen(path), GFP_KERNEL); | |
| if (!ce->path) { | |
| kmem_cache_free(cache_slab, ce); | |
| return ERR_PTR(-ENOMEM); | |
| } | |
| INIT_HLIST_NODE(&ce->hlist); | |
| INIT_LIST_HEAD(&ce->tlist); | |
| rc = copy_ref_data(refs, numrefs, ce, NULL); | |
| if (rc) { | |
| kfree(ce->path); | |
| kmem_cache_free(cache_slab, ce); | |
| ce = ERR_PTR(rc); | |
| } | |
| return ce; | |
| } | |
| /* Must be called with htable_rw_lock held */ | |
| static void remove_oldest_entry(void) | |
| { | |
| int i; | |
| struct cache_entry *ce; | |
| struct cache_entry *to_del = NULL; | |
| for (i = 0; i < CACHE_HTABLE_SIZE; i++) { | |
| struct hlist_head *l = &cache_htable[i]; | |
| hlist_for_each_entry(ce, l, hlist) { | |
| if (hlist_unhashed(&ce->hlist)) | |
| continue; | |
| if (!to_del || timespec64_compare(&ce->etime, | |
| &to_del->etime) < 0) | |
| to_del = ce; | |
| } | |
| } | |
| if (!to_del) { | |
| cifs_dbg(FYI, "%s: no entry to remove\n", __func__); | |
| return; | |
| } | |
| cifs_dbg(FYI, "%s: removing entry\n", __func__); | |
| dump_ce(to_del); | |
| flush_cache_ent(to_del); | |
| } | |
| /* Add a new DFS cache entry */ | |
| static int add_cache_entry(const char *path, unsigned int hash, | |
| struct dfs_info3_param *refs, int numrefs) | |
| { | |
| struct cache_entry *ce; | |
| ce = alloc_cache_entry(path, refs, numrefs); | |
| if (IS_ERR(ce)) | |
| return PTR_ERR(ce); | |
| spin_lock(&cache_ttl_lock); | |
| if (!cache_ttl) { | |
| cache_ttl = ce->ttl; | |
| queue_delayed_work(dfscache_wq, &refresh_task, cache_ttl * HZ); | |
| } else { | |
| cache_ttl = min_t(int, cache_ttl, ce->ttl); | |
| mod_delayed_work(dfscache_wq, &refresh_task, cache_ttl * HZ); | |
| } | |
| spin_unlock(&cache_ttl_lock); | |
| down_write(&htable_rw_lock); | |
| hlist_add_head(&ce->hlist, &cache_htable[hash]); | |
| dump_ce(ce); | |
| up_write(&htable_rw_lock); | |
| return 0; | |
| } | |
| static struct cache_entry *__lookup_cache_entry(const char *path) | |
| { | |
| struct cache_entry *ce; | |
| unsigned int h; | |
| bool found = false; | |
| h = cache_entry_hash(path, strlen(path)); | |
| hlist_for_each_entry(ce, &cache_htable[h], hlist) { | |
| if (!strcasecmp(path, ce->path)) { | |
| found = true; | |
| dump_ce(ce); | |
| break; | |
| } | |
| } | |
| if (!found) | |
| ce = ERR_PTR(-ENOENT); | |
| return ce; | |
| } | |
| /* | |
| * Find a DFS cache entry in hash table and optionally check prefix path against | |
| * @path. | |
| * Use whole path components in the match. | |
| * Must be called with htable_rw_lock held. | |
| * | |
| * Return ERR_PTR(-ENOENT) if the entry is not found. | |
| */ | |
| static struct cache_entry *lookup_cache_entry(const char *path, unsigned int *hash) | |
| { | |
| struct cache_entry *ce = ERR_PTR(-ENOENT); | |
| unsigned int h; | |
| int cnt = 0; | |
| char *npath; | |
| char *s, *e; | |
| char sep; | |
| npath = kstrndup(path, strlen(path), GFP_KERNEL); | |
| if (!npath) | |
| return ERR_PTR(-ENOMEM); | |
| s = npath; | |
| sep = *npath; | |
| while ((s = strchr(s, sep)) && ++cnt < 3) | |
| s++; | |
| if (cnt < 3) { | |
| h = cache_entry_hash(path, strlen(path)); | |
| ce = __lookup_cache_entry(path); | |
| goto out; | |
| } | |
| /* | |
| * Handle paths that have more than two path components and are a complete prefix of the DFS | |
| * referral request path (@path). | |
| * | |
| * See MS-DFSC 3.2.5.5 "Receiving a Root Referral Request or Link Referral Request". | |
| */ | |
| h = cache_entry_hash(npath, strlen(npath)); | |
| e = npath + strlen(npath) - 1; | |
| while (e > s) { | |
| char tmp; | |
| /* skip separators */ | |
| while (e > s && *e == sep) | |
| e--; | |
| if (e == s) | |
| goto out; | |
| tmp = *(e+1); | |
| *(e+1) = 0; | |
| ce = __lookup_cache_entry(npath); | |
| if (!IS_ERR(ce)) { | |
| h = cache_entry_hash(npath, strlen(npath)); | |
| break; | |
| } | |
| *(e+1) = tmp; | |
| /* backward until separator */ | |
| while (e > s && *e != sep) | |
| e--; | |
| } | |
| out: | |
| if (hash) | |
| *hash = h; | |
| kfree(npath); | |
| return ce; | |
| } | |
| static void __vol_release(struct vol_info *vi) | |
| { | |
| kfree(vi->fullpath); | |
| kfree(vi->mntdata); | |
| cifs_cleanup_volume_info_contents(&vi->smb_vol); | |
| kfree(vi); | |
| } | |
| static void vol_release(struct kref *kref) | |
| { | |
| struct vol_info *vi = container_of(kref, struct vol_info, refcnt); | |
| spin_lock(&vol_list_lock); | |
| list_del(&vi->list); | |
| spin_unlock(&vol_list_lock); | |
| __vol_release(vi); | |
| } | |
| static inline void free_vol_list(void) | |
| { | |
| struct vol_info *vi, *nvi; | |
| list_for_each_entry_safe(vi, nvi, &vol_list, list) { | |
| list_del_init(&vi->list); | |
| __vol_release(vi); | |
| } | |
| } | |
| /** | |
| * dfs_cache_destroy - destroy DFS referral cache | |
| */ | |
| void dfs_cache_destroy(void) | |
| { | |
| cancel_delayed_work_sync(&refresh_task); | |
| unload_nls(cache_nlsc); | |
| free_vol_list(); | |
| flush_cache_ents(); | |
| kmem_cache_destroy(cache_slab); | |
| destroy_workqueue(dfscache_wq); | |
| cifs_dbg(FYI, "%s: destroyed DFS referral cache\n", __func__); | |
| } | |
| /* Must be called with htable_rw_lock held */ | |
| static int __update_cache_entry(const char *path, | |
| const struct dfs_info3_param *refs, | |
| int numrefs) | |
| { | |
| int rc; | |
| struct cache_entry *ce; | |
| char *s, *th = NULL; | |
| ce = lookup_cache_entry(path, NULL); | |
| if (IS_ERR(ce)) | |
| return PTR_ERR(ce); | |
| if (ce->tgthint) { | |
| s = ce->tgthint->name; | |
| th = kstrndup(s, strlen(s), GFP_ATOMIC); | |
| if (!th) | |
| return -ENOMEM; | |
| } | |
| free_tgts(ce); | |
| ce->numtgts = 0; | |
| rc = copy_ref_data(refs, numrefs, ce, th); | |
| kfree(th); | |
| return rc; | |
| } | |
| static int get_dfs_referral(const unsigned int xid, struct cifs_ses *ses, | |
| const struct nls_table *nls_codepage, int remap, | |
| const char *path, struct dfs_info3_param **refs, | |
| int *numrefs) | |
| { | |
| cifs_dbg(FYI, "%s: get an DFS referral for %s\n", __func__, path); | |
| if (!ses || !ses->server || !ses->server->ops->get_dfs_refer) | |
| return -EOPNOTSUPP; | |
| if (unlikely(!nls_codepage)) | |
| return -EINVAL; | |
| *refs = NULL; | |
| *numrefs = 0; | |
| return ses->server->ops->get_dfs_refer(xid, ses, path, refs, numrefs, | |
| nls_codepage, remap); | |
| } | |
| /* Update an expired cache entry by getting a new DFS referral from server */ | |
| static int update_cache_entry(const char *path, | |
| const struct dfs_info3_param *refs, | |
| int numrefs) | |
| { | |
| int rc; | |
| down_write(&htable_rw_lock); | |
| rc = __update_cache_entry(path, refs, numrefs); | |
| up_write(&htable_rw_lock); | |
| return rc; | |
| } | |
| /* | |
| * 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 int __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 hash; | |
| struct cache_entry *ce; | |
| struct dfs_info3_param *refs = NULL; | |
| int numrefs = 0; | |
| bool newent = false; | |
| cifs_dbg(FYI, "%s: search path: %s\n", __func__, path); | |
| down_read(&htable_rw_lock); | |
| ce = lookup_cache_entry(path, &hash); | |
| /* | |
| * If @noreq is set, no requests will be sent to the server. Just return | |
| * the cache entry. | |
| */ | |
| if (noreq) { | |
| up_read(&htable_rw_lock); | |
| return PTR_ERR_OR_ZERO(ce); | |
| } | |
| if (!IS_ERR(ce)) { | |
| if (!cache_entry_expired(ce)) { | |
| dump_ce(ce); | |
| up_read(&htable_rw_lock); | |
| return 0; | |
| } | |
| } else { | |
| newent = true; | |
| } | |
| up_read(&htable_rw_lock); | |
| /* | |
| * No entry was found. | |
| * | |
| * Request a new DFS referral in order to create a new cache entry, or | |
| * updating an existing one. | |
| */ | |
| rc = get_dfs_referral(xid, ses, nls_codepage, remap, path, | |
| &refs, &numrefs); | |
| if (rc) | |
| return rc; | |
| dump_refs(refs, numrefs); | |
| if (!newent) { | |
| rc = update_cache_entry(path, refs, numrefs); | |
| goto out_free_refs; | |
| } | |
| if (atomic_read(&cache_count) >= CACHE_MAX_ENTRIES) { | |
| cifs_dbg(FYI, "%s: reached max cache size (%d)\n", | |
| __func__, CACHE_MAX_ENTRIES); | |
| down_write(&htable_rw_lock); | |
| remove_oldest_entry(); | |
| up_write(&htable_rw_lock); | |
| } | |
| rc = add_cache_entry(path, hash, refs, numrefs); | |
| if (!rc) | |
| atomic_inc(&cache_count); | |
| out_free_refs: | |
| free_dfs_info_array(refs, numrefs); | |
| return rc; | |
| } | |
| /* | |
| * Set up a DFS referral from a given cache entry. | |
| * | |
| * Must be called with htable_rw_lock held. | |
| */ | |
| static int setup_referral(const char *path, struct cache_entry *ce, | |
| struct dfs_info3_param *ref, const char *target) | |
| { | |
| 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_ATOMIC); | |
| if (!ref->path_name) | |
| return -ENOMEM; | |
| ref->node_name = kstrndup(target, strlen(target), GFP_ATOMIC); | |
| if (!ref->node_name) { | |
| rc = -ENOMEM; | |
| goto err_free_path; | |
| } | |
| ref->path_consumed = ce->path_consumed; | |
| ref->ttl = ce->ttl; | |
| ref->server_type = ce->srvtype; | |
| ref->ref_flag = 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_targets(struct cache_entry *ce, struct dfs_cache_tgt_list *tl) | |
| { | |
| int rc; | |
| struct list_head *head = &tl->tl_list; | |
| struct cache_dfs_tgt *t; | |
| struct dfs_cache_tgt_iterator *it, *nit; | |
| memset(tl, 0, sizeof(*tl)); | |
| INIT_LIST_HEAD(head); | |
| list_for_each_entry(t, &ce->tlist, list) { | |
| it = kzalloc(sizeof(*it), GFP_ATOMIC); | |
| if (!it) { | |
| rc = -ENOMEM; | |
| goto err_free_it; | |
| } | |
| it->it_name = kstrndup(t->name, strlen(t->name), GFP_ATOMIC); | |
| if (!it->it_name) { | |
| kfree(it); | |
| rc = -ENOMEM; | |
| goto err_free_it; | |
| } | |
| it->it_path_consumed = t->path_consumed; | |
| if (ce->tgthint == t) | |
| list_add(&it->it_list, head); | |
| else | |
| list_add_tail(&it->it_list, head); | |
| } | |
| tl->tl_numtgts = 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 cache_entry *ce; | |
| rc = get_normalized_path(path, &npath); | |
| if (rc) | |
| return rc; | |
| rc = __dfs_cache_find(xid, ses, nls_codepage, remap, npath, false); | |
| if (rc) | |
| goto out_free_path; | |
| down_read(&htable_rw_lock); | |
| ce = lookup_cache_entry(npath, NULL); | |
| if (IS_ERR(ce)) { | |
| up_read(&htable_rw_lock); | |
| rc = PTR_ERR(ce); | |
| goto out_free_path; | |
| } | |
| if (ref) | |
| rc = setup_referral(path, ce, ref, get_tgt_name(ce)); | |
| else | |
| rc = 0; | |
| if (!rc && tgt_list) | |
| rc = get_targets(ce, tgt_list); | |
| up_read(&htable_rw_lock); | |
| out_free_path: | |
| 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 cache_entry *ce; | |
| rc = get_normalized_path(path, &npath); | |
| if (rc) | |
| return rc; | |
| cifs_dbg(FYI, "%s: path: %s\n", __func__, npath); | |
| down_read(&htable_rw_lock); | |
| ce = lookup_cache_entry(npath, NULL); | |
| if (IS_ERR(ce)) { | |
| rc = PTR_ERR(ce); | |
| goto out_unlock; | |
| } | |
| if (ref) | |
| rc = setup_referral(path, ce, ref, get_tgt_name(ce)); | |
| else | |
| rc = 0; | |
| if (!rc && tgt_list) | |
| rc = get_targets(ce, tgt_list); | |
| out_unlock: | |
| up_read(&htable_rw_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 cache_entry *ce; | |
| struct cache_dfs_tgt *t; | |
| rc = get_normalized_path(path, &npath); | |
| if (rc) | |
| return rc; | |
| cifs_dbg(FYI, "%s: update target hint - path: %s\n", __func__, npath); | |
| rc = __dfs_cache_find(xid, ses, nls_codepage, remap, npath, false); | |
| if (rc) | |
| goto out_free_path; | |
| down_write(&htable_rw_lock); | |
| ce = lookup_cache_entry(npath, NULL); | |
| if (IS_ERR(ce)) { | |
| rc = PTR_ERR(ce); | |
| goto out_unlock; | |
| } | |
| t = ce->tgthint; | |
| if (likely(!strcasecmp(it->it_name, t->name))) | |
| goto out_unlock; | |
| list_for_each_entry(t, &ce->tlist, list) { | |
| if (!strcasecmp(t->name, it->it_name)) { | |
| ce->tgthint = t; | |
| cifs_dbg(FYI, "%s: new target hint: %s\n", __func__, | |
| it->it_name); | |
| break; | |
| } | |
| } | |
| out_unlock: | |
| up_write(&htable_rw_lock); | |
| out_free_path: | |
| 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 cache_entry *ce; | |
| struct cache_dfs_tgt *t; | |
| if (!it) | |
| return -EINVAL; | |
| rc = get_normalized_path(path, &npath); | |
| if (rc) | |
| return rc; | |
| cifs_dbg(FYI, "%s: path: %s\n", __func__, npath); | |
| down_write(&htable_rw_lock); | |
| ce = lookup_cache_entry(npath, NULL); | |
| if (IS_ERR(ce)) { | |
| rc = PTR_ERR(ce); | |
| goto out_unlock; | |
| } | |
| rc = 0; | |
| t = ce->tgthint; | |
| if (unlikely(!strcasecmp(it->it_name, t->name))) | |
| goto out_unlock; | |
| list_for_each_entry(t, &ce->tlist, list) { | |
| if (!strcasecmp(t->name, it->it_name)) { | |
| ce->tgthint = t; | |
| cifs_dbg(FYI, "%s: new target hint: %s\n", __func__, | |
| it->it_name); | |
| break; | |
| } | |
| } | |
| out_unlock: | |
| up_write(&htable_rw_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 cache_entry *ce; | |
| if (!it || !ref) | |
| return -EINVAL; | |
| rc = get_normalized_path(path, &npath); | |
| if (rc) | |
| return rc; | |
| cifs_dbg(FYI, "%s: path: %s\n", __func__, npath); | |
| down_read(&htable_rw_lock); | |
| ce = lookup_cache_entry(npath, NULL); | |
| if (IS_ERR(ce)) { | |
| rc = PTR_ERR(ce); | |
| goto out_unlock; | |
| } | |
| cifs_dbg(FYI, "%s: target name: %s\n", __func__, it->it_name); | |
| rc = setup_referral(path, ce, ref, it->it_name); | |
| out_unlock: | |
| up_read(&htable_rw_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_sensitive(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. | |
| * | |
| * @mntdata: mount data. | |
| * @vol: cifs volume. | |
| * @fullpath: origin full path. | |
| * | |
| * Return zero if volume was set up correctly, otherwise non-zero. | |
| */ | |
| int dfs_cache_add_vol(char *mntdata, struct smb_vol *vol, const char *fullpath) | |
| { | |
| int rc; | |
| struct vol_info *vi; | |
| if (!vol || !fullpath || !mntdata) | |
| return -EINVAL; | |
| cifs_dbg(FYI, "%s: fullpath: %s\n", __func__, fullpath); | |
| vi = kzalloc(sizeof(*vi), GFP_KERNEL); | |
| if (!vi) | |
| return -ENOMEM; | |
| vi->fullpath = kstrndup(fullpath, strlen(fullpath), GFP_KERNEL); | |
| if (!vi->fullpath) { | |
| rc = -ENOMEM; | |
| goto err_free_vi; | |
| } | |
| rc = dup_vol(vol, &vi->smb_vol); | |
| if (rc) | |
| goto err_free_fullpath; | |
| vi->mntdata = mntdata; | |
| spin_lock_init(&vi->smb_vol_lock); | |
| kref_init(&vi->refcnt); | |
| spin_lock(&vol_list_lock); | |
| list_add_tail(&vi->list, &vol_list); | |
| spin_unlock(&vol_list_lock); | |
| return 0; | |
| err_free_fullpath: | |
| kfree(vi->fullpath); | |
| err_free_vi: | |
| kfree(vi); | |
| return rc; | |
| } | |
| /* Must be called with vol_list_lock held */ | |
| static struct vol_info *find_vol(const char *fullpath) | |
| { | |
| struct vol_info *vi; | |
| list_for_each_entry(vi, &vol_list, list) { | |
| cifs_dbg(FYI, "%s: vi->fullpath: %s\n", __func__, vi->fullpath); | |
| if (!strcasecmp(vi->fullpath, 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) | |
| { | |
| struct vol_info *vi; | |
| if (!fullpath || !server) | |
| return -EINVAL; | |
| cifs_dbg(FYI, "%s: fullpath: %s\n", __func__, fullpath); | |
| spin_lock(&vol_list_lock); | |
| vi = find_vol(fullpath); | |
| if (IS_ERR(vi)) { | |
| spin_unlock(&vol_list_lock); | |
| return PTR_ERR(vi); | |
| } | |
| kref_get(&vi->refcnt); | |
| spin_unlock(&vol_list_lock); | |
| cifs_dbg(FYI, "%s: updating volume info\n", __func__); | |
| spin_lock(&vi->smb_vol_lock); | |
| memcpy(&vi->smb_vol.dstaddr, &server->dstaddr, | |
| sizeof(vi->smb_vol.dstaddr)); | |
| spin_unlock(&vi->smb_vol_lock); | |
| kref_put(&vi->refcnt, vol_release); | |
| return 0; | |
| } | |
| /** | |
| * dfs_cache_del_vol - remove volume info in DFS cache during umount() | |
| * | |
| * @fullpath: fullpath to look up in volume list. | |
| */ | |
| void dfs_cache_del_vol(const char *fullpath) | |
| { | |
| struct vol_info *vi; | |
| if (!fullpath || !*fullpath) | |
| return; | |
| cifs_dbg(FYI, "%s: fullpath: %s\n", __func__, fullpath); | |
| spin_lock(&vol_list_lock); | |
| vi = find_vol(fullpath); | |
| spin_unlock(&vol_list_lock); | |
| if (!IS_ERR(vi)) | |
| kref_put(&vi->refcnt, vol_release); | |
| } | |
| /** | |
| * dfs_cache_get_tgt_share - parse a DFS target | |
| * | |
| * @path: DFS full path | |
| * @it: DFS target iterator. | |
| * @share: tree name. | |
| * @prefix: prefix path. | |
| * | |
| * Return zero if target was parsed correctly, otherwise non-zero. | |
| */ | |
| int dfs_cache_get_tgt_share(char *path, const struct dfs_cache_tgt_iterator *it, | |
| char **share, char **prefix) | |
| { | |
| char *s, sep, *p; | |
| size_t len; | |
| size_t plen1, plen2; | |
| if (!it || !path || !share || !prefix || strlen(path) < it->it_path_consumed) | |
| return -EINVAL; | |
| *share = NULL; | |
| *prefix = NULL; | |
| sep = it->it_name[0]; | |
| if (sep != '\\' && sep != '/') | |
| return -EINVAL; | |
| s = strchr(it->it_name + 1, sep); | |
| if (!s) | |
| return -EINVAL; | |
| /* point to prefix in target node */ | |
| s = strchrnul(s + 1, sep); | |
| /* extract target share */ | |
| *share = kstrndup(it->it_name, s - it->it_name, GFP_KERNEL); | |
| if (!*share) | |
| return -ENOMEM; | |
| /* skip separator */ | |
| if (*s) | |
| s++; | |
| /* point to prefix in DFS path */ | |
| p = path + it->it_path_consumed; | |
| if (*p == sep) | |
| p++; | |
| /* merge prefix paths from DFS path and target node */ | |
| plen1 = it->it_name + strlen(it->it_name) - s; | |
| plen2 = path + strlen(path) - p; | |
| if (plen1 || plen2) { | |
| len = plen1 + plen2 + 2; | |
| *prefix = kmalloc(len, GFP_KERNEL); | |
| if (!*prefix) { | |
| kfree(*share); | |
| *share = NULL; | |
| return -ENOMEM; | |
| } | |
| if (plen1) | |
| scnprintf(*prefix, len, "%.*s%c%.*s", (int)plen1, s, sep, (int)plen2, p); | |
| else | |
| strscpy(*prefix, p, len); | |
| } | |
| return 0; | |
| } | |
| /* 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); | |
| } | |
| static bool is_dfs_link(const char *path) | |
| { | |
| char *s; | |
| s = strchr(path + 1, '\\'); | |
| if (!s) | |
| return false; | |
| return !!strchr(s + 1, '\\'); | |
| } | |
| static char *get_dfs_root(const char *path) | |
| { | |
| char *s, *npath; | |
| s = strchr(path + 1, '\\'); | |
| if (!s) | |
| return ERR_PTR(-EINVAL); | |
| s = strchr(s + 1, '\\'); | |
| if (!s) | |
| return ERR_PTR(-EINVAL); | |
| npath = kstrndup(path, s - path, GFP_KERNEL); | |
| if (!npath) | |
| return ERR_PTR(-ENOMEM); | |
| return npath; | |
| } | |
| static inline void put_tcp_server(struct TCP_Server_Info *server) | |
| { | |
| cifs_put_tcp_session(server, 0); | |
| } | |
| static struct TCP_Server_Info *get_tcp_server(struct smb_vol *vol) | |
| { | |
| struct TCP_Server_Info *server; | |
| server = cifs_find_tcp_session(vol); | |
| if (IS_ERR_OR_NULL(server)) | |
| return NULL; | |
| spin_lock(&GlobalMid_Lock); | |
| if (server->tcpStatus != CifsGood) { | |
| spin_unlock(&GlobalMid_Lock); | |
| put_tcp_server(server); | |
| return NULL; | |
| } | |
| spin_unlock(&GlobalMid_Lock); | |
| return server; | |
| } | |
| /* Find root SMB session out of a DFS link path */ | |
| static struct cifs_ses *find_root_ses(struct vol_info *vi, | |
| struct cifs_tcon *tcon, | |
| const char *path) | |
| { | |
| char *rpath; | |
| int rc; | |
| struct cache_entry *ce; | |
| struct dfs_info3_param ref = {0}; | |
| char *mdata = NULL, *devname = NULL; | |
| struct TCP_Server_Info *server; | |
| struct cifs_ses *ses; | |
| struct smb_vol vol = {NULL}; | |
| rpath = get_dfs_root(path); | |
| if (IS_ERR(rpath)) | |
| return ERR_CAST(rpath); | |
| down_read(&htable_rw_lock); | |
| ce = lookup_cache_entry(rpath, NULL); | |
| if (IS_ERR(ce)) { | |
| up_read(&htable_rw_lock); | |
| ses = ERR_CAST(ce); | |
| goto out; | |
| } | |
| rc = setup_referral(path, ce, &ref, get_tgt_name(ce)); | |
| if (rc) { | |
| up_read(&htable_rw_lock); | |
| ses = ERR_PTR(rc); | |
| goto out; | |
| } | |
| up_read(&htable_rw_lock); | |
| mdata = cifs_compose_mount_options(vi->mntdata, rpath, &ref, | |
| &devname); | |
| free_dfs_info_param(&ref); | |
| if (IS_ERR(mdata)) { | |
| ses = ERR_CAST(mdata); | |
| mdata = NULL; | |
| goto out; | |
| } | |
| rc = cifs_setup_volume_info(&vol, mdata, devname, false); | |
| kfree(devname); | |
| if (rc) { | |
| ses = ERR_PTR(rc); | |
| goto out; | |
| } | |
| server = get_tcp_server(&vol); | |
| if (!server) { | |
| ses = ERR_PTR(-EHOSTDOWN); | |
| goto out; | |
| } | |
| ses = cifs_get_smb_ses(server, &vol); | |
| out: | |
| cifs_cleanup_volume_info_contents(&vol); | |
| kfree(mdata); | |
| kfree(rpath); | |
| return ses; | |
| } | |
| /* Refresh DFS cache entry from a given tcon */ | |
| static int refresh_tcon(struct vol_info *vi, struct cifs_tcon *tcon) | |
| { | |
| int rc = 0; | |
| unsigned int xid; | |
| char *path, *npath; | |
| struct cache_entry *ce; | |
| struct cifs_ses *root_ses = NULL, *ses; | |
| struct dfs_info3_param *refs = NULL; | |
| int numrefs = 0; | |
| xid = get_xid(); | |
| path = tcon->dfs_path + 1; | |
| rc = get_normalized_path(path, &npath); | |
| if (rc) | |
| goto out_free_xid; | |
| down_read(&htable_rw_lock); | |
| ce = lookup_cache_entry(npath, NULL); | |
| if (IS_ERR(ce)) { | |
| rc = PTR_ERR(ce); | |
| up_read(&htable_rw_lock); | |
| goto out_free_path; | |
| } | |
| if (!cache_entry_expired(ce)) { | |
| up_read(&htable_rw_lock); | |
| goto out_free_path; | |
| } | |
| up_read(&htable_rw_lock); | |
| /* If it's a DFS Link, then use root SMB session for refreshing it */ | |
| if (is_dfs_link(npath)) { | |
| ses = root_ses = find_root_ses(vi, tcon, npath); | |
| if (IS_ERR(ses)) { | |
| rc = PTR_ERR(ses); | |
| root_ses = NULL; | |
| goto out_free_path; | |
| } | |
| } else { | |
| ses = tcon->ses; | |
| } | |
| rc = get_dfs_referral(xid, ses, cache_nlsc, tcon->remap, npath, &refs, | |
| &numrefs); | |
| if (!rc) { | |
| dump_refs(refs, numrefs); | |
| rc = update_cache_entry(npath, refs, numrefs); | |
| free_dfs_info_array(refs, numrefs); | |
| } | |
| if (root_ses) | |
| cifs_put_smb_ses(root_ses); | |
| out_free_path: | |
| free_normalized_path(path, npath); | |
| out_free_xid: | |
| free_xid(xid); | |
| return rc; | |
| } | |
| /* | |
| * Worker that will refresh DFS cache based on lowest TTL value from a DFS | |
| * referral. | |
| */ | |
| static void refresh_cache_worker(struct work_struct *work) | |
| { | |
| struct vol_info *vi, *nvi; | |
| struct TCP_Server_Info *server; | |
| LIST_HEAD(vols); | |
| LIST_HEAD(tcons); | |
| struct cifs_tcon *tcon, *ntcon; | |
| int rc; | |
| /* | |
| * Find SMB volumes that are eligible (server->tcpStatus == CifsGood) | |
| * for refreshing. | |
| */ | |
| spin_lock(&vol_list_lock); | |
| list_for_each_entry(vi, &vol_list, list) { | |
| server = get_tcp_server(&vi->smb_vol); | |
| if (!server) | |
| continue; | |
| kref_get(&vi->refcnt); | |
| list_add_tail(&vi->rlist, &vols); | |
| put_tcp_server(server); | |
| } | |
| spin_unlock(&vol_list_lock); | |
| /* Walk through all TCONs and refresh any expired cache entry */ | |
| list_for_each_entry_safe(vi, nvi, &vols, rlist) { | |
| spin_lock(&vi->smb_vol_lock); | |
| server = get_tcp_server(&vi->smb_vol); | |
| spin_unlock(&vi->smb_vol_lock); | |
| if (!server) | |
| goto next_vol; | |
| get_tcons(server, &tcons); | |
| rc = 0; | |
| list_for_each_entry_safe(tcon, ntcon, &tcons, ulist) { | |
| /* | |
| * Skip tcp server if any of its tcons failed to refresh | |
| * (possibily due to reconnects). | |
| */ | |
| if (!rc) | |
| rc = refresh_tcon(vi, tcon); | |
| list_del_init(&tcon->ulist); | |
| cifs_put_tcon(tcon); | |
| } | |
| put_tcp_server(server); | |
| next_vol: | |
| list_del_init(&vi->rlist); | |
| kref_put(&vi->refcnt, vol_release); | |
| } | |
| spin_lock(&cache_ttl_lock); | |
| queue_delayed_work(dfscache_wq, &refresh_task, cache_ttl * HZ); | |
| spin_unlock(&cache_ttl_lock); | |
| } |