Commit | Line | Data |
---|---|---|
2874c5fd | 1 | // SPDX-License-Identifier: GPL-2.0-or-later |
6d5ae0de IM |
2 | /* |
3 | * Contains the CIFS DFS referral mounting routines used for handling | |
4 | * traversal via DFS junction point | |
5 | * | |
6 | * Copyright (c) 2007 Igor Mammedov | |
366781c1 | 7 | * Copyright (C) International Business Machines Corp., 2008 |
6d5ae0de | 8 | * Author(s): Igor Mammedov (niallain@gmail.com) |
366781c1 | 9 | * Steve French (sfrench@us.ibm.com) |
6d5ae0de IM |
10 | */ |
11 | ||
12 | #include <linux/dcache.h> | |
13 | #include <linux/mount.h> | |
14 | #include <linux/namei.h> | |
5a0e3ad6 | 15 | #include <linux/slab.h> |
6d5ae0de IM |
16 | #include <linux/vfs.h> |
17 | #include <linux/fs.h> | |
166faf21 | 18 | #include <linux/inet.h> |
6d5ae0de IM |
19 | #include "cifsglob.h" |
20 | #include "cifsproto.h" | |
21 | #include "cifsfs.h" | |
22 | #include "dns_resolve.h" | |
23 | #include "cifs_debug.h" | |
bc8ebdc4 | 24 | #include "cifs_unicode.h" |
1c780228 | 25 | #include "dfs_cache.h" |
6d5ae0de | 26 | |
8d142137 | 27 | static LIST_HEAD(cifs_dfs_automount_list); |
6d5ae0de | 28 | |
78d31a3a IM |
29 | static void cifs_dfs_expire_automounts(struct work_struct *work); |
30 | static DECLARE_DELAYED_WORK(cifs_dfs_automount_task, | |
31 | cifs_dfs_expire_automounts); | |
32 | static int cifs_dfs_mountpoint_expiry_timeout = 500 * HZ; | |
33 | ||
34 | static void cifs_dfs_expire_automounts(struct work_struct *work) | |
35 | { | |
36 | struct list_head *list = &cifs_dfs_automount_list; | |
37 | ||
38 | mark_mounts_for_expiry(list); | |
39 | if (!list_empty(list)) | |
40 | schedule_delayed_work(&cifs_dfs_automount_task, | |
41 | cifs_dfs_mountpoint_expiry_timeout); | |
42 | } | |
6d5ae0de | 43 | |
78d31a3a | 44 | void cifs_dfs_release_automount_timer(void) |
6d5ae0de | 45 | { |
78d31a3a | 46 | BUG_ON(!list_empty(&cifs_dfs_automount_list)); |
3e24e132 | 47 | cancel_delayed_work_sync(&cifs_dfs_automount_task); |
6d5ae0de IM |
48 | } |
49 | ||
50 | /** | |
d9deef0a JL |
51 | * cifs_build_devname - build a devicename from a UNC and optional prepath |
52 | * @nodename: pointer to UNC string | |
53 | * @prepath: pointer to prefixpath (or NULL if there isn't one) | |
6d5ae0de | 54 | * |
d9deef0a JL |
55 | * Build a new cifs devicename after chasing a DFS referral. Allocate a buffer |
56 | * big enough to hold the final thing. Copy the UNC from the nodename, and | |
57 | * concatenate the prepath onto the end of it if there is one. | |
58 | * | |
59 | * Returns pointer to the built string, or a ERR_PTR. Caller is responsible | |
60 | * for freeing the returned string. | |
6d5ae0de | 61 | */ |
d9deef0a JL |
62 | static char * |
63 | cifs_build_devname(char *nodename, const char *prepath) | |
6d5ae0de | 64 | { |
d9deef0a JL |
65 | size_t pplen; |
66 | size_t unclen; | |
67 | char *dev; | |
68 | char *pos; | |
6d5ae0de | 69 | |
d9deef0a JL |
70 | /* skip over any preceding delimiters */ |
71 | nodename += strspn(nodename, "\\"); | |
72 | if (!*nodename) | |
7b91e266 | 73 | return ERR_PTR(-EINVAL); |
d9deef0a JL |
74 | |
75 | /* get length of UNC and set pos to last char */ | |
76 | unclen = strlen(nodename); | |
77 | pos = nodename + unclen - 1; | |
78 | ||
79 | /* trim off any trailing delimiters */ | |
80 | while (*pos == '\\') { | |
81 | --pos; | |
82 | --unclen; | |
6d5ae0de IM |
83 | } |
84 | ||
d9deef0a JL |
85 | /* allocate a buffer: |
86 | * +2 for preceding "//" | |
87 | * +1 for delimiter between UNC and prepath | |
88 | * +1 for trailing NULL | |
89 | */ | |
90 | pplen = prepath ? strlen(prepath) : 0; | |
91 | dev = kmalloc(2 + unclen + 1 + pplen + 1, GFP_KERNEL); | |
92 | if (!dev) | |
93 | return ERR_PTR(-ENOMEM); | |
94 | ||
95 | pos = dev; | |
96 | /* add the initial "//" */ | |
97 | *pos = '/'; | |
98 | ++pos; | |
99 | *pos = '/'; | |
100 | ++pos; | |
101 | ||
102 | /* copy in the UNC portion from referral */ | |
103 | memcpy(pos, nodename, unclen); | |
104 | pos += unclen; | |
105 | ||
106 | /* copy the prefixpath remainder (if there is one) */ | |
107 | if (pplen) { | |
108 | *pos = '/'; | |
109 | ++pos; | |
110 | memcpy(pos, prepath, pplen); | |
111 | pos += pplen; | |
6d5ae0de | 112 | } |
6d5ae0de | 113 | |
d9deef0a JL |
114 | /* NULL terminator */ |
115 | *pos = '\0'; | |
116 | ||
117 | convert_delimiter(dev, '/'); | |
118 | return dev; | |
6d5ae0de IM |
119 | } |
120 | ||
121 | ||
122 | /** | |
c6c00919 | 123 | * cifs_compose_mount_options - creates mount options for refferral |
6d5ae0de | 124 | * @sb_mountdata: parent/root DFS mount options (template) |
c6c00919 | 125 | * @fullpath: full path in UNC format |
2c55608f | 126 | * @ref: server's referral |
d9345e0a | 127 | * @devname: optional pointer for saving device name |
6d5ae0de IM |
128 | * |
129 | * creates mount options for submount based on template options sb_mountdata | |
130 | * and replacing unc,ip,prefixpath options with ones we've got form ref_unc. | |
131 | * | |
132 | * Returns: pointer to new mount options or ERR_PTR. | |
133 | * Caller is responcible for freeing retunrned value if it is not error. | |
134 | */ | |
c6c00919 SF |
135 | char *cifs_compose_mount_options(const char *sb_mountdata, |
136 | const char *fullpath, | |
2c55608f | 137 | const struct dfs_info3_param *ref, |
366781c1 | 138 | char **devname) |
6d5ae0de IM |
139 | { |
140 | int rc; | |
d9345e0a | 141 | char *name; |
c6fbba05 | 142 | char *mountdata = NULL; |
d9deef0a | 143 | const char *prepath = NULL; |
6d5ae0de IM |
144 | int md_len; |
145 | char *tkn_e; | |
146 | char *srvIP = NULL; | |
147 | char sep = ','; | |
148 | int off, noff; | |
149 | ||
150 | if (sb_mountdata == NULL) | |
151 | return ERR_PTR(-EINVAL); | |
152 | ||
11e31647 | 153 | if (strlen(fullpath) - ref->path_consumed) { |
d9deef0a | 154 | prepath = fullpath + ref->path_consumed; |
11e31647 SP |
155 | /* skip initial delimiter */ |
156 | if (*prepath == '/' || *prepath == '\\') | |
157 | prepath++; | |
158 | } | |
d9deef0a | 159 | |
d9345e0a PA |
160 | name = cifs_build_devname(ref->node_name, prepath); |
161 | if (IS_ERR(name)) { | |
162 | rc = PTR_ERR(name); | |
163 | name = NULL; | |
7b91e266 JL |
164 | goto compose_mount_options_err; |
165 | } | |
166 | ||
d9345e0a | 167 | rc = dns_resolve_server_name_to_ip(name, &srvIP); |
67b7626a | 168 | if (rc < 0) { |
f96637be | 169 | cifs_dbg(FYI, "%s: Failed to resolve server part of %s to IP: %d\n", |
d9345e0a | 170 | __func__, name, rc); |
c6fbba05 | 171 | goto compose_mount_options_err; |
6d5ae0de | 172 | } |
b8028983 | 173 | |
d9deef0a JL |
174 | /* |
175 | * In most cases, we'll be building a shorter string than the original, | |
176 | * but we do have to assume that the address in the ip= option may be | |
177 | * much longer than the original. Add the max length of an address | |
178 | * string to the length of the original string to allow for worst case. | |
2c55608f | 179 | */ |
d9deef0a | 180 | md_len = strlen(sb_mountdata) + INET6_ADDRSTRLEN; |
f34d69c3 | 181 | mountdata = kzalloc(md_len + sizeof("ip=") + 1, GFP_KERNEL); |
6d5ae0de | 182 | if (mountdata == NULL) { |
c6fbba05 SF |
183 | rc = -ENOMEM; |
184 | goto compose_mount_options_err; | |
6d5ae0de IM |
185 | } |
186 | ||
187 | /* copy all options except of unc,ip,prefixpath */ | |
188 | off = 0; | |
189 | if (strncmp(sb_mountdata, "sep=", 4) == 0) { | |
190 | sep = sb_mountdata[4]; | |
191 | strncpy(mountdata, sb_mountdata, 5); | |
192 | off += 5; | |
193 | } | |
2c55608f IM |
194 | |
195 | do { | |
196 | tkn_e = strchr(sb_mountdata + off, sep); | |
197 | if (tkn_e == NULL) | |
198 | noff = strlen(sb_mountdata + off); | |
199 | else | |
200 | noff = tkn_e - (sb_mountdata + off) + 1; | |
201 | ||
87e747cd | 202 | if (strncasecmp(sb_mountdata + off, "unc=", 4) == 0) { |
6d5ae0de IM |
203 | off += noff; |
204 | continue; | |
205 | } | |
87e747cd | 206 | if (strncasecmp(sb_mountdata + off, "ip=", 3) == 0) { |
6d5ae0de IM |
207 | off += noff; |
208 | continue; | |
209 | } | |
87e747cd | 210 | if (strncasecmp(sb_mountdata + off, "prefixpath=", 11) == 0) { |
6d5ae0de IM |
211 | off += noff; |
212 | continue; | |
213 | } | |
2c55608f | 214 | strncat(mountdata, sb_mountdata + off, noff); |
6d5ae0de | 215 | off += noff; |
2c55608f IM |
216 | } while (tkn_e); |
217 | strcat(mountdata, sb_mountdata + off); | |
6d5ae0de IM |
218 | mountdata[md_len] = '\0'; |
219 | ||
220 | /* copy new IP and ref share name */ | |
2c55608f IM |
221 | if (mountdata[strlen(mountdata) - 1] != sep) |
222 | strncat(mountdata, &sep, 1); | |
223 | strcat(mountdata, "ip="); | |
6d5ae0de | 224 | strcat(mountdata, srvIP); |
6d5ae0de | 225 | |
d9345e0a PA |
226 | if (devname) |
227 | *devname = name; | |
228 | ||
f96637be JP |
229 | /*cifs_dbg(FYI, "%s: parent mountdata: %s\n", __func__, sb_mountdata);*/ |
230 | /*cifs_dbg(FYI, "%s: submount mountdata: %s\n", __func__, mountdata );*/ | |
6d5ae0de IM |
231 | |
232 | compose_mount_options_out: | |
233 | kfree(srvIP); | |
234 | return mountdata; | |
c6fbba05 SF |
235 | |
236 | compose_mount_options_err: | |
237 | kfree(mountdata); | |
238 | mountdata = ERR_PTR(rc); | |
d9345e0a | 239 | kfree(name); |
c6fbba05 | 240 | goto compose_mount_options_out; |
6d5ae0de IM |
241 | } |
242 | ||
f67909cf SF |
243 | /** |
244 | * cifs_dfs_do_refmount - mounts specified path using provided refferal | |
245 | * @cifs_sb: parent/root superblock | |
246 | * @fullpath: full path in UNC format | |
247 | * @ref: server's referral | |
248 | */ | |
93faccbb EB |
249 | static struct vfsmount *cifs_dfs_do_refmount(struct dentry *mntpt, |
250 | struct cifs_sb_info *cifs_sb, | |
f67909cf | 251 | const char *fullpath, const struct dfs_info3_param *ref) |
6d5ae0de | 252 | { |
6d5ae0de IM |
253 | struct vfsmount *mnt; |
254 | char *mountdata; | |
4a367dc0 PA |
255 | char *devname; |
256 | ||
257 | /* | |
258 | * Always pass down the DFS full path to smb3_do_mount() so we | |
259 | * can use it later for failover. | |
260 | */ | |
261 | devname = kstrndup(fullpath, strlen(fullpath), GFP_KERNEL); | |
262 | if (!devname) | |
263 | return ERR_PTR(-ENOMEM); | |
264 | ||
265 | convert_delimiter(devname, '/'); | |
c6c00919 | 266 | |
f67909cf | 267 | /* strip first '\' from fullpath */ |
c6c00919 | 268 | mountdata = cifs_compose_mount_options(cifs_sb->mountdata, |
4a367dc0 PA |
269 | fullpath + 1, ref, NULL); |
270 | if (IS_ERR(mountdata)) { | |
271 | kfree(devname); | |
6d5ae0de | 272 | return (struct vfsmount *)mountdata; |
4a367dc0 | 273 | } |
6d5ae0de | 274 | |
93faccbb | 275 | mnt = vfs_submount(mntpt, &cifs_fs_type, devname, mountdata); |
6d5ae0de IM |
276 | kfree(mountdata); |
277 | kfree(devname); | |
278 | return mnt; | |
6d5ae0de IM |
279 | } |
280 | ||
366781c1 | 281 | static void dump_referral(const struct dfs_info3_param *ref) |
6d5ae0de | 282 | { |
f96637be JP |
283 | cifs_dbg(FYI, "DFS: ref path: %s\n", ref->path_name); |
284 | cifs_dbg(FYI, "DFS: node path: %s\n", ref->node_name); | |
259594be | 285 | cifs_dbg(FYI, "DFS: fl: %d, srv_type: %d\n", |
f96637be | 286 | ref->flags, ref->server_type); |
259594be | 287 | cifs_dbg(FYI, "DFS: ref_flags: %d, path_consumed: %d\n", |
f96637be | 288 | ref->ref_flag, ref->path_consumed); |
6d5ae0de IM |
289 | } |
290 | ||
01c64fea DH |
291 | /* |
292 | * Create a vfsmount that we can automount | |
293 | */ | |
294 | static struct vfsmount *cifs_dfs_do_automount(struct dentry *mntpt) | |
6d5ae0de | 295 | { |
1c780228 | 296 | struct dfs_info3_param referral = {0}; |
6d5ae0de | 297 | struct cifs_sb_info *cifs_sb; |
96daf2b0 | 298 | struct cifs_ses *ses; |
1c780228 PA |
299 | struct cifs_tcon *tcon; |
300 | char *full_path, *root_path; | |
6d5786a3 | 301 | unsigned int xid; |
1c780228 | 302 | int len; |
01c64fea DH |
303 | int rc; |
304 | struct vfsmount *mnt; | |
6d5ae0de | 305 | |
f96637be | 306 | cifs_dbg(FYI, "in %s\n", __func__); |
01c64fea | 307 | BUG_ON(IS_ROOT(mntpt)); |
6d5ae0de | 308 | |
c6fbba05 SF |
309 | /* |
310 | * The MSDFS spec states that paths in DFS referral requests and | |
311 | * responses must be prefixed by a single '\' character instead of | |
312 | * the double backslashes usually used in the UNC. This function | |
313 | * gives us the latter, so we must adjust the result. | |
314 | */ | |
01c64fea | 315 | mnt = ERR_PTR(-ENOMEM); |
268a635d | 316 | |
8393072b AA |
317 | cifs_sb = CIFS_SB(mntpt->d_sb); |
318 | if (cifs_sb->mnt_cifs_flags & CIFS_MOUNT_NO_DFS) { | |
319 | mnt = ERR_PTR(-EREMOTE); | |
320 | goto cdda_exit; | |
321 | } | |
322 | ||
268a635d AA |
323 | /* always use tree name prefix */ |
324 | full_path = build_path_from_dentry_optional_prefix(mntpt, true); | |
01c64fea | 325 | if (full_path == NULL) |
31c2659d | 326 | goto cdda_exit; |
6d5ae0de | 327 | |
1c780228 PA |
328 | cifs_dbg(FYI, "%s: full_path: %s\n", __func__, full_path); |
329 | ||
330 | if (!cifs_sb_master_tlink(cifs_sb)) { | |
331 | cifs_dbg(FYI, "%s: master tlink is NULL\n", __func__); | |
332 | goto free_full_path; | |
333 | } | |
334 | ||
335 | tcon = cifs_sb_master_tcon(cifs_sb); | |
336 | if (!tcon) { | |
337 | cifs_dbg(FYI, "%s: master tcon is NULL\n", __func__); | |
01c64fea | 338 | goto free_full_path; |
7ffec372 | 339 | } |
7ffec372 | 340 | |
1c780228 PA |
341 | root_path = kstrdup(tcon->treeName, GFP_KERNEL); |
342 | if (!root_path) { | |
343 | mnt = ERR_PTR(-ENOMEM); | |
344 | goto free_full_path; | |
345 | } | |
346 | cifs_dbg(FYI, "%s: root path: %s\n", __func__, root_path); | |
347 | ||
348 | ses = tcon->ses; | |
6d5786a3 | 349 | xid = get_xid(); |
6d5ae0de | 350 | |
1c780228 PA |
351 | /* |
352 | * If DFS root has been expired, then unconditionally fetch it again to | |
353 | * refresh DFS referral cache. | |
354 | */ | |
355 | rc = dfs_cache_find(xid, ses, cifs_sb->local_nls, cifs_remap(cifs_sb), | |
356 | root_path + 1, NULL, NULL); | |
357 | if (!rc) { | |
358 | rc = dfs_cache_find(xid, ses, cifs_sb->local_nls, | |
359 | cifs_remap(cifs_sb), full_path + 1, | |
360 | &referral, NULL); | |
6d5ae0de IM |
361 | } |
362 | ||
1c780228 PA |
363 | free_xid(xid); |
364 | ||
365 | if (rc) { | |
01c64fea | 366 | mnt = ERR_PTR(rc); |
1c780228 PA |
367 | goto free_root_path; |
368 | } | |
6d5ae0de | 369 | |
1c780228 PA |
370 | dump_referral(&referral); |
371 | ||
372 | len = strlen(referral.node_name); | |
373 | if (len < 2) { | |
374 | cifs_dbg(VFS, "%s: Net Address path too short: %s\n", | |
375 | __func__, referral.node_name); | |
376 | mnt = ERR_PTR(-EINVAL); | |
377 | goto free_dfs_ref; | |
378 | } | |
379 | /* | |
380 | * cifs_mount() will retry every available node server in case | |
381 | * of failures. | |
382 | */ | |
383 | mnt = cifs_dfs_do_refmount(mntpt, cifs_sb, full_path, &referral); | |
384 | cifs_dbg(FYI, "%s: cifs_dfs_do_refmount:%s , mnt:%p\n", __func__, | |
385 | referral.node_name, mnt); | |
386 | ||
387 | free_dfs_ref: | |
388 | free_dfs_info_param(&referral); | |
389 | free_root_path: | |
390 | kfree(root_path); | |
01c64fea | 391 | free_full_path: |
6d5ae0de | 392 | kfree(full_path); |
31c2659d | 393 | cdda_exit: |
f96637be | 394 | cifs_dbg(FYI, "leaving %s\n" , __func__); |
01c64fea DH |
395 | return mnt; |
396 | } | |
397 | ||
398 | /* | |
399 | * Attempt to automount the referral | |
400 | */ | |
401 | struct vfsmount *cifs_dfs_d_automount(struct path *path) | |
402 | { | |
403 | struct vfsmount *newmnt; | |
01c64fea | 404 | |
f96637be | 405 | cifs_dbg(FYI, "in %s\n", __func__); |
01c64fea DH |
406 | |
407 | newmnt = cifs_dfs_do_automount(path->dentry); | |
408 | if (IS_ERR(newmnt)) { | |
f96637be | 409 | cifs_dbg(FYI, "leaving %s [automount failed]\n" , __func__); |
01c64fea DH |
410 | return newmnt; |
411 | } | |
412 | ||
ea5b778a DH |
413 | mntget(newmnt); /* prevent immediate expiration */ |
414 | mnt_set_expiry(newmnt, &cifs_dfs_automount_list); | |
415 | schedule_delayed_work(&cifs_dfs_automount_task, | |
416 | cifs_dfs_mountpoint_expiry_timeout); | |
f96637be | 417 | cifs_dbg(FYI, "leaving %s [ok]\n" , __func__); |
ea5b778a | 418 | return newmnt; |
6d5ae0de IM |
419 | } |
420 | ||
6e1d5dcc | 421 | const struct inode_operations cifs_dfs_referral_inode_operations = { |
6d5ae0de | 422 | }; |