--- /dev/null	Mon Aug  5 17:26:21 2019
+++ new/usr/src/uts/common/fs/smbsrv/smb2_durable.c	Mon Aug  5 17:26:21 2019
@@ -0,0 +1,1709 @@
+/*
+ * This file and its contents are supplied under the terms of the
+ * Common Development and Distribution License ("CDDL"), version 1.0.
+ * You may only use this file in accordance with the terms of version
+ * 1.0 of the CDDL.
+ *
+ * A full copy of the text of the CDDL should have accompanied this
+ * source.  A copy of the CDDL is also available via the Internet at
+ * http://www.illumos.org/license/CDDL.
+ */
+
+/*
+ * Copyright 2018 Nexenta Systems, Inc.  All rights reserved.
+ */
+
+/*
+ * SMB2 Durable Handle support
+ */
+
+#include <sys/types.h>
+#include <sys/cmn_err.h>
+#include <sys/fcntl.h>
+#include <sys/nbmlock.h>
+#include <sys/sid.h>
+#include <smbsrv/string.h>
+#include <smbsrv/smb_kproto.h>
+#include <smbsrv/smb_fsops.h>
+#include <smbsrv/smbinfo.h>
+#include <smbsrv/smb2_kproto.h>
+
+/* Windows default values from [MS-SMB2] */
+/*
+ * (times in seconds)
+ * resilient:
+ * MaxTimeout = 300 (win7+)
+ * if timeout > MaxTimeout, ERROR
+ * if timeout != 0, timeout = req.timeout
+ * if timeout == 0, timeout = (infinity) (Win7/w2k8r2)
+ * if timeout == 0, timeout = 120 (Win8+)
+ * v2:
+ * if timeout != 0, timeout = MIN(timeout, 300) (spec)
+ * if timeout != 0, timeout = timeout (win8/2k12)
+ * if timeout == 0, timeout = Share.CATimeout. \
+ *	if Share.CATimeout == 0, timeout = 60 (win8/w2k12)
+ * if timeout == 0, timeout = 180 (win8.1/w2k12r2)
+ * open.timeout = 60 (win8/w2k12r2) (i.e. we ignore the request)
+ * v1:
+ * open.timeout = 16 minutes
+ */
+
+uint32_t smb2_dh_def_timeout = 60 * MILLISEC;	/* mSec. */
+uint32_t smb2_dh_max_timeout = 300 * MILLISEC;	/* mSec. */
+
+uint32_t smb2_res_def_timeout = 120 * MILLISEC;	/* mSec. */
+uint32_t smb2_res_max_timeout = 300 * MILLISEC;	/* mSec. */
+
+uint32_t smb2_persist_timeout = 300 * MILLISEC;	/* mSec. */
+
+/* Max. size of the file used to store a CA handle. */
+static uint32_t smb2_dh_max_cah_size = 64 * 1024;
+static uint32_t smb2_ca_info_version = 1;
+
+/*
+ * Want this to have invariant layout on disk, where the
+ * last two uint32_t values are stored as a uint64_t
+ */
+struct nvlk {
+	uint64_t lk_start;
+	uint64_t lk_len;
+	/* (lk_pid << 32) | lk_type */
+#ifdef	_BIG_ENDIAN
+	uint32_t lk_pid, lk_type;
+#else
+	uint32_t lk_type, lk_pid;
+#endif
+};
+
+static void smb2_dh_import_share(void *);
+static smb_ofile_t *smb2_dh_import_handle(smb_request_t *, smb_node_t *,
+    uint64_t);
+static int smb2_dh_read_nvlist(smb_request_t *, smb_node_t *, struct nvlist **);
+static int smb2_dh_import_cred(smb_ofile_t *, char *);
+
+#define	DH_SN_SIZE 24	/* size of DH stream name buffers */
+/*
+ * Build the stream name used to store a CA handle.
+ * i.e. ":0123456789abcdef:$CA"
+ * Note: smb_fsop_create adds the SUNWsmb prefix,
+ * so we compose the name without the prefix.
+ */
+static inline void
+smb2_dh_make_stream_name(char *buf, size_t buflen, uint64_t id)
+{
+	ASSERT(buflen >= DH_SN_SIZE);
+	(void) snprintf(buf, buflen,
+	    ":%016" PRIx64 ":$CA", id);
+}
+
+/*
+ * smb_dh_should_save
+ *
+ * During session tear-down, decide whether to keep a durable handle.
+ *
+ * There are two cases where we save durable handles:
+ * 1. An SMB2 LOGOFF request was received
+ * 2. An unexpected disconnect from the client
+ *    Note: Specifying a PrevSessionID in session setup
+ *    is considered a disconnect (we just haven't learned about it yet)
+ * In every other case, we close durable handles.
+ *
+ * [MS-SMB2] 3.3.5.6 SMB2_LOGOFF
+ * [MS-SMB2] 3.3.7.1 Handling Loss of a Connection
+ *
+ * If any of the following are true, preserve for reconnect:
+ *
+ * - Open.IsResilient is TRUE.
+ *
+ * - Open.OplockLevel == SMB2_OPLOCK_LEVEL_BATCH and
+ *   Open.OplockState == Held, and Open.IsDurable is TRUE.
+ *
+ * - Open.OplockLevel == SMB2_OPLOCK_LEVEL_LEASE,
+ *   Lease.LeaseState SMB2_LEASE_HANDLE_CACHING,
+ *   Open.OplockState == Held, and Open.IsDurable is TRUE.
+ *
+ * - Open.IsPersistent is TRUE.
+ *
+ * We also deal with some special cases for shutdown of the
+ * server, session, user, tree (in that order). Other than
+ * the cases above, shutdown (or forced termination) should
+ * destroy durable handles.
+ */
+boolean_t
+smb_dh_should_save(smb_ofile_t *of)
+{
+	ASSERT(MUTEX_HELD(&of->f_mutex));
+	ASSERT(of->dh_vers != SMB2_NOT_DURABLE);
+
+	/* SMB service shutting down, destroy DH */
+	if (of->f_server->sv_state == SMB_SERVER_STATE_STOPPING)
+		return (B_FALSE);
+
+	/*
+	 * SMB Session (connection) going away (server up).
+	 * If server initiated disconnect, destroy DH
+	 * If client initiated disconnect, save all DH.
+	 */
+	if (of->f_session->s_state == SMB_SESSION_STATE_TERMINATED)
+		return (B_FALSE);
+	if (of->f_session->s_state == SMB_SESSION_STATE_DISCONNECTED)
+		return (B_TRUE);
+
+	/*
+	 * SMB User logoff, session still "up".
+	 * Action depends on why/how this logoff happened,
+	 * determined based on user->preserve_opens
+	 */
+	if (of->f_user->u_state == SMB_USER_STATE_LOGGING_OFF) {
+		switch (of->f_user->preserve_opens) {
+		case SMB2_DH_PRESERVE_NONE:
+			/* Server-initiated */
+			return (B_FALSE);
+		case SMB2_DH_PRESERVE_SOME:
+			/* Previous session logoff. */
+			goto preserve_some;
+		case SMB2_DH_PRESERVE_ALL:
+			/* Protocol logoff request */
+			return (B_TRUE);
+		}
+	}
+
+	/*
+	 * SMB tree disconnecting (user still logged on)
+	 * i.e. when kshare export forces disconnection.
+	 */
+	if (of->f_tree->t_state == SMB_TREE_STATE_DISCONNECTING)
+		return (B_FALSE);
+
+preserve_some:
+	/* preserve_opens == SMB2_DH_PRESERVE_SOME */
+
+	switch (of->dh_vers) {
+	case SMB2_RESILIENT:
+		return (B_TRUE);
+
+	case SMB2_DURABLE_V2:
+		if (of->dh_persist)
+			return (B_TRUE);
+		/* FALLTHROUGH */
+	case SMB2_DURABLE_V1:
+		/* IS durable (v1 or v2) */
+		if ((of->f_oplock.og_state & (OPLOCK_LEVEL_BATCH |
+		    OPLOCK_LEVEL_CACHE_HANDLE)) != 0)
+			return (B_TRUE);
+		/* FALLTHROUGH */
+	case SMB2_NOT_DURABLE:
+	default:
+		break;
+	}
+
+	return (B_FALSE);
+}
+
+/*
+ * Is this stream name a CA handle? i.e.
+ * ":0123456789abcdef:$CA"
+ */
+static boolean_t
+smb2_dh_match_ca_name(const char *name, uint64_t *idp)
+{
+	static const char suffix[] = ":$CA";
+	u_longlong_t ull;
+	const char *p = name;
+	char *p2 = NULL;
+	int len, rc;
+
+	if (*p++ != ':')
+		return (B_FALSE);
+
+	rc = ddi_strtoull(p, &p2, 16, &ull);
+	if (rc != 0 || p2 != (p + 16))
+		return (B_FALSE);
+	p += 16;
+
+	len = sizeof (suffix) - 1;
+	if (strncmp(p, suffix, len) != 0)
+		return (B_FALSE);
+	p += len;
+
+	if (*p != '\0')
+		return (B_FALSE);
+
+	*idp = (uint64_t)ull;
+	return (B_TRUE);
+}
+
+/*
+ * smb2_dh_new_ca_share
+ *
+ * Called when a new share has ca=true.  Find or create the CA dir,
+ * and start a thread to import persistent handles.
+ */
+int
+smb2_dh_new_ca_share(smb_server_t *sv, smb_kshare_t *shr)
+{
+	smb_kshare_t	*shr2;
+	smb_request_t	*sr;
+
+	ASSERT(STYPE_ISDSK(shr->shr_type));
+
+	/*
+	 * Need to lookup the kshare again, to get a hold.
+	 * Add a function to just get the hold?
+	 */
+	shr2 = smb_kshare_lookup(sv, shr->shr_name);
+	if (shr2 != shr)
+		return (EINVAL);
+
+	sr = smb_request_alloc(sv->sv_session, 0);
+	if (sr == NULL) {
+		/* shutting down? */
+		smb_kshare_release(sv, shr);
+		return (EINTR);
+	}
+	sr->sr_state = SMB_REQ_STATE_SUBMITTED;
+
+	/*
+	 * Mark this share as "busy importing persistent handles"
+	 * so we can hold off tree connect until that's done.
+	 * Will clear and wakeup below.
+	 */
+	mutex_enter(&shr->shr_mutex);
+	shr->shr_import_busy = sr;
+	mutex_exit(&shr->shr_mutex);
+
+	/*
+	 * Start a taskq job to import any CA handles.
+	 * The hold on the kshare is given to this job,
+	 * which releases it when it's done.
+	 */
+	sr->arg.tcon.si = shr;	/* hold from above */
+	(void) taskq_dispatch(
+	    sv->sv_worker_pool,
+	    smb2_dh_import_share, sr, TQ_SLEEP);
+
+	return (0);
+}
+
+int smb2_dh_import_delay = 0;
+
+static void
+smb2_dh_import_share(void *arg)
+{
+	smb_request_t	*sr = arg;
+	smb_kshare_t	*shr = sr->arg.tcon.si;
+	smb_node_t	*snode;
+	cred_t		*kcr = zone_kcred();
+	smb_streaminfo_t *str_info = NULL;
+	uint64_t	id;
+	smb_node_t	*str_node;
+	smb_odir_t	*od = NULL;
+	smb_ofile_t	*of;
+	int		rc;
+	boolean_t	eof;
+
+	sr->sr_state = SMB_REQ_STATE_ACTIVE;
+
+	if (smb2_dh_import_delay > 0)
+		delay(SEC_TO_TICK(smb2_dh_import_delay));
+
+	/*
+	 * Borrow the server's "root" user.
+	 *
+	 * This takes the place of smb_session_lookup_ssnid()
+	 * that would happen in smb2_dispatch for a normal SR.
+	 * As usual, this hold is released in smb_request_free.
+	 */
+	sr->uid_user = sr->sr_server->sv_rootuser;
+	smb_user_hold_internal(sr->uid_user);
+	sr->user_cr = sr->uid_user->u_cred;
+
+	/*
+	 * Create a temporary tree connect
+	 */
+	sr->arg.tcon.path = shr->shr_name;
+	sr->tid_tree = smb_tree_alloc(sr, shr, shr->shr_root_node,
+	    ACE_ALL_PERMS, 0);
+	if (sr->tid_tree == NULL) {
+		cmn_err(CE_NOTE, "smb2_dh_import_share: "
+		    "failed connect share <%s>", shr->shr_name);
+		goto out;
+	}
+	snode = sr->tid_tree->t_snode;
+
+	/*
+	 * Get the buffers we'll use to read CA handle data.
+	 * Stash in sr_request_buf for smb2_dh_import_handle().
+	 * Also a buffer for the stream name info.
+	 */
+	sr->sr_req_length = smb2_dh_max_cah_size;
+	sr->sr_request_buf = kmem_alloc(sr->sr_req_length, KM_SLEEP);
+	str_info = kmem_alloc(sizeof (smb_streaminfo_t), KM_SLEEP);
+
+	/*
+	 * Open the ext. attr dir under the share root and
+	 * import CA handles for this share.
+	 */
+	if (smb_odir_openat(sr, snode, &od) != 0) {
+		cmn_err(CE_NOTE, "Share [%s] CA import, no xattr dir?",
+		    shr->shr_name);
+		goto out;
+	}
+
+	eof = B_FALSE;
+	do {
+		/*
+		 * If the kshare gets unshared before we finish,
+		 * bail out so we don't hold things up.
+		 */
+		if (shr->shr_flags & SMB_SHRF_REMOVED)
+			break;
+
+		/*
+		 * Read a stream name and info
+		 */
+		rc = smb_odir_read_streaminfo(sr, od, str_info, &eof);
+		if ((rc != 0) || (eof))
+			break;
+
+		/*
+		 * Skip anything not a CA handle.
+		 */
+		if (!smb2_dh_match_ca_name(str_info->si_name, &id)) {
+			continue;
+		}
+
+		/*
+		 * Lookup stream node and import
+		 */
+		str_node = NULL;
+		rc = smb_fsop_lookup_name(sr, kcr, SMB_CASE_SENSITIVE,
+		    snode, snode, str_info->si_name, &str_node);
+		if (rc != 0) {
+			cmn_err(CE_NOTE, "Share [%s] CA import, "
+			    "lookup <%s> failed rc=%d",
+			    shr->shr_name, str_info->si_name, rc);
+			continue;
+		}
+		of = smb2_dh_import_handle(sr, str_node, id);
+		smb_node_release(str_node);
+		if (of != NULL) {
+			smb_ofile_release(of);
+			of = NULL;
+		}
+		sr->fid_ofile = NULL;
+
+	} while (!eof);
+
+out:
+	if (od) {
+		smb_odir_close(od);
+		smb_odir_release(od);
+	}
+
+	if (str_info != NULL)
+		kmem_free(str_info, sizeof (smb_streaminfo_t));
+	/* Let smb_request_free clean up sr->sr_request_buf */
+
+	/*
+	 * We did a (temporary, internal) tree connect above,
+	 * which we need to undo before we return.  Note that
+	 * smb_request_free will do the final release of
+	 * sr->tid_tree, sr->uid_user
+	 */
+	if (sr->tid_tree != NULL)
+		smb_tree_disconnect(sr->tid_tree, B_FALSE);
+
+	/*
+	 * Wake up any waiting tree connect(s).
+	 * See smb_tree_connect_disk().
+	 */
+	mutex_enter(&shr->shr_mutex);
+	shr->shr_import_busy = NULL;
+	cv_broadcast(&shr->shr_cv);
+	mutex_exit(&shr->shr_mutex);
+
+	smb_kshare_release(sr->sr_server, shr);
+	smb_request_free(sr);
+}
+
+/*
+ * This returns the new ofile mostly for dtrace.
+ */
+static smb_ofile_t *
+smb2_dh_import_handle(smb_request_t *sr, smb_node_t *str_node,
+    uint64_t persist_id)
+{
+	uint8_t		client_uuid[UUID_LEN];
+	smb_tree_t	*tree = sr->tid_tree;
+	smb_arg_open_t	*op = &sr->arg.open;
+	smb_pathname_t	*pn = &op->fqi.fq_path;
+	cred_t		*kcr = zone_kcred();
+	struct nvlist	*nvl = NULL;
+	char		*sidstr = NULL;
+	smb_ofile_t	*of = NULL;
+	smb_attr_t	*pa;
+	boolean_t	did_open = B_FALSE;
+	boolean_t	have_lease = B_FALSE;
+	hrtime_t	hrt;
+	uint64_t	*u64p;
+	uint64_t	u64;
+	uint32_t	u32;
+	uint32_t	status;
+	char		*s;
+	uint8_t		*u8p;
+	uint_t		alen;
+	int		rc;
+
+	/*
+	 * While we're called with arg.tcon, we now want to use
+	 * smb_arg_open for the rest of import, so clear it.
+	 */
+	bzero(op, sizeof (*op));
+	op->create_disposition = FILE_OPEN;
+
+	/*
+	 * Read and unpack the NVL
+	 */
+	rc = smb2_dh_read_nvlist(sr, str_node, &nvl);
+	if (rc != 0)
+		return (NULL);
+
+	/*
+	 * Known CA info version?
+	 */
+	u32 = 0;
+	rc = nvlist_lookup_uint32(nvl, "info_version", &u32);
+	if (rc != 0 || u32 != smb2_ca_info_version) {
+		cmn_err(CE_NOTE, "CA import (%s/%s) bad vers=%d",
+		    tree->t_resource, str_node->od_name, u32);
+		goto errout;
+	}
+
+	/*
+	 * The persist ID in the nvlist should match the one
+	 * encoded in the file name. (not enforced)
+	 */
+	u64 = 0;
+	rc = nvlist_lookup_uint64(nvl, "file_persistid", &u64);
+	if (rc != 0 || u64 != persist_id) {
+		cmn_err(CE_WARN, "CA import (%s/%s) bad id=%016" PRIx64,
+		    tree->t_resource, str_node->od_name, u64);
+		/* goto errout? (allow) */
+	}
+
+	/*
+	 * Does it belong in the share being imported?
+	 */
+	s = NULL;
+	rc = nvlist_lookup_string(nvl, "share_name", &s);
+	if (rc != 0) {
+		cmn_err(CE_NOTE, "CA import (%s/%s) no share_name",
+		    tree->t_resource, str_node->od_name);
+		goto errout;
+	}
+	if (smb_strcasecmp(s, tree->t_sharename, 0) != 0) {
+		/* Normal (not an error) */
+#ifdef DEBUG
+		cmn_err(CE_NOTE, "CA import (%s/%s) other share",
+		    tree->t_resource, str_node->od_name);
+#endif
+		goto errout;
+	}
+
+	/*
+	 * Get the path name (for lookup)
+	 */
+	rc = nvlist_lookup_string(nvl, "path_name", &pn->pn_path);
+	if (rc != 0) {
+		cmn_err(CE_NOTE, "CA import (%s/%s) no path_name",
+		    tree->t_resource, str_node->od_name);
+		goto errout;
+	}
+
+	/*
+	 * owner sid
+	 */
+	rc = nvlist_lookup_string(nvl, "owner_sid", &sidstr);
+	if (rc != 0) {
+		cmn_err(CE_NOTE, "CA import (%s/%s) no owner_sid",
+		    tree->t_resource, str_node->od_name);
+		goto errout;
+	}
+
+	/*
+	 * granted access
+	 */
+	rc = nvlist_lookup_uint32(nvl,
+	    "granted_access", &op->desired_access);
+	if (rc != 0) {
+		cmn_err(CE_NOTE, "CA import (%s/%s) no granted_access",
+		    tree->t_resource, str_node->od_name);
+		goto errout;
+	}
+
+	/*
+	 * share access
+	 */
+	rc = nvlist_lookup_uint32(nvl,
+	    "share_access", &op->share_access);
+	if (rc != 0) {
+		cmn_err(CE_NOTE, "CA import (%s/%s) no share_access",
+		    tree->t_resource, str_node->od_name);
+		goto errout;
+	}
+
+	/*
+	 * create options
+	 */
+	rc = nvlist_lookup_uint32(nvl,
+	    "create_options", &op->create_options);
+	if (rc != 0) {
+		cmn_err(CE_NOTE, "CA import (%s/%s) no create_options",
+		    tree->t_resource, str_node->od_name);
+		goto errout;
+	}
+
+	/*
+	 * create guid (client-assigned)
+	 */
+	alen = UUID_LEN;
+	u8p = NULL;
+	rc = nvlist_lookup_uint8_array(nvl, "file_guid", &u8p, &alen);
+	if (rc != 0 || alen != UUID_LEN) {
+		cmn_err(CE_NOTE, "CA import (%s/%s) bad file_guid",
+		    tree->t_resource, str_node->od_name);
+		goto errout;
+	}
+	bcopy(u8p, op->create_guid, UUID_LEN);
+
+	/*
+	 * client uuid (identifies the client)
+	 */
+	alen = UUID_LEN;
+	u8p = NULL;
+	rc = nvlist_lookup_uint8_array(nvl, "client_uuid", &u8p, &alen);
+	if (rc != 0 || alen != UUID_LEN) {
+		cmn_err(CE_NOTE, "CA import (%s/%s) no client_uuid",
+		    tree->t_resource, str_node->od_name);
+		goto errout;
+	}
+	bcopy(u8p, client_uuid, UUID_LEN);
+
+	/*
+	 * Lease key (optional)
+	 */
+	alen = SMB_LEASE_KEY_SZ;
+	u8p = NULL;
+	rc = nvlist_lookup_uint8_array(nvl, "lease_uuid", &u8p, &alen);
+	if (rc == 0) {
+		bcopy(u8p, op->lease_key, UUID_LEN);
+		(void) nvlist_lookup_uint32(nvl,
+		    "lease_state", &op->lease_state);
+		(void) nvlist_lookup_uint16(nvl,
+		    "lease_epoch", &op->lease_epoch);
+		(void) nvlist_lookup_uint16(nvl,
+		    "lease_version", &op->lease_version);
+		have_lease = B_TRUE;
+	} else {
+		(void) nvlist_lookup_uint32(nvl,
+		    "oplock_state", &op->op_oplock_state);
+	}
+
+	/*
+	 * Done getting what we need from the NV list.
+	 * (re)open the file
+	 */
+	status = smb_common_open(sr);
+	if (status != 0) {
+		cmn_err(CE_NOTE, "CA import (%s/%s) open failed 0x%x",
+		    tree->t_resource, str_node->od_name, status);
+		(void) smb_node_set_delete_on_close(str_node, kcr, 0);
+		goto errout;
+	}
+	of = sr->fid_ofile;
+	did_open = B_TRUE;
+
+	/*
+	 * Now restore the rest of the SMB2 level state.
+	 * See smb2_create after smb_common_open
+	 */
+
+	/*
+	 * Setup of->f_cr with owner SID
+	 */
+	rc = smb2_dh_import_cred(of, sidstr);
+	if (rc != 0) {
+		cmn_err(CE_NOTE, "CA import (%s/%s) import cred failed",
+		    tree->t_resource, str_node->od_name);
+		goto errout;
+	}
+
+	/*
+	 * Use the persist ID we previously assigned.
+	 * Like smb_ofile_set_persistid_ph()
+	 */
+	rc = smb_ofile_insert_persistid(of, persist_id);
+	if (rc != 0) {
+		cmn_err(CE_NOTE, "CA import (%s/%s) "
+		    "insert_persistid rc=%d",
+		    tree->t_resource, str_node->od_name, rc);
+		goto errout;
+	}
+
+	/*
+	 * Like smb2_lease_create()
+	 *
+	 * Lease state is stored in each persistent handle, but
+	 * only one handle has the state we want.  As we import
+	 * each handle, "upgrade" the lease if the handle we're
+	 * importing has a "better" lease state (higher epoch or
+	 * more cache rights).  After all handles are imported,
+	 * that will get the lease to the right state.
+	 */
+	if (have_lease) {
+		smb_lease_t *ls;
+		status = smb2_lease_create(sr, client_uuid);
+		if (status != 0) {
+			cmn_err(CE_NOTE, "CA import (%s/%s) get lease 0x%x",
+			    tree->t_resource, str_node->od_name, status);
+			goto errout;
+		}
+		ls = of->f_lease;
+
+		/* Use most current "epoch". */
+		mutex_enter(&ls->ls_mutex);
+		if (ls->ls_epoch < op->lease_epoch)
+			ls->ls_epoch = op->lease_epoch;
+		mutex_exit(&ls->ls_mutex);
+
+		/*
+		 * Get the lease (and oplock)
+		 * uses op->lease_state
+		 */
+		op->op_oplock_level = SMB2_OPLOCK_LEVEL_LEASE;
+		smb2_lease_acquire(sr);
+
+	} else {
+		/*
+		 * No lease; maybe get an oplock
+		 * uses: op->op_oplock_level
+		 */
+		if (op->op_oplock_state & OPLOCK_LEVEL_BATCH) {
+			op->op_oplock_level = SMB2_OPLOCK_LEVEL_BATCH;
+		} else if (op->op_oplock_state & OPLOCK_LEVEL_ONE) {
+			op->op_oplock_level = SMB2_OPLOCK_LEVEL_EXCLUSIVE;
+		} else if (op->op_oplock_state & OPLOCK_LEVEL_TWO) {
+			op->op_oplock_level = SMB2_OPLOCK_LEVEL_II;
+		} else {
+			op->op_oplock_level = SMB2_OPLOCK_LEVEL_NONE;
+		}
+		smb2_oplock_acquire(sr);
+	}
+
+	/*
+	 * Byte range locks
+	 */
+	alen = 0;
+	u64p = NULL;
+	if (nvlist_lookup_uint64_array(nvl, "locks", &u64p, &alen) == 0) {
+		uint_t	i;
+		uint_t nlocks = alen / 3;
+		struct nvlk	*nlp;
+
+		nlp = (struct nvlk *)u64p;
+		for (i = 0; i < nlocks; i++) {
+			status = smb_lock_range(
+			    sr,
+			    nlp->lk_start,
+			    nlp->lk_len,
+			    nlp->lk_pid,
+			    nlp->lk_type,
+			    0);
+			if (status != 0) {
+				cmn_err(CE_NOTE, "CA import (%s/%s) "
+				    "get lock %d failed 0x%x",
+				    tree->t_resource,
+				    str_node->od_name,
+				    i, status);
+			}
+			nlp++;
+		}
+	}
+	alen = SMB_OFILE_LSEQ_MAX;
+	u8p = NULL;
+	if (nvlist_lookup_uint8_array(nvl, "lockseq", &u8p, &alen) == 0) {
+		if (alen != SMB_OFILE_LSEQ_MAX) {
+			cmn_err(CE_NOTE, "CA import (%s/%s) "
+			    "get lockseq bad len=%d",
+			    tree->t_resource,
+			    str_node->od_name,
+			    alen);
+		} else {
+			mutex_enter(&of->f_mutex);
+			bcopy(u8p, of->f_lock_seq, alen);
+			mutex_exit(&of->f_mutex);
+		}
+	}
+
+	/*
+	 * Optional "sticky" times (set pending attributes)
+	 */
+	mutex_enter(&of->f_mutex);
+	pa = &of->f_pending_attr;
+	if (nvlist_lookup_hrtime(nvl, "atime", &hrt) == 0) {
+		hrt2ts(hrt, &pa->sa_vattr.va_atime);
+		pa->sa_mask |= SMB_AT_ATIME;
+	}
+	if (nvlist_lookup_hrtime(nvl, "mtime", &hrt) == 0) {
+		hrt2ts(hrt, &pa->sa_vattr.va_mtime);
+		pa->sa_mask |= SMB_AT_MTIME;
+	}
+	if (nvlist_lookup_hrtime(nvl, "ctime", &hrt) == 0) {
+		hrt2ts(hrt, &pa->sa_vattr.va_ctime);
+		pa->sa_mask |= SMB_AT_CTIME;
+	}
+	mutex_exit(&of->f_mutex);
+
+	/*
+	 * Make durable and persistent.
+	 * See smb2_dh_make_persistent()
+	 */
+	of->dh_vers = SMB2_DURABLE_V2;
+	bcopy(op->create_guid, of->dh_create_guid, UUID_LEN);
+	of->dh_persist = B_TRUE;
+	of->dh_nvfile = str_node;
+	smb_node_ref(str_node);
+	of->dh_nvlist = nvl;
+	nvl = NULL;
+
+	/*
+	 * Now make it state orphaned...
+	 * See smb_ofile_drop(), then
+	 * smb_ofile_save_dh()
+	 */
+	mutex_enter(&of->f_mutex);
+	of->f_state = SMB_OFILE_STATE_SAVE_DH;
+	of->dh_timeout_offset = MSEC2NSEC(smb2_persist_timeout);
+	mutex_exit(&of->f_mutex);
+
+	/*
+	 * Finished!
+	 */
+	return (of);
+
+errout:
+	if (did_open) {
+		smb_ofile_close(of, 0);
+		smb_ofile_release(of);
+	} else {
+		ASSERT(of == NULL);
+	}
+
+	if (nvl != NULL)
+		nvlist_free(nvl);
+
+	return (NULL);
+}
+
+static int
+smb2_dh_read_nvlist(smb_request_t *sr, smb_node_t *node,
+    struct nvlist **nvlpp)
+{
+	smb_attr_t	attr;
+	iovec_t		iov;
+	uio_t		uio;
+	smb_kshare_t	*shr = sr->arg.tcon.si;
+	cred_t		*kcr = zone_kcred();
+	size_t		flen;
+	int		rc;
+
+	bzero(&attr, sizeof (attr));
+	attr.sa_mask = SMB_AT_SIZE;
+	rc = smb_node_getattr(NULL, node, kcr, NULL, &attr);
+	if (rc != 0) {
+		cmn_err(CE_NOTE, "CA import (%s/%s) getattr rc=%d",
+		    shr->shr_path, node->od_name, rc);
+		return (rc);
+	}
+
+	if (attr.sa_vattr.va_size < 4 ||
+	    attr.sa_vattr.va_size > sr->sr_req_length) {
+		cmn_err(CE_NOTE, "CA import (%s/%s) bad size=%" PRIu64,
+		    shr->shr_path, node->od_name,
+		    (uint64_t)attr.sa_vattr.va_size);
+		return (EINVAL);
+	}
+	flen = (size_t)attr.sa_vattr.va_size;
+
+	bzero(&uio, sizeof (uio));
+	iov.iov_base = sr->sr_request_buf;
+	iov.iov_len = flen;
+	uio.uio_iov = &iov;
+	uio.uio_iovcnt = 1;
+	uio.uio_resid = flen;
+	uio.uio_segflg = UIO_SYSSPACE;
+	uio.uio_extflg = UIO_COPY_DEFAULT;
+	rc = smb_fsop_read(sr, kcr, node, NULL, &uio, 0);
+	if (rc != 0) {
+		cmn_err(CE_NOTE, "CA import (%s/%s) read, rc=%d",
+		    shr->shr_path, node->od_name, rc);
+		return (rc);
+	}
+	if (uio.uio_resid != 0) {
+		cmn_err(CE_NOTE, "CA import (%s/%s) short read",
+		    shr->shr_path, node->od_name);
+		return (EIO);
+	}
+
+	rc = nvlist_unpack(sr->sr_request_buf, flen, nvlpp, KM_SLEEP);
+	if (rc != 0) {
+		cmn_err(CE_NOTE, "CA import (%s/%s) unpack, rc=%d",
+		    shr->shr_path, node->od_name, rc);
+		return (rc);
+	}
+
+	return (0);
+}
+
+/*
+ * Setup a vestigal credential in of->f_cr just good enough for
+ * smb_is_same_user to determine if the caller owned this ofile.
+ * At reconnect, of->f_cr will be replaced with the caller's.
+ */
+static int
+smb2_dh_import_cred(smb_ofile_t *of, char *sidstr)
+{
+#ifdef	_FAKE_KERNEL
+	_NOTE(ARGUNUSED(sidstr))
+	/* fksmbd doesn't have real credentials. */
+	of->f_cr = CRED();
+	crhold(of->f_cr);
+#else
+	char tmpstr[SMB_SID_STRSZ];
+	ksid_t		ksid;
+	cred_t		*cr, *oldcr;
+	int		rc;
+
+	(void) strlcpy(tmpstr, sidstr, sizeof (tmpstr));
+	bzero(&ksid, sizeof (ksid));
+
+	rc = smb_sid_splitstr(tmpstr, &ksid.ks_rid);
+	if (rc != 0)
+		return (rc);
+	cr = crget();
+
+	ksid.ks_domain = ksid_lookupdomain(tmpstr);
+	crsetsid(cr, &ksid, KSID_USER);
+	ksiddomain_hold(ksid.ks_domain);
+	crsetsid(cr, &ksid, KSID_OWNER);
+
+	/*
+	 * Just to avoid leaving the KSID_GROUP slot NULL,
+	 * put the "everyone" SID there (S-1-1-0).
+	 */
+	ksid.ks_domain = ksid_lookupdomain("S-1-1");
+	ksid.ks_rid = 0;
+	crsetsid(cr, &ksid, KSID_GROUP);
+
+	oldcr = of->f_cr;
+	of->f_cr = cr;
+	if (oldcr != NULL)
+		crfree(oldcr);
+#endif
+
+	return (0);
+}
+
+/*
+ * Set Delete-on-Close (DoC) on the persistent state file so it will be
+ * removed when the last ref. goes away (in smb2_dh_close_persistent).
+ *
+ * This is called in just two places:
+ * (1) SMB2_close request -- client tells us to destroy the handle.
+ * (2) smb2_dh_expire -- client has forgotten about this handle.
+ * All other (server-initiated) close calls should leave these
+ * persistent state files in the file system.
+ */
+void
+smb2_dh_setdoc_persistent(smb_ofile_t *of)
+{
+	smb_node_t *strnode;
+	uint32_t status;
+
+	mutex_enter(&of->dh_nvlock);
+	if ((strnode = of->dh_nvfile) != NULL)
+		smb_node_ref(strnode);
+	mutex_exit(&of->dh_nvlock);
+
+	if (strnode != NULL) {
+		status = smb_node_set_delete_on_close(strnode,
+		    zone_kcred(), SMB_CASE_SENSITIVE);
+		if (status != 0) {
+			cmn_err(CE_WARN, "Can't set DoC on CA file: %s",
+			    strnode->od_name);
+			DTRACE_PROBE1(rm__ca__err, smb_ofile_t *, of);
+		}
+		smb_node_release(strnode);
+	}
+}
+
+/*
+ * During ofile close, free the persistent handle state nvlist and
+ * drop our reference to the state file node (which may unlink it
+ * if smb2_dh_setdoc_persistent was called).
+ */
+void
+smb2_dh_close_persistent(smb_ofile_t *of)
+{
+	smb_node_t	*strnode;
+	struct nvlist	*nvl;
+
+	/*
+	 * Clear out nvlist and stream linkage
+	 */
+	mutex_enter(&of->dh_nvlock);
+	strnode = of->dh_nvfile;
+	of->dh_nvfile = NULL;
+	nvl = of->dh_nvlist;
+	of->dh_nvlist = NULL;
+	mutex_exit(&of->dh_nvlock);
+
+	if (nvl != NULL)
+		nvlist_free(nvl);
+
+	if (strnode != NULL)
+		smb_node_release(strnode);
+}
+
+/*
+ * Make this durable handle persistent.
+ * If we succeed, set of->dh_persist = TRUE.
+ */
+int
+smb2_dh_make_persistent(smb_request_t *sr, smb_ofile_t *of)
+{
+	char		fname[DH_SN_SIZE];
+	char		sidstr[SMB_SID_STRSZ];
+	smb_attr_t	attr;
+	smb_arg_open_t	*op = &sr->arg.open;
+	cred_t		*kcr = zone_kcred();
+	smb_node_t	*dnode = of->f_tree->t_snode;
+	smb_node_t	*fnode = NULL;
+	ksid_t		*ksid;
+	int		rc;
+
+	ASSERT(of->dh_nvfile == NULL);
+
+	/*
+	 * Create the persistent handle nvlist file.
+	 * It's a named stream in the share root,
+	 */
+	smb2_dh_make_stream_name(fname, sizeof (fname), of->f_persistid);
+
+	bzero(&attr, sizeof (attr));
+	attr.sa_mask = SMB_AT_TYPE | SMB_AT_MODE | SMB_AT_SIZE;
+	attr.sa_vattr.va_type = VREG;
+	attr.sa_vattr.va_mode = 0640;
+	attr.sa_vattr.va_size = 4;
+	rc = smb_fsop_create(sr, kcr, dnode, fname, &attr, &fnode);
+	if (rc != 0)
+		return (rc);
+
+	mutex_enter(&of->dh_nvlock);
+
+	/* fnode is held. rele in smb2_dh_close_persistent */
+	of->dh_nvfile = fnode;
+	(void) nvlist_alloc(&of->dh_nvlist, NV_UNIQUE_NAME, KM_SLEEP);
+
+	/*
+	 * Want the ksid as a string
+	 */
+	ksid = crgetsid(of->f_user->u_cred, KSID_USER);
+	(void) snprintf(sidstr, sizeof (sidstr), "%s-%u",
+	    ksid->ks_domain->kd_name, ksid->ks_rid);
+
+	/*
+	 * Fill in the fixed parts of the nvlist
+	 */
+	(void) nvlist_add_uint32(of->dh_nvlist,
+	    "info_version", smb2_ca_info_version);
+	(void) nvlist_add_string(of->dh_nvlist,
+	    "owner_sid", sidstr);
+	(void) nvlist_add_string(of->dh_nvlist,
+	    "share_name", of->f_tree->t_sharename);
+	(void) nvlist_add_uint64(of->dh_nvlist,
+	    "file_persistid", of->f_persistid);
+	(void) nvlist_add_uint8_array(of->dh_nvlist,
+	    "file_guid", of->dh_create_guid, UUID_LEN);
+	(void) nvlist_add_string(of->dh_nvlist,
+	    "client_ipaddr", sr->session->ip_addr_str);
+	(void) nvlist_add_uint8_array(of->dh_nvlist,
+	    "client_uuid", sr->session->clnt_uuid, UUID_LEN);
+	(void) nvlist_add_string(of->dh_nvlist,
+	    "path_name", op->fqi.fq_path.pn_path);
+	(void) nvlist_add_uint32(of->dh_nvlist,
+	    "granted_access", of->f_granted_access);
+	(void) nvlist_add_uint32(of->dh_nvlist,
+	    "share_access", of->f_share_access);
+	(void) nvlist_add_uint32(of->dh_nvlist,
+	    "create_options", of->f_create_options);
+	if (of->f_lease != NULL) {
+		smb_lease_t *ls = of->f_lease;
+		(void) nvlist_add_uint8_array(of->dh_nvlist,
+		    "lease_uuid", ls->ls_key, 16);
+		(void) nvlist_add_uint32(of->dh_nvlist,
+		    "lease_state", ls->ls_state);
+		(void) nvlist_add_uint16(of->dh_nvlist,
+		    "lease_epoch", ls->ls_epoch);
+		(void) nvlist_add_uint16(of->dh_nvlist,
+		    "lease_version", ls->ls_version);
+	} else {
+		(void) nvlist_add_uint32(of->dh_nvlist,
+		    "oplock_state", of->f_oplock.og_state);
+	}
+	mutex_exit(&of->dh_nvlock);
+
+	smb2_dh_update_locks(sr, of);
+
+	/* Tell sr update nvlist file */
+	sr->dh_nvl_dirty = B_TRUE;
+
+	return (0);
+}
+
+void
+smb2_dh_update_nvfile(smb_request_t *sr)
+{
+	smb_attr_t	attr;
+	iovec_t		iov;
+	uio_t		uio;
+	smb_ofile_t	*of = sr->fid_ofile;
+	cred_t		*kcr = zone_kcred();
+	char		*buf = NULL;
+	size_t		buflen = 0;
+	uint32_t	wcnt;
+	int		rc;
+
+	if (of == NULL || of->dh_persist == B_FALSE)
+		return;
+
+	mutex_enter(&of->dh_nvlock);
+	if (of->dh_nvlist == NULL || of->dh_nvfile == NULL) {
+		mutex_exit(&of->dh_nvlock);
+		return;
+	}
+
+	rc = nvlist_size(of->dh_nvlist, &buflen, NV_ENCODE_XDR);
+	if (rc != 0)
+		goto out;
+	buf = kmem_zalloc(buflen, KM_SLEEP);
+
+	rc = nvlist_pack(of->dh_nvlist, &buf, &buflen,
+	    NV_ENCODE_XDR, KM_SLEEP);
+	if (rc != 0)
+		goto out;
+
+	bzero(&attr, sizeof (attr));
+	attr.sa_mask = SMB_AT_SIZE;
+	attr.sa_vattr.va_size = buflen;
+	rc = smb_node_setattr(sr, of->dh_nvfile, kcr, NULL, &attr);
+	if (rc != 0)
+		goto out;
+
+	bzero(&uio, sizeof (uio));
+	iov.iov_base = (void *) buf;
+	iov.iov_len = buflen;
+	uio.uio_iov = &iov;
+	uio.uio_iovcnt = 1;
+	uio.uio_resid = buflen;
+	uio.uio_segflg = UIO_SYSSPACE;
+	uio.uio_extflg = UIO_COPY_DEFAULT;
+	rc = smb_fsop_write(sr, kcr, of->dh_nvfile,
+	    NULL, &uio, &wcnt, 0);
+	if (rc == 0 && wcnt != buflen)
+		rc = EIO;
+
+out:
+	mutex_exit(&of->dh_nvlock);
+
+	if (rc != 0) {
+		cmn_err(CE_WARN,
+		    "clnt(%s) failed to update persistent handle, rc=%d",
+		    sr->session->ip_addr_str, rc);
+	}
+
+	if (buf != NULL) {
+		kmem_free(buf, buflen);
+	}
+}
+
+/*
+ * Called after f_oplock (and lease) changes
+ * If lease, update: lease_state, lease_epoch
+ * else (oplock) update: oplock_state
+ */
+void
+smb2_dh_update_oplock(smb_request_t *sr, smb_ofile_t *of)
+{
+	smb_lease_t *ls;
+
+	mutex_enter(&of->dh_nvlock);
+	if (of->dh_nvlist == NULL) {
+		mutex_exit(&of->dh_nvlock);
+		return;
+	}
+
+	if (of->f_lease != NULL) {
+		ls = of->f_lease;
+		(void) nvlist_add_uint32(of->dh_nvlist,
+		    "lease_state", ls->ls_state);
+		(void) nvlist_add_uint16(of->dh_nvlist,
+		    "lease_epoch", ls->ls_epoch);
+	} else {
+		(void) nvlist_add_uint32(of->dh_nvlist,
+		    "oplock_state", of->f_oplock.og_state);
+	}
+	mutex_exit(&of->dh_nvlock);
+
+	sr->dh_nvl_dirty = B_TRUE;
+}
+
+/*
+ * Save locks from this ofile as an array of uint64_t, where the
+ * elements are triplets: (start, length, (pid << 32) | type)
+ * Note pid should always be zero for SMB2, so we could use
+ * that 32-bit spot for something else if needed.
+ */
+void
+smb2_dh_update_locks(smb_request_t *sr, smb_ofile_t *of)
+{
+	uint8_t		lseq[SMB_OFILE_LSEQ_MAX];
+	smb_node_t	*node = of->f_node;
+	smb_llist_t	*llist = &node->n_lock_list;
+	size_t		vec_sz;	// storage size
+	uint_t		my_cnt = 0;
+	uint64_t	*vec = NULL;
+	struct nvlk	*nlp;
+	smb_lock_t	*lock;
+
+	smb_llist_enter(llist, RW_READER);
+	vec_sz = (llist->ll_count + 1) * sizeof (struct nvlk);
+	vec = kmem_alloc(vec_sz, KM_SLEEP);
+	nlp = (struct nvlk *)vec;
+	for (lock = smb_llist_head(llist);
+	    lock != NULL;
+	    lock = smb_llist_next(llist, lock)) {
+		if (lock->l_file != of)
+			continue;
+		nlp->lk_start = lock->l_start;
+		nlp->lk_len = lock->l_length;
+		nlp->lk_pid = lock->l_pid;
+		nlp->lk_type = lock->l_type;
+		nlp++;
+		my_cnt++;
+	}
+	smb_llist_exit(llist);
+
+	mutex_enter(&of->f_mutex);
+	bcopy(of->f_lock_seq, lseq, sizeof (lseq));
+	mutex_exit(&of->f_mutex);
+
+	mutex_enter(&of->dh_nvlock);
+	if (of->dh_nvlist != NULL) {
+
+		(void) nvlist_add_uint64_array(of->dh_nvlist,
+		    "locks", vec, my_cnt * 3);
+
+		(void) nvlist_add_uint8_array(of->dh_nvlist,
+		    "lockseq", lseq, sizeof (lseq));
+	}
+	mutex_exit(&of->dh_nvlock);
+
+	kmem_free(vec, vec_sz);
+
+	sr->dh_nvl_dirty = B_TRUE;
+}
+
+/*
+ * Save "sticky" times
+ */
+void
+smb2_dh_update_times(smb_request_t *sr, smb_ofile_t *of, smb_attr_t *attr)
+{
+	hrtime_t t;
+
+	mutex_enter(&of->dh_nvlock);
+	if (of->dh_nvlist == NULL) {
+		mutex_exit(&of->dh_nvlock);
+		return;
+	}
+
+	if (attr->sa_mask & SMB_AT_ATIME) {
+		t = ts2hrt(&attr->sa_vattr.va_atime);
+		(void) nvlist_add_hrtime(of->dh_nvlist, "atime", t);
+	}
+	if (attr->sa_mask & SMB_AT_MTIME) {
+		t = ts2hrt(&attr->sa_vattr.va_mtime);
+		(void) nvlist_add_hrtime(of->dh_nvlist, "mtime", t);
+	}
+	if (attr->sa_mask & SMB_AT_CTIME) {
+		t = ts2hrt(&attr->sa_vattr.va_ctime);
+		(void) nvlist_add_hrtime(of->dh_nvlist, "ctime", t);
+	}
+	mutex_exit(&of->dh_nvlock);
+
+	sr->dh_nvl_dirty = B_TRUE;
+}
+
+
+/*
+ * Requirements for ofile found during reconnect (MS-SMB2 3.3.5.9.7):
+ * - security descriptor must match provided descriptor
+ *
+ * If file is leased:
+ * - lease must be requested
+ * - client guid must match session guid
+ * - file name must match given name
+ * - lease key must match provided lease key
+ * If file is not leased:
+ * - Lease must not be requested
+ *
+ * dh_v2 only:
+ * - SMB2_DHANDLE_FLAG_PERSISTENT must be set if dh_persist is true
+ * - SMB2_DHANDLE_FLAG_PERSISTENT must not be set if dh_persist is false
+ * - desired access, share access, and create_options must be ignored
+ * - createguid must match
+ */
+static uint32_t
+smb2_dh_reconnect_checks(smb_request_t *sr, smb_ofile_t *of)
+{
+	smb_arg_open_t	*op = &sr->sr_open;
+	char *fname;
+
+	if (of->f_lease != NULL) {
+		if (bcmp(sr->session->clnt_uuid,
+		    of->f_lease->ls_clnt, 16) != 0)
+			return (NT_STATUS_OBJECT_NAME_NOT_FOUND);
+
+		if (op->op_oplock_level != SMB2_OPLOCK_LEVEL_LEASE)
+			return (NT_STATUS_OBJECT_NAME_NOT_FOUND);
+		if (bcmp(op->lease_key, of->f_lease->ls_key,
+		    SMB_LEASE_KEY_SZ) != 0)
+			return (NT_STATUS_OBJECT_NAME_NOT_FOUND);
+
+		/*
+		 * We're supposed to check the name is the same.
+		 * Not really necessary to do this, so just do
+		 * minimal effort (check last component)
+		 */
+		fname = strrchr(op->fqi.fq_path.pn_path, '\\');
+		if (fname != NULL)
+			fname++;
+		else
+			fname = op->fqi.fq_path.pn_path;
+		if (smb_strcasecmp(fname, of->f_node->od_name, 0) != 0) {
+#ifdef	DEBUG
+			cmn_err(CE_NOTE, "reconnect name <%s> of name <%s>",
+			    fname, of->f_node->od_name);
+#endif
+			return (NT_STATUS_INVALID_PARAMETER);
+		}
+	} else {
+		if (op->op_oplock_level == SMB2_OPLOCK_LEVEL_LEASE)
+			return (NT_STATUS_OBJECT_NAME_NOT_FOUND);
+	}
+
+	if (op->dh_vers == SMB2_DURABLE_V2) {
+		boolean_t op_persist =
+		    ((op->dh_v2_flags & SMB2_DHANDLE_FLAG_PERSISTENT) != 0);
+		if (of->dh_persist != op_persist)
+			return (NT_STATUS_OBJECT_NAME_NOT_FOUND);
+		if (memcmp(op->create_guid, of->dh_create_guid, UUID_LEN))
+			return (NT_STATUS_OBJECT_NAME_NOT_FOUND);
+	}
+
+	if (!smb_is_same_user(sr->user_cr, of->f_cr))
+		return (NT_STATUS_ACCESS_DENIED);
+
+	return (NT_STATUS_SUCCESS);
+}
+
+/*
+ * [MS-SMB2] 3.3.5.9.7 and 3.3.5.9.12 (durable reconnect v1/v2)
+ *
+ * Looks up an ofile on the server's sv_dh_list by the persistid.
+ * If found, it validates the request.
+ * (see smb2_dh_reconnect_checks() for details)
+ * If the checks are passed, add it onto the new tree's list.
+ *
+ * Note that the oplock break code path can get to an ofile via the node
+ * ofile list.  It starts with a ref taken in smb_ofile_hold_olbrk, which
+ * waits if the ofile is found in state RECONNECT.  That wait happens with
+ * the node ofile list lock held as reader, and the oplock mutex held.
+ * Implications of that are: While we're in state RECONNECT, we shoud NOT
+ * block (at least, not for long) and must not try to enter any of the
+ * node ofile list lock or oplock mutex.  Thankfully, we don't need to
+ * enter those while reclaiming an orphaned ofile.
+ */
+uint32_t
+smb2_dh_reconnect(smb_request_t *sr)
+{
+	smb_arg_open_t	*op = &sr->sr_open;
+	smb_tree_t *tree = sr->tid_tree;
+	smb_ofile_t *of;
+	cred_t *old_cr;
+	uint32_t status = NT_STATUS_OBJECT_NAME_NOT_FOUND;
+	uint16_t fid = 0;
+
+	if (smb_idpool_alloc(&tree->t_fid_pool, &fid))
+		return (NT_STATUS_TOO_MANY_OPENED_FILES);
+
+	/* Find orphaned handle. */
+	of = smb_ofile_lookup_by_persistid(sr, op->dh_fileid.persistent);
+	if (of == NULL)
+		goto errout;
+
+	mutex_enter(&of->f_mutex);
+	if (of->f_state != SMB_OFILE_STATE_ORPHANED) {
+		mutex_exit(&of->f_mutex);
+		goto errout;
+	}
+
+	status = smb2_dh_reconnect_checks(sr, of);
+	if (status != NT_STATUS_SUCCESS) {
+		mutex_exit(&of->f_mutex);
+		goto errout;
+	}
+
+	/*
+	 * Note: cv_broadcast(&of->f_cv) when we're
+	 * done messing around in this state.
+	 * See: smb_ofile_hold_olbrk()
+	 */
+	of->f_state = SMB_OFILE_STATE_RECONNECT;
+	mutex_exit(&of->f_mutex);
+
+	/*
+	 * At this point, we should be the only thread with a ref on the
+	 * ofile, and the RECONNECT state should prevent new refs from
+	 * being granted, or other durable threads from observing or
+	 * reclaiming it. Put this ofile in the new tree, similar to
+	 * the last part of smb_ofile_open.
+	 */
+
+	old_cr = of->f_cr;
+	of->f_cr = sr->user_cr;
+	crhold(of->f_cr);
+	crfree(old_cr);
+
+	of->f_session = sr->session; /* hold is via user and tree */
+	smb_user_hold_internal(sr->uid_user);
+	of->f_user = sr->uid_user;
+	smb_tree_hold_internal(tree);
+	of->f_tree = tree;
+	of->f_fid = fid;
+
+	smb_llist_enter(&tree->t_ofile_list, RW_WRITER);
+	smb_llist_insert_tail(&tree->t_ofile_list, of);
+	smb_llist_exit(&tree->t_ofile_list);
+	atomic_inc_32(&tree->t_open_files);
+	atomic_inc_32(&sr->session->s_file_cnt);
+
+	/*
+	 * The ofile is now in the caller's session & tree.
+	 *
+	 * In case smb_ofile_hold or smb_oplock_send_brk() are
+	 * waiting for state RECONNECT to complete, wakeup.
+	 */
+	mutex_enter(&of->f_mutex);
+	of->dh_expire_time = 0;
+	of->f_state = SMB_OFILE_STATE_OPEN;
+	cv_broadcast(&of->f_cv);
+	mutex_exit(&of->f_mutex);
+
+	/*
+	 * The ofile is now visible in the new session.
+	 * From here, this is similar to the last part of
+	 * smb_common_open().
+	 */
+	op->fqi.fq_fattr.sa_mask = SMB_AT_ALL;
+	(void) smb_node_getattr(sr, of->f_node, zone_kcred(), of,
+	    &op->fqi.fq_fattr);
+
+	/*
+	 * Set up the fileid and dosattr in open_param for response
+	 */
+	op->fileid = op->fqi.fq_fattr.sa_vattr.va_nodeid;
+	op->dattr = op->fqi.fq_fattr.sa_dosattr;
+
+	/*
+	 * Set up the file type in open_param for the response
+	 * The ref. from ofile lookup is "given" to fid_ofile.
+	 */
+	op->ftype = SMB_FTYPE_DISK;
+	sr->smb_fid = of->f_fid;
+	sr->fid_ofile = of;
+
+	if (smb_node_is_file(of->f_node)) {
+		op->dsize = op->fqi.fq_fattr.sa_vattr.va_size;
+	} else {
+		/* directory or symlink */
+		op->dsize = 0;
+	}
+
+	op->create_options = 0; /* no more modifications wanted */
+	op->action_taken = SMB_OACT_OPENED;
+	return (NT_STATUS_SUCCESS);
+
+errout:
+	if (of != NULL)
+		smb_ofile_release(of);
+	if (fid != 0)
+		smb_idpool_free(&tree->t_fid_pool, fid);
+
+	return (status);
+}
+
+/*
+ * Durable handle expiration
+ * ofile state is _EXPIRED
+ */
+static void
+smb2_dh_expire(void *arg)
+{
+	smb_ofile_t *of = (smb_ofile_t *)arg;
+
+	if (of->dh_persist)
+		smb2_dh_setdoc_persistent(of);
+	smb_ofile_close(of, 0);
+	smb_ofile_release(of);
+}
+
+void
+smb2_durable_timers(smb_server_t *sv)
+{
+	smb_hash_t *hash;
+	smb_llist_t *bucket;
+	smb_ofile_t *of;
+	hrtime_t now;
+	int i;
+
+	hash = sv->sv_persistid_ht;
+	now = gethrtime();
+
+	for (i = 0; i < hash->num_buckets; i++) {
+		bucket = &hash->buckets[i].b_list;
+		smb_llist_enter(bucket, RW_READER);
+		for (of = smb_llist_head(bucket);
+		    of != NULL;
+		    of = smb_llist_next(bucket, of)) {
+			SMB_OFILE_VALID(of);
+
+			/*
+			 * Check outside the mutex first to avoid some
+			 * mutex_enter work in this loop.  If the state
+			 * changes under foot, the worst that happens
+			 * is we either enter the mutex when we might
+			 * not have needed to, or we miss some DH in
+			 * this pass and get it on the next.
+			 */
+			if (of->f_state != SMB_OFILE_STATE_ORPHANED)
+				continue;
+
+			mutex_enter(&of->f_mutex);
+			/* STATE_ORPHANED implies dh_expire_time != 0 */
+			if (of->f_state == SMB_OFILE_STATE_ORPHANED &&
+			    of->dh_expire_time <= now) {
+				of->f_state = SMB_OFILE_STATE_EXPIRED;
+				/* inline smb_ofile_hold_internal() */
+				of->f_refcnt++;
+				smb_llist_post(bucket, of, smb2_dh_expire);
+			}
+			mutex_exit(&of->f_mutex);
+		}
+		smb_llist_exit(bucket);
+	}
+}
+
+/*
+ * This is called when we're about to add a new open to some node.
+ * If we still have orphaned durable handles on this node, let's
+ * assume the client has lost interest in those and close them,
+ * otherwise we might conflict with our own orphaned handles.
+ *
+ * We need this because we import persistent handles "speculatively"
+ * during share import (before the client ever asks for reconnect).
+ * That allows us to avoid any need for a "create blackout" (or
+ * "grace period") because the imported handles prevent unwanted
+ * conflicting opens from other clients.  However, if some client
+ * "forgets" about a persistent handle (*cough* Hyper-V) and tries
+ * a new (conflicting) open instead of a reconnect, that might
+ * fail unless we expire our orphaned durables handle first.
+ *
+ * Logic similar to smb_node_open_check()
+ */
+void
+smb2_dh_close_my_orphans(smb_request_t *sr, smb_ofile_t *new_of)
+{
+	smb_node_t *node = new_of->f_node;
+	smb_ofile_t *of;
+
+	SMB_NODE_VALID(node);
+
+	smb_llist_enter(&node->n_ofile_list, RW_READER);
+	for (of = smb_llist_head(&node->n_ofile_list);
+	    of != NULL;
+	    of = smb_llist_next(&node->n_ofile_list, of)) {
+
+		/* Same client? */
+		if (of->f_lease != NULL &&
+		    bcmp(sr->session->clnt_uuid,
+		    of->f_lease->ls_clnt, 16) != 0)
+			continue;
+
+		if (!smb_is_same_user(sr->user_cr, of->f_cr))
+			continue;
+
+		mutex_enter(&of->f_mutex);
+		if (of->f_state == SMB_OFILE_STATE_ORPHANED) {
+			of->f_state = SMB_OFILE_STATE_EXPIRED;
+			/* inline smb_ofile_hold_internal() */
+			of->f_refcnt++;
+			smb_llist_post(&node->n_ofile_list,
+			    of, smb2_dh_expire);
+		}
+		mutex_exit(&of->f_mutex);
+	}
+
+	smb_llist_exit(&node->n_ofile_list);
+}
+
+/*
+ * Called for each orphaned DH during shutdown.
+ * Clean out any in-memory state, but leave any
+ * on-disk persistent handle state in place.
+ */
+static void
+smb2_dh_cleanup(void *arg)
+{
+	smb_ofile_t *of = (smb_ofile_t *)arg;
+	smb_node_t *strnode;
+	struct nvlist *nvl;
+
+	/*
+	 * Intentionally skip smb2_dh_close_persistent by
+	 * clearing dh_nvfile before smb_ofile_close().
+	 */
+	mutex_enter(&of->dh_nvlock);
+	strnode = of->dh_nvfile;
+	of->dh_nvfile = NULL;
+	nvl = of->dh_nvlist;
+	of->dh_nvlist = NULL;
+	mutex_exit(&of->dh_nvlock);
+
+	if (nvl != NULL)
+		nvlist_free(nvl);
+
+	if (strnode != NULL)
+		smb_node_release(strnode);
+
+	smb_ofile_close(of, 0);
+	smb_ofile_release(of);
+}
+
+/*
+ * Clean out durable handles during shutdown.
+ *
+ * Like, smb2_durable_timers but cleanup only in-memory state,
+ * and leave any persistent state there for later reconnect.
+ */
+void
+smb2_dh_shutdown(smb_server_t *sv)
+{
+	smb_hash_t *hash;
+	smb_llist_t *bucket;
+	smb_ofile_t *of;
+	int i;
+
+	hash = sv->sv_persistid_ht;
+
+	for (i = 0; i < hash->num_buckets; i++) {
+		bucket = &hash->buckets[i].b_list;
+		smb_llist_enter(bucket, RW_READER);
+		of = smb_llist_head(bucket);
+		while (of != NULL) {
+			SMB_OFILE_VALID(of);
+			mutex_enter(&of->f_mutex);
+
+			switch (of->f_state) {
+			case SMB_OFILE_STATE_ORPHANED:
+				of->f_state = SMB_OFILE_STATE_EXPIRED;
+				/* inline smb_ofile_hold_internal() */
+				of->f_refcnt++;
+				smb_llist_post(bucket, of, smb2_dh_cleanup);
+				break;
+			default:
+				break;
+			}
+			mutex_exit(&of->f_mutex);
+			of = smb_llist_next(bucket, of);
+		}
+		smb_llist_exit(bucket);
+	}
+
+#ifdef	DEBUG
+	for (i = 0; i < hash->num_buckets; i++) {
+		bucket = &hash->buckets[i].b_list;
+		smb_llist_enter(bucket, RW_READER);
+		of = smb_llist_head(bucket);
+		while (of != NULL) {
+			SMB_OFILE_VALID(of);
+			cmn_err(CE_NOTE, "dh_shutdown leaked of=%p",
+			    (void *)of);
+			of = smb_llist_next(bucket, of);
+		}
+		smb_llist_exit(bucket);
+	}
+#endif	// DEBUG
+}
+
+uint32_t
+smb2_fsctl_set_resilient(smb_request_t *sr, smb_fsctl_t *fsctl)
+{
+	uint32_t timeout;
+	smb_ofile_t *of = sr->fid_ofile;
+
+	/*
+	 * Note: The spec does not explicitly prohibit resilient directories
+	 * the same way it prohibits durable directories. We prohibit them
+	 * anyway as a simplifying assumption, as there doesn't seem to be
+	 * much use for it. (HYPER-V only seems to use it on files anyway)
+	 */
+	if (fsctl->InputCount < 8 || !smb_node_is_file(of->f_node))
+		return (NT_STATUS_INVALID_PARAMETER);
+
+	(void) smb_mbc_decodef(fsctl->in_mbc, "l4.",
+	    &timeout); /* milliseconds */
+
+	if (smb2_enable_dh == 0)
+		return (NT_STATUS_NOT_SUPPORTED);
+
+	/*
+	 * The spec wants us to return INVALID_PARAMETER if the timeout
+	 * is too large, but we have no way of informing the client
+	 * what an appropriate timeout is, so just set the timeout to
+	 * our max and return SUCCESS.
+	 */
+	if (timeout == 0)
+		timeout = smb2_res_def_timeout;
+	if (timeout > smb2_res_max_timeout)
+		timeout = smb2_res_max_timeout;
+
+	mutex_enter(&of->f_mutex);
+	of->dh_vers = SMB2_RESILIENT;
+	of->dh_timeout_offset = MSEC2NSEC(timeout);
+	mutex_exit(&of->f_mutex);
+
+	return (NT_STATUS_SUCCESS);
+}
