smb2_durable.c revision 55f0a249fd3511728b02627190771a4ce4ddf20e
1/*
2 * This file and its contents are supplied under the terms of the
3 * Common Development and Distribution License ("CDDL"), version 1.0.
4 * You may only use this file in accordance with the terms of version
5 * 1.0 of the CDDL.
6 *
7 * A full copy of the text of the CDDL should have accompanied this
8 * source.  A copy of the CDDL is also available via the Internet at
9 * http://www.illumos.org/license/CDDL.
10 */
11
12/*
13 * Copyright 2017 Nexenta Systems, Inc.  All rights reserved.
14 */
15
16/*
17 * SMB2 Durable Handle support
18 */
19
20#include <sys/types.h>
21#include <sys/cmn_err.h>
22#include <sys/fcntl.h>
23#include <sys/nbmlock.h>
24#include <smbsrv/string.h>
25#include <smbsrv/smb_kproto.h>
26#include <smbsrv/smb_fsops.h>
27#include <smbsrv/smbinfo.h>
28#include <smbsrv/smb2_kproto.h>
29
30/* Windows default values from [MS-SMB2] */
31/*
32 * (times in seconds)
33 * resilient:
34 * MaxTimeout = 300 (win7+)
35 * if timeout > MaxTimeout, ERROR
36 * if timeout != 0, timeout = req.timeout
37 * if timeout == 0, timeout = (infinity) (Win7/w2k8r2)
38 * if timeout == 0, timeout = 120 (Win8+)
39 * v2:
40 * if timeout != 0, timeout = MIN(timeout, 300) (spec)
41 * if timeout != 0, timeout = timeout (win8/2k12)
42 * if timeout == 0, timeout = Share.CATimeout. \
43 *	if Share.CATimeout == 0, timeout = 60 (win8/w2k12)
44 * if timeout == 0, timeout = 180 (win8.1/w2k12r2)
45 * open.timeout = 60 (win8/w2k12r2) (i.e. we ignore the request)
46 * v1:
47 * open.timeout = 16 minutes
48 */
49
50uint32_t smb2_dh_def_timeout = 60 * MILLISEC;	/* mSec. */
51uint32_t smb2_dh_max_timeout = 300 * MILLISEC;	/* mSec. */
52
53uint32_t smb2_res_def_timeout = 120 * MILLISEC;	/* mSec. */
54uint32_t smb2_res_max_timeout = 300 * MILLISEC;	/* mSec. */
55
56/*
57 * smb_dh_should_save
58 *
59 * During session tear-down, decide whether to keep a durable handle.
60 *
61 * There are two cases where we save durable handles:
62 * 1. An SMB2 LOGOFF request was received
63 * 2. An unexpected disconnect from the client
64 *    Note: Specifying a PrevSessionID in session setup
65 *    is considered a disconnect (we just haven't learned about it yet)
66 * In every other case, we close durable handles.
67 *
68 * [MS-SMB2] 3.3.5.6 SMB2_LOGOFF
69 * [MS-SMB2] 3.3.7.1 Handling Loss of a Connection
70 *
71 * If any of the following are true, preserve for reconnect:
72 *
73 * - Open.IsResilient is TRUE.
74 *
75 * - Open.OplockLevel == SMB2_OPLOCK_LEVEL_BATCH and
76 *   Open.OplockState == Held, and Open.IsDurable is TRUE.
77 *
78 * - Open.OplockLevel == SMB2_OPLOCK_LEVEL_LEASE,
79 *   Lease.LeaseState SMB2_LEASE_HANDLE_CACHING,
80 *   Open.OplockState == Held, and Open.IsDurable is TRUE.
81 *
82 * - Open.IsPersistent is TRUE.
83 */
84boolean_t
85smb_dh_should_save(smb_ofile_t *of)
86{
87	ASSERT(MUTEX_HELD(&of->f_mutex));
88	ASSERT(of->dh_vers != SMB2_NOT_DURABLE);
89
90	if (of->f_user->preserve_opens == SMB2_DH_PRESERVE_NONE)
91		return (B_FALSE);
92
93	if (of->f_user->preserve_opens == SMB2_DH_PRESERVE_ALL)
94		return (B_TRUE);
95
96	switch (of->dh_vers) {
97	case SMB2_RESILIENT:
98		return (B_TRUE);
99
100	case SMB2_DURABLE_V2:
101		if (of->dh_persist)
102			return (B_TRUE);
103		/* FALLTHROUGH */
104	case SMB2_DURABLE_V1:
105		/* IS durable (v1 or v2) */
106		if ((of->f_oplock.og_state & (OPLOCK_LEVEL_BATCH |
107		    OPLOCK_LEVEL_CACHE_HANDLE)) != 0)
108			return (B_TRUE);
109		/* FALLTHROUGH */
110	case SMB2_NOT_DURABLE:
111	default:
112		break;
113	}
114
115	return (B_FALSE);
116}
117
118/*
119 * Requirements for ofile found during reconnect (MS-SMB2 3.3.5.9.7):
120 * - security descriptor must match provided descriptor
121 *
122 * If file is leased:
123 * - lease must be requested
124 * - client guid must match session guid
125 * - file name must match given name
126 * - lease key must match provided lease key
127 * If file is not leased:
128 * - Lease must not be requested
129 *
130 * dh_v2 only:
131 * - SMB2_DHANDLE_FLAG_PERSISTENT must be set if dh_persist is true
132 * - SMB2_DHANDLE_FLAG_PERSISTENT must not be set if dh_persist is false
133 * - desired access, share access, and create_options must be ignored
134 * - createguid must match
135 */
136static uint32_t
137smb2_dh_reconnect_checks(smb_request_t *sr, smb_ofile_t *of)
138{
139	smb_arg_open_t	*op = &sr->sr_open;
140	char *fname;
141
142	if (of->f_lease != NULL) {
143		if (bcmp(sr->session->clnt_uuid,
144		    of->f_lease->ls_clnt, 16) != 0)
145			return (NT_STATUS_OBJECT_NAME_NOT_FOUND);
146
147		if (op->op_oplock_level != SMB2_OPLOCK_LEVEL_LEASE)
148			return (NT_STATUS_OBJECT_NAME_NOT_FOUND);
149		if (bcmp(op->lease_key, of->f_lease->ls_key,
150		    SMB_LEASE_KEY_SZ) != 0)
151			return (NT_STATUS_OBJECT_NAME_NOT_FOUND);
152
153		/*
154		 * We're supposed to check the name is the same.
155		 * Not really necessary to do this, so just do
156		 * minimal effort (check last component)
157		 */
158		fname = strrchr(op->fqi.fq_path.pn_path, '\\');
159		if (fname != NULL)
160			fname++;
161		else
162			fname = op->fqi.fq_path.pn_path;
163		if (smb_strcasecmp(fname, of->f_node->od_name, 0) != 0) {
164#ifdef	DEBUG
165			cmn_err(CE_NOTE, "reconnect name <%s> of name <%s>",
166			    fname, of->f_node->od_name);
167#endif
168			return (NT_STATUS_INVALID_PARAMETER);
169		}
170	} else {
171		if (op->op_oplock_level == SMB2_OPLOCK_LEVEL_LEASE)
172			return (NT_STATUS_OBJECT_NAME_NOT_FOUND);
173	}
174
175	if (op->dh_vers == SMB2_DURABLE_V2) {
176		boolean_t op_persist =
177		    ((op->dh_v2_flags & SMB2_DHANDLE_FLAG_PERSISTENT) != 0);
178		if (of->dh_persist != op_persist)
179			return (NT_STATUS_OBJECT_NAME_NOT_FOUND);
180		if (memcmp(op->create_guid, of->dh_create_guid, UUID_LEN))
181			return (NT_STATUS_OBJECT_NAME_NOT_FOUND);
182	}
183
184	if (!smb_is_same_user(sr->user_cr, of->f_cr))
185		return (NT_STATUS_ACCESS_DENIED);
186
187	return (NT_STATUS_SUCCESS);
188}
189
190/*
191 * [MS-SMB2] 3.3.5.9.7 and 3.3.5.9.12 (durable reconnect v1/v2)
192 *
193 * Looks up an ofile on the server's sv_dh_list by the persistid.
194 * If found, it validates the request.
195 * (see smb2_dh_reconnect_checks() for details)
196 * If the checks are passed, add it onto the new tree's list.
197 *
198 * Note that the oplock break code path can get to an ofile via the node
199 * ofile list.  It starts with a ref taken in smb_ofile_hold_olbrk, which
200 * waits if the ofile is found in state RECONNECT.  That wait happens with
201 * the node ofile list lock held as reader, and the oplock mutex held.
202 * Implications of that are: While we're in state RECONNECT, we shoud NOT
203 * block (at least, not for long) and must not try to enter any of the
204 * node ofile list lock or oplock mutex.  Thankfully, we don't need to
205 * enter those while reclaiming an orphaned ofile.
206 */
207uint32_t
208smb2_dh_reconnect(smb_request_t *sr)
209{
210	smb_arg_open_t	*op = &sr->sr_open;
211	smb_tree_t *tree = sr->tid_tree;
212	smb_ofile_t *of;
213	cred_t *old_cr;
214	uint32_t status = NT_STATUS_OBJECT_NAME_NOT_FOUND;
215	uint16_t fid = 0;
216
217	if (smb_idpool_alloc(&tree->t_fid_pool, &fid))
218		return (NT_STATUS_TOO_MANY_OPENED_FILES);
219
220	/* Find orphaned handle. */
221	of = smb_ofile_lookup_by_persistid(sr, op->dh_fileid.persistent);
222	if (of == NULL)
223		goto errout;
224
225	mutex_enter(&of->f_mutex);
226	if (of->f_state != SMB_OFILE_STATE_ORPHANED) {
227		mutex_exit(&of->f_mutex);
228		goto errout;
229	}
230
231	status = smb2_dh_reconnect_checks(sr, of);
232	if (status != NT_STATUS_SUCCESS) {
233		mutex_exit(&of->f_mutex);
234		goto errout;
235	}
236
237	/*
238	 * Note: cv_broadcast(&of->f_cv) when we're
239	 * done messing around in this state.
240	 * See: smb_ofile_hold_olbrk()
241	 */
242	of->f_state = SMB_OFILE_STATE_RECONNECT;
243	mutex_exit(&of->f_mutex);
244
245	/*
246	 * At this point, we should be the only thread with a ref on the
247	 * ofile, and the RECONNECT state should prevent new refs from
248	 * being granted, or other durable threads from observing or
249	 * reclaiming it. Put this ofile in the new tree, similar to
250	 * the last part of smb_ofile_open.
251	 */
252
253	old_cr = of->f_cr;
254	of->f_cr = sr->user_cr;
255	crhold(of->f_cr);
256	crfree(old_cr);
257
258	of->f_session = sr->session; /* hold is via user and tree */
259	smb_user_hold_internal(sr->uid_user);
260	of->f_user = sr->uid_user;
261	smb_tree_hold_internal(tree);
262	of->f_tree = tree;
263	of->f_fid = fid;
264
265	smb_llist_enter(&tree->t_ofile_list, RW_WRITER);
266	smb_llist_insert_tail(&tree->t_ofile_list, of);
267	smb_llist_exit(&tree->t_ofile_list);
268	atomic_inc_32(&tree->t_open_files);
269	atomic_inc_32(&sr->session->s_file_cnt);
270
271	/*
272	 * The ofile is now in the caller's session & tree.
273	 *
274	 * In case smb_ofile_hold or smb_oplock_send_brk() are
275	 * waiting for state RECONNECT to complete, wakeup.
276	 */
277	mutex_enter(&of->f_mutex);
278	of->dh_expire_time = 0;
279	of->f_state = SMB_OFILE_STATE_OPEN;
280	cv_broadcast(&of->f_cv);
281	mutex_exit(&of->f_mutex);
282
283	/*
284	 * The ofile is now visible in the new session.
285	 * From here, this is similar to the last part of
286	 * smb_common_open().
287	 */
288	op->fqi.fq_fattr.sa_mask = SMB_AT_ALL;
289	(void) smb_node_getattr(sr, of->f_node, zone_kcred(), of,
290	    &op->fqi.fq_fattr);
291
292	/*
293	 * Set up the fileid and dosattr in open_param for response
294	 */
295	op->fileid = op->fqi.fq_fattr.sa_vattr.va_nodeid;
296	op->dattr = op->fqi.fq_fattr.sa_dosattr;
297
298	/*
299	 * Set up the file type in open_param for the response
300	 * The ref. from ofile lookup is "given" to fid_ofile.
301	 */
302	op->ftype = SMB_FTYPE_DISK;
303	sr->smb_fid = of->f_fid;
304	sr->fid_ofile = of;
305
306	if (smb_node_is_file(of->f_node)) {
307		op->dsize = op->fqi.fq_fattr.sa_vattr.va_size;
308	} else {
309		/* directory or symlink */
310		op->dsize = 0;
311	}
312
313	op->create_options = 0; /* no more modifications wanted */
314	op->action_taken = SMB_OACT_OPENED;
315	return (NT_STATUS_SUCCESS);
316
317errout:
318	if (of != NULL)
319		smb_ofile_release(of);
320	if (fid != 0)
321		smb_idpool_free(&tree->t_fid_pool, fid);
322
323	return (status);
324}
325
326/*
327 * Durable handle expiration
328 * ofile state is _EXPIRED
329 */
330static void
331smb2_dh_expire(void *arg)
332{
333	smb_ofile_t *of = (smb_ofile_t *)arg;
334
335	smb_ofile_close(of, 0);
336	smb_ofile_release(of);
337}
338
339void
340smb2_durable_timers(smb_server_t *sv)
341{
342	smb_hash_t *hash;
343	smb_llist_t *bucket;
344	smb_ofile_t *of;
345	hrtime_t now;
346	int i;
347
348	hash = sv->sv_persistid_ht;
349	now = gethrtime();
350
351	for (i = 0; i < hash->num_buckets; i++) {
352		bucket = &hash->buckets[i].b_list;
353		smb_llist_enter(bucket, RW_READER);
354		for (of = smb_llist_head(bucket);
355		    of != NULL;
356		    of = smb_llist_next(bucket, of)) {
357			SMB_OFILE_VALID(of);
358
359			/*
360			 * Check outside the mutex first to avoid some
361			 * mutex_enter work in this loop.  If the state
362			 * changes under foot, the worst that happens
363			 * is we either enter the mutex when we might
364			 * not have needed to, or we miss some DH in
365			 * this pass and get it on the next.
366			 */
367			if (of->f_state != SMB_OFILE_STATE_ORPHANED)
368				continue;
369
370			mutex_enter(&of->f_mutex);
371			/* STATE_ORPHANED implies dh_expire_time != 0 */
372			if (of->f_state == SMB_OFILE_STATE_ORPHANED &&
373			    of->dh_expire_time <= now) {
374				of->f_state = SMB_OFILE_STATE_EXPIRED;
375				/* inline smb_ofile_hold_internal() */
376				of->f_refcnt++;
377				smb_llist_post(bucket, of, smb2_dh_expire);
378			}
379			mutex_exit(&of->f_mutex);
380		}
381		smb_llist_exit(bucket);
382	}
383}
384
385/*
386 * Clean out durable handles during shutdown.
387 * Like, smb2_durable_timers but expire all,
388 * and make sure the hash buckets are empty.
389 */
390void
391smb2_dh_shutdown(smb_server_t *sv)
392{
393	smb_hash_t *hash;
394	smb_llist_t *bucket;
395	smb_ofile_t *of;
396	int i;
397
398	hash = sv->sv_persistid_ht;
399
400	for (i = 0; i < hash->num_buckets; i++) {
401		bucket = &hash->buckets[i].b_list;
402		smb_llist_enter(bucket, RW_READER);
403		of = smb_llist_head(bucket);
404		while (of != NULL) {
405			SMB_OFILE_VALID(of);
406			mutex_enter(&of->f_mutex);
407
408			switch (of->f_state) {
409			case SMB_OFILE_STATE_ORPHANED:
410				of->f_state = SMB_OFILE_STATE_EXPIRED;
411				/* inline smb_ofile_hold_internal() */
412				of->f_refcnt++;
413				smb_llist_post(bucket, of, smb2_dh_expire);
414				break;
415			default:
416				break;
417			}
418			mutex_exit(&of->f_mutex);
419			of = smb_llist_next(bucket, of);
420		}
421		smb_llist_exit(bucket);
422	}
423
424#ifdef	DEBUG
425	for (i = 0; i < hash->num_buckets; i++) {
426		bucket = &hash->buckets[i].b_list;
427		smb_llist_enter(bucket, RW_READER);
428		of = smb_llist_head(bucket);
429		while (of != NULL) {
430			SMB_OFILE_VALID(of);
431			cmn_err(CE_NOTE, "dh_shutdown leaked of=%p",
432			    (void *)of);
433			of = smb_llist_next(bucket, of);
434		}
435		smb_llist_exit(bucket);
436	}
437#endif	// DEBUG
438}
439
440uint32_t
441smb2_fsctl_set_resilient(smb_request_t *sr, smb_fsctl_t *fsctl)
442{
443	uint32_t timeout;
444	smb_ofile_t *of = sr->fid_ofile;
445
446	/*
447	 * Note: The spec does not explicitly prohibit resilient directories
448	 * the same way it prohibits durable directories. We prohibit them
449	 * anyway as a simplifying assumption, as there doesn't seem to be
450	 * much use for it. (HYPER-V only seems to use it on files anyway)
451	 */
452	if (fsctl->InputCount < 8 || !smb_node_is_file(of->f_node))
453		return (NT_STATUS_INVALID_PARAMETER);
454
455	(void) smb_mbc_decodef(fsctl->in_mbc, "l4.",
456	    &timeout); /* milliseconds */
457
458	if (smb2_enable_dh == 0)
459		return (NT_STATUS_NOT_SUPPORTED);
460
461	/*
462	 * The spec wants us to return INVALID_PARAMETER if the timeout
463	 * is too large, but we have no way of informing the client
464	 * what an appropriate timeout is, so just set the timeout to
465	 * our max and return SUCCESS.
466	 */
467	if (timeout == 0)
468		timeout = smb2_res_def_timeout;
469	if (timeout > smb2_res_max_timeout)
470		timeout = smb2_res_max_timeout;
471
472	mutex_enter(&of->f_mutex);
473	of->dh_vers = SMB2_RESILIENT;
474	of->dh_timeout_offset = MSEC2NSEC(timeout);
475	mutex_exit(&of->f_mutex);
476
477	return (NT_STATUS_SUCCESS);
478}
479