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" |
a2a52a8a | 26 | #include "fs_context.h" |
6d5ae0de | 27 | |
8d142137 | 28 | static LIST_HEAD(cifs_dfs_automount_list); |
6d5ae0de | 29 | |
78d31a3a IM |
30 | static void cifs_dfs_expire_automounts(struct work_struct *work); |
31 | static DECLARE_DELAYED_WORK(cifs_dfs_automount_task, | |
32 | cifs_dfs_expire_automounts); | |
33 | static int cifs_dfs_mountpoint_expiry_timeout = 500 * HZ; | |
34 | ||
35 | static void cifs_dfs_expire_automounts(struct work_struct *work) | |
36 | { | |
37 | struct list_head *list = &cifs_dfs_automount_list; | |
38 | ||
39 | mark_mounts_for_expiry(list); | |
40 | if (!list_empty(list)) | |
41 | schedule_delayed_work(&cifs_dfs_automount_task, | |
42 | cifs_dfs_mountpoint_expiry_timeout); | |
43 | } | |
6d5ae0de | 44 | |
78d31a3a | 45 | void cifs_dfs_release_automount_timer(void) |
6d5ae0de | 46 | { |
78d31a3a | 47 | BUG_ON(!list_empty(&cifs_dfs_automount_list)); |
3e24e132 | 48 | cancel_delayed_work_sync(&cifs_dfs_automount_task); |
6d5ae0de IM |
49 | } |
50 | ||
51 | /** | |
d9deef0a JL |
52 | * cifs_build_devname - build a devicename from a UNC and optional prepath |
53 | * @nodename: pointer to UNC string | |
54 | * @prepath: pointer to prefixpath (or NULL if there isn't one) | |
6d5ae0de | 55 | * |
d9deef0a JL |
56 | * Build a new cifs devicename after chasing a DFS referral. Allocate a buffer |
57 | * big enough to hold the final thing. Copy the UNC from the nodename, and | |
58 | * concatenate the prepath onto the end of it if there is one. | |
59 | * | |
60 | * Returns pointer to the built string, or a ERR_PTR. Caller is responsible | |
61 | * for freeing the returned string. | |
6d5ae0de | 62 | */ |
d9deef0a JL |
63 | static char * |
64 | cifs_build_devname(char *nodename, const char *prepath) | |
6d5ae0de | 65 | { |
d9deef0a JL |
66 | size_t pplen; |
67 | size_t unclen; | |
68 | char *dev; | |
69 | char *pos; | |
6d5ae0de | 70 | |
d9deef0a JL |
71 | /* skip over any preceding delimiters */ |
72 | nodename += strspn(nodename, "\\"); | |
73 | if (!*nodename) | |
7b91e266 | 74 | return ERR_PTR(-EINVAL); |
d9deef0a JL |
75 | |
76 | /* get length of UNC and set pos to last char */ | |
77 | unclen = strlen(nodename); | |
78 | pos = nodename + unclen - 1; | |
79 | ||
80 | /* trim off any trailing delimiters */ | |
81 | while (*pos == '\\') { | |
82 | --pos; | |
83 | --unclen; | |
6d5ae0de IM |
84 | } |
85 | ||
d9deef0a JL |
86 | /* allocate a buffer: |
87 | * +2 for preceding "//" | |
88 | * +1 for delimiter between UNC and prepath | |
89 | * +1 for trailing NULL | |
90 | */ | |
91 | pplen = prepath ? strlen(prepath) : 0; | |
92 | dev = kmalloc(2 + unclen + 1 + pplen + 1, GFP_KERNEL); | |
93 | if (!dev) | |
94 | return ERR_PTR(-ENOMEM); | |
95 | ||
96 | pos = dev; | |
97 | /* add the initial "//" */ | |
98 | *pos = '/'; | |
99 | ++pos; | |
100 | *pos = '/'; | |
101 | ++pos; | |
102 | ||
103 | /* copy in the UNC portion from referral */ | |
104 | memcpy(pos, nodename, unclen); | |
105 | pos += unclen; | |
106 | ||
107 | /* copy the prefixpath remainder (if there is one) */ | |
108 | if (pplen) { | |
109 | *pos = '/'; | |
110 | ++pos; | |
111 | memcpy(pos, prepath, pplen); | |
112 | pos += pplen; | |
6d5ae0de | 113 | } |
6d5ae0de | 114 | |
d9deef0a JL |
115 | /* NULL terminator */ |
116 | *pos = '\0'; | |
117 | ||
118 | convert_delimiter(dev, '/'); | |
119 | return dev; | |
6d5ae0de IM |
120 | } |
121 | ||
122 | ||
123 | /** | |
5739375e | 124 | * cifs_compose_mount_options - creates mount options for referral |
6d5ae0de | 125 | * @sb_mountdata: parent/root DFS mount options (template) |
c6c00919 | 126 | * @fullpath: full path in UNC format |
5739375e | 127 | * @ref: optional server's referral |
aaf36df3 | 128 | * @devname: return the built cifs device name if passed pointer not NULL |
6d5ae0de IM |
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. | |
5739375e | 133 | * Caller is responsible for freeing returned value if it is not error. |
6d5ae0de | 134 | */ |
c6c00919 | 135 | char *cifs_compose_mount_options(const char *sb_mountdata, |
0d4873f9 RS |
136 | const char *fullpath, |
137 | const struct dfs_info3_param *ref, | |
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 | ||
5739375e | 153 | if (ref) { |
03313d1c PA |
154 | if (WARN_ON_ONCE(!ref->node_name || ref->path_consumed < 0)) |
155 | return ERR_PTR(-EINVAL); | |
156 | ||
5739375e PAS |
157 | if (strlen(fullpath) - ref->path_consumed) { |
158 | prepath = fullpath + ref->path_consumed; | |
159 | /* skip initial delimiter */ | |
160 | if (*prepath == '/' || *prepath == '\\') | |
161 | prepath++; | |
162 | } | |
d9deef0a | 163 | |
5739375e PAS |
164 | name = cifs_build_devname(ref->node_name, prepath); |
165 | if (IS_ERR(name)) { | |
166 | rc = PTR_ERR(name); | |
167 | name = NULL; | |
168 | goto compose_mount_options_err; | |
169 | } | |
170 | } else { | |
171 | name = cifs_build_devname((char *)fullpath, NULL); | |
172 | if (IS_ERR(name)) { | |
173 | rc = PTR_ERR(name); | |
174 | name = NULL; | |
175 | goto compose_mount_options_err; | |
176 | } | |
7b91e266 JL |
177 | } |
178 | ||
506c1da4 | 179 | rc = dns_resolve_server_name_to_ip(name, &srvIP, NULL); |
67b7626a | 180 | if (rc < 0) { |
f96637be | 181 | cifs_dbg(FYI, "%s: Failed to resolve server part of %s to IP: %d\n", |
d9345e0a | 182 | __func__, name, rc); |
c6fbba05 | 183 | goto compose_mount_options_err; |
6d5ae0de | 184 | } |
b8028983 | 185 | |
d9deef0a JL |
186 | /* |
187 | * In most cases, we'll be building a shorter string than the original, | |
188 | * but we do have to assume that the address in the ip= option may be | |
189 | * much longer than the original. Add the max length of an address | |
190 | * string to the length of the original string to allow for worst case. | |
2c55608f | 191 | */ |
d9deef0a | 192 | md_len = strlen(sb_mountdata) + INET6_ADDRSTRLEN; |
f34d69c3 | 193 | mountdata = kzalloc(md_len + sizeof("ip=") + 1, GFP_KERNEL); |
6d5ae0de | 194 | if (mountdata == NULL) { |
c6fbba05 SF |
195 | rc = -ENOMEM; |
196 | goto compose_mount_options_err; | |
6d5ae0de IM |
197 | } |
198 | ||
199 | /* copy all options except of unc,ip,prefixpath */ | |
200 | off = 0; | |
201 | if (strncmp(sb_mountdata, "sep=", 4) == 0) { | |
202 | sep = sb_mountdata[4]; | |
203 | strncpy(mountdata, sb_mountdata, 5); | |
204 | off += 5; | |
205 | } | |
2c55608f IM |
206 | |
207 | do { | |
208 | tkn_e = strchr(sb_mountdata + off, sep); | |
209 | if (tkn_e == NULL) | |
210 | noff = strlen(sb_mountdata + off); | |
211 | else | |
212 | noff = tkn_e - (sb_mountdata + off) + 1; | |
213 | ||
50630b3f RS |
214 | if (strncasecmp(sb_mountdata + off, "cruid=", 6) == 0) { |
215 | off += noff; | |
216 | continue; | |
217 | } | |
87e747cd | 218 | if (strncasecmp(sb_mountdata + off, "unc=", 4) == 0) { |
6d5ae0de IM |
219 | off += noff; |
220 | continue; | |
221 | } | |
87e747cd | 222 | if (strncasecmp(sb_mountdata + off, "ip=", 3) == 0) { |
6d5ae0de IM |
223 | off += noff; |
224 | continue; | |
225 | } | |
87e747cd | 226 | if (strncasecmp(sb_mountdata + off, "prefixpath=", 11) == 0) { |
6d5ae0de IM |
227 | off += noff; |
228 | continue; | |
229 | } | |
2c55608f | 230 | strncat(mountdata, sb_mountdata + off, noff); |
6d5ae0de | 231 | off += noff; |
2c55608f IM |
232 | } while (tkn_e); |
233 | strcat(mountdata, sb_mountdata + off); | |
6d5ae0de IM |
234 | mountdata[md_len] = '\0'; |
235 | ||
236 | /* copy new IP and ref share name */ | |
2c55608f IM |
237 | if (mountdata[strlen(mountdata) - 1] != sep) |
238 | strncat(mountdata, &sep, 1); | |
239 | strcat(mountdata, "ip="); | |
6d5ae0de | 240 | strcat(mountdata, srvIP); |
6d5ae0de | 241 | |
0d4873f9 RS |
242 | if (devname) |
243 | *devname = name; | |
244 | else | |
245 | kfree(name); | |
d9345e0a | 246 | |
f96637be JP |
247 | /*cifs_dbg(FYI, "%s: parent mountdata: %s\n", __func__, sb_mountdata);*/ |
248 | /*cifs_dbg(FYI, "%s: submount mountdata: %s\n", __func__, mountdata );*/ | |
6d5ae0de IM |
249 | |
250 | compose_mount_options_out: | |
251 | kfree(srvIP); | |
252 | return mountdata; | |
c6fbba05 SF |
253 | |
254 | compose_mount_options_err: | |
255 | kfree(mountdata); | |
256 | mountdata = ERR_PTR(rc); | |
d9345e0a | 257 | kfree(name); |
c6fbba05 | 258 | goto compose_mount_options_out; |
6d5ae0de IM |
259 | } |
260 | ||
f67909cf | 261 | /** |
5739375e PAS |
262 | * cifs_dfs_do_mount - mounts specified path using DFS full path |
263 | * | |
264 | * Always pass down @fullpath to smb3_do_mount() so we can use the root server | |
265 | * to perform failover in case we failed to connect to the first target in the | |
266 | * referral. | |
267 | * | |
607dfc79 | 268 | * @mntpt: directory entry for the path we are trying to automount |
f67909cf SF |
269 | * @cifs_sb: parent/root superblock |
270 | * @fullpath: full path in UNC format | |
f67909cf | 271 | */ |
5739375e PAS |
272 | static struct vfsmount *cifs_dfs_do_mount(struct dentry *mntpt, |
273 | struct cifs_sb_info *cifs_sb, | |
274 | const char *fullpath) | |
6d5ae0de | 275 | { |
6d5ae0de IM |
276 | struct vfsmount *mnt; |
277 | char *mountdata; | |
4a367dc0 PA |
278 | char *devname; |
279 | ||
8d767223 | 280 | devname = kstrdup(fullpath, GFP_KERNEL); |
4a367dc0 PA |
281 | if (!devname) |
282 | return ERR_PTR(-ENOMEM); | |
283 | ||
284 | convert_delimiter(devname, '/'); | |
c6c00919 | 285 | |
24e0a1ef RS |
286 | /* TODO: change to call fs_context_for_mount(), fill in context directly, call fc_mount */ |
287 | ||
288 | /* See afs_mntpt_do_automount in fs/afs/mntpt.c for an example */ | |
289 | ||
f67909cf | 290 | /* strip first '\' from fullpath */ |
a2a52a8a | 291 | mountdata = cifs_compose_mount_options(cifs_sb->ctx->mount_options, |
0d4873f9 | 292 | fullpath + 1, NULL, NULL); |
4a367dc0 PA |
293 | if (IS_ERR(mountdata)) { |
294 | kfree(devname); | |
6d5ae0de | 295 | return (struct vfsmount *)mountdata; |
4a367dc0 | 296 | } |
6d5ae0de | 297 | |
93faccbb | 298 | mnt = vfs_submount(mntpt, &cifs_fs_type, devname, mountdata); |
6d5ae0de IM |
299 | kfree(mountdata); |
300 | kfree(devname); | |
301 | return mnt; | |
6d5ae0de IM |
302 | } |
303 | ||
01c64fea DH |
304 | /* |
305 | * Create a vfsmount that we can automount | |
306 | */ | |
307 | static struct vfsmount *cifs_dfs_do_automount(struct dentry *mntpt) | |
6d5ae0de | 308 | { |
6d5ae0de | 309 | struct cifs_sb_info *cifs_sb; |
96daf2b0 | 310 | struct cifs_ses *ses; |
1c780228 | 311 | struct cifs_tcon *tcon; |
f6a9bc33 | 312 | void *page; |
1c780228 | 313 | char *full_path, *root_path; |
6d5786a3 | 314 | unsigned int xid; |
01c64fea DH |
315 | int rc; |
316 | struct vfsmount *mnt; | |
6d5ae0de | 317 | |
f96637be | 318 | cifs_dbg(FYI, "in %s\n", __func__); |
01c64fea | 319 | BUG_ON(IS_ROOT(mntpt)); |
6d5ae0de | 320 | |
c6fbba05 SF |
321 | /* |
322 | * The MSDFS spec states that paths in DFS referral requests and | |
323 | * responses must be prefixed by a single '\' character instead of | |
324 | * the double backslashes usually used in the UNC. This function | |
325 | * gives us the latter, so we must adjust the result. | |
326 | */ | |
01c64fea | 327 | mnt = ERR_PTR(-ENOMEM); |
268a635d | 328 | |
8393072b AA |
329 | cifs_sb = CIFS_SB(mntpt->d_sb); |
330 | if (cifs_sb->mnt_cifs_flags & CIFS_MOUNT_NO_DFS) { | |
331 | mnt = ERR_PTR(-EREMOTE); | |
332 | goto cdda_exit; | |
333 | } | |
334 | ||
f6a9bc33 | 335 | page = alloc_dentry_path(); |
268a635d | 336 | /* always use tree name prefix */ |
f6a9bc33 AV |
337 | full_path = build_path_from_dentry_optional_prefix(mntpt, page, true); |
338 | if (IS_ERR(full_path)) { | |
339 | mnt = ERR_CAST(full_path); | |
340 | goto free_full_path; | |
341 | } | |
6d5ae0de | 342 | |
15425523 PAS |
343 | convert_delimiter(full_path, '\\'); |
344 | ||
1c780228 PA |
345 | cifs_dbg(FYI, "%s: full_path: %s\n", __func__, full_path); |
346 | ||
347 | if (!cifs_sb_master_tlink(cifs_sb)) { | |
348 | cifs_dbg(FYI, "%s: master tlink is NULL\n", __func__); | |
349 | goto free_full_path; | |
350 | } | |
351 | ||
352 | tcon = cifs_sb_master_tcon(cifs_sb); | |
353 | if (!tcon) { | |
354 | cifs_dbg(FYI, "%s: master tcon is NULL\n", __func__); | |
01c64fea | 355 | goto free_full_path; |
7ffec372 | 356 | } |
7ffec372 | 357 | |
1c780228 PA |
358 | root_path = kstrdup(tcon->treeName, GFP_KERNEL); |
359 | if (!root_path) { | |
360 | mnt = ERR_PTR(-ENOMEM); | |
361 | goto free_full_path; | |
362 | } | |
363 | cifs_dbg(FYI, "%s: root path: %s\n", __func__, root_path); | |
364 | ||
365 | ses = tcon->ses; | |
6d5786a3 | 366 | xid = get_xid(); |
6d5ae0de | 367 | |
1c780228 PA |
368 | /* |
369 | * If DFS root has been expired, then unconditionally fetch it again to | |
370 | * refresh DFS referral cache. | |
371 | */ | |
372 | rc = dfs_cache_find(xid, ses, cifs_sb->local_nls, cifs_remap(cifs_sb), | |
373 | root_path + 1, NULL, NULL); | |
374 | if (!rc) { | |
375 | rc = dfs_cache_find(xid, ses, cifs_sb->local_nls, | |
376 | cifs_remap(cifs_sb), full_path + 1, | |
5739375e | 377 | NULL, NULL); |
6d5ae0de IM |
378 | } |
379 | ||
1c780228 PA |
380 | free_xid(xid); |
381 | ||
382 | if (rc) { | |
01c64fea | 383 | mnt = ERR_PTR(rc); |
1c780228 PA |
384 | goto free_root_path; |
385 | } | |
1c780228 | 386 | /* |
5739375e PAS |
387 | * OK - we were able to get and cache a referral for @full_path. |
388 | * | |
389 | * Now, pass it down to cifs_mount() and it will retry every available | |
390 | * node server in case of failures - no need to do it here. | |
1c780228 | 391 | */ |
5739375e PAS |
392 | mnt = cifs_dfs_do_mount(mntpt, cifs_sb, full_path); |
393 | cifs_dbg(FYI, "%s: cifs_dfs_do_mount:%s , mnt:%p\n", __func__, | |
394 | full_path + 1, mnt); | |
1c780228 | 395 | |
1c780228 PA |
396 | free_root_path: |
397 | kfree(root_path); | |
01c64fea | 398 | free_full_path: |
f6a9bc33 | 399 | free_dentry_path(page); |
31c2659d | 400 | cdda_exit: |
f96637be | 401 | cifs_dbg(FYI, "leaving %s\n" , __func__); |
01c64fea DH |
402 | return mnt; |
403 | } | |
404 | ||
405 | /* | |
406 | * Attempt to automount the referral | |
407 | */ | |
408 | struct vfsmount *cifs_dfs_d_automount(struct path *path) | |
409 | { | |
410 | struct vfsmount *newmnt; | |
01c64fea | 411 | |
f96637be | 412 | cifs_dbg(FYI, "in %s\n", __func__); |
01c64fea DH |
413 | |
414 | newmnt = cifs_dfs_do_automount(path->dentry); | |
415 | if (IS_ERR(newmnt)) { | |
f96637be | 416 | cifs_dbg(FYI, "leaving %s [automount failed]\n" , __func__); |
01c64fea DH |
417 | return newmnt; |
418 | } | |
419 | ||
ea5b778a DH |
420 | mntget(newmnt); /* prevent immediate expiration */ |
421 | mnt_set_expiry(newmnt, &cifs_dfs_automount_list); | |
422 | schedule_delayed_work(&cifs_dfs_automount_task, | |
423 | cifs_dfs_mountpoint_expiry_timeout); | |
f96637be | 424 | cifs_dbg(FYI, "leaving %s [ok]\n" , __func__); |
ea5b778a | 425 | return newmnt; |
6d5ae0de IM |
426 | } |
427 | ||
6e1d5dcc | 428 | const struct inode_operations cifs_dfs_referral_inode_operations = { |
6d5ae0de | 429 | }; |