#include <smbsrv/smb2_kproto.h>
#include <smbsrv/smb_oplock.h>
#define SSZ_OPLOCK 24
#define SSZ_LEASE_ACK 36
#define SSZ_LEASE_BRK 44
#define NODE_FLAGS_DELETING (NODE_FLAGS_DELETE_ON_CLOSE |\
NODE_FLAGS_DELETE_COMMITTED)
static const char lease_zero[UUID_LEN] = { 0 };
static kmem_cache_t *smb_lease_cache = NULL;
void
smb2_lease_init()
{
if (smb_lease_cache != NULL)
return;
smb_lease_cache = kmem_cache_create("smb_lease_cache",
sizeof (smb_lease_t), 8, NULL, NULL, NULL, NULL, NULL, 0);
}
void
smb2_lease_fini()
{
if (smb_lease_cache != NULL) {
kmem_cache_destroy(smb_lease_cache);
smb_lease_cache = NULL;
}
}
static boolean_t
smb2_lease_hold(smb_lease_t *ls)
{
boolean_t ret;
mutex_enter(&ls->ls_mutex);
ret = !ls->ls_destroying;
if (ret)
ls->ls_refcnt++;
mutex_exit(&ls->ls_mutex);
return (ret);
}
static void
lease_destroy(smb_lease_t *ls)
{
smb_node_release(ls->ls_node);
mutex_destroy(&ls->ls_mutex);
kmem_cache_free(smb_lease_cache, ls);
}
void
smb2_lease_rele(smb_lease_t *ls)
{
smb_llist_t *bucket;
boolean_t destroy = B_FALSE;
mutex_enter(&ls->ls_mutex);
ls->ls_refcnt--;
if (ls->ls_refcnt != 0 || ls->ls_destroying) {
mutex_exit(&ls->ls_mutex);
return;
}
ls->ls_destroying = B_TRUE;
mutex_exit(&ls->ls_mutex);
bucket = ls->ls_bucket;
smb_llist_enter(bucket, RW_WRITER);
mutex_enter(&ls->ls_mutex);
if (ls->ls_refcnt == 0) {
smb_llist_remove(bucket, ls);
destroy = B_TRUE;
} else {
ls->ls_destroying = B_FALSE;
}
mutex_exit(&ls->ls_mutex);
smb_llist_exit(bucket);
if (destroy) {
lease_destroy(ls);
}
}
static uint_t
smb_hash_uuid(const uint8_t *uuid)
{
char *k = (char *)uuid;
uint_t hash = 0;
uint_t g;
int i;
ASSERT(k);
for (i = 0; i < UUID_LEN; i++) {
hash = (hash << 4) + k[i];
if ((g = (hash & 0xf0000000)) != 0) {
hash ^= (g >> 24);
hash ^= g;
}
}
return (hash);
}
uint32_t
smb2_lease_create(smb_request_t *sr, uint8_t *clnt)
{
smb_arg_open_t *op = &sr->arg.open;
uint8_t *key = op->lease_key;
smb_ofile_t *of = sr->fid_ofile;
smb_hash_t *ht = sr->sr_server->sv_lease_ht;
smb_llist_t *bucket;
smb_lease_t *lease;
smb_lease_t *newlease;
size_t hashkey;
uint32_t status = NT_STATUS_INVALID_PARAMETER;
if (bcmp(key, lease_zero, UUID_LEN) == 0)
return (status);
hashkey = smb_hash_uuid(key);
hashkey &= (ht->num_buckets - 1);
bucket = &ht->buckets[hashkey].b_list;
newlease = kmem_cache_alloc(smb_lease_cache, KM_SLEEP);
bzero(newlease, sizeof (smb_lease_t));
mutex_init(&newlease->ls_mutex, NULL, MUTEX_DEFAULT, NULL);
newlease->ls_bucket = bucket;
newlease->ls_node = of->f_node;
smb_node_ref(newlease->ls_node);
newlease->ls_refcnt = 1;
newlease->ls_epoch = op->lease_epoch;
newlease->ls_version = op->lease_version;
bcopy(key, newlease->ls_key, UUID_LEN);
bcopy(clnt, newlease->ls_clnt, UUID_LEN);
smb_llist_enter(bucket, RW_WRITER);
for (lease = smb_llist_head(bucket); lease != NULL;
lease = smb_llist_next(bucket, lease)) {
if (bcmp(lease->ls_key, key, UUID_LEN) == 0 &&
bcmp(lease->ls_clnt, clnt, UUID_LEN) == 0 &&
(lease->ls_node->flags & NODE_FLAGS_DELETING) == 0 &&
smb2_lease_hold(lease))
break;
}
if (lease == NULL) {
lease = newlease;
smb_llist_insert_head(bucket, lease);
newlease = NULL;
}
smb_llist_exit(bucket);
if (lease->ls_node != of->f_node) {
#ifdef DEBUG
cmn_err(CE_NOTE, "new lease on node %p (%s) "
"conflicts with existing node %p (%s)",
(void *) of->f_node,
of->f_node->od_name,
(void *) lease->ls_node,
lease->ls_node->od_name);
#endif
DTRACE_PROBE2(dup_lease, smb_request_t *, sr,
smb_lease_t *, lease);
smb2_lease_rele(lease);
lease = NULL;
}
if (newlease != NULL) {
lease_destroy(newlease);
}
if (lease != NULL) {
of->f_lease = lease;
status = NT_STATUS_SUCCESS;
}
return (status);
}
static smb_lease_t *
lease_lookup(smb_request_t *sr, uint8_t *lease_key)
{
smb_server_t *sv = sr->sr_server;
uint8_t *clnt_uuid = sr->session->clnt_uuid;
smb_hash_t *ht = sv->sv_lease_ht;
smb_llist_t *bucket;
smb_lease_t *lease;
size_t hashkey;
hashkey = smb_hash_uuid(lease_key);
hashkey &= (ht->num_buckets - 1);
bucket = &ht->buckets[hashkey].b_list;
smb_llist_enter(bucket, RW_READER);
lease = smb_llist_head(bucket);
while (lease != NULL) {
if (bcmp(lease->ls_key, lease_key, UUID_LEN) == 0 &&
bcmp(lease->ls_clnt, clnt_uuid, UUID_LEN) == 0 &&
smb2_lease_hold(lease))
break;
lease = smb_llist_next(bucket, lease);
}
smb_llist_exit(bucket);
return (lease);
}
static uint32_t
lease_find_oplock(smb_request_t *sr, smb_lease_t *lease)
{
smb_node_t *node = lease->ls_node;
smb_ofile_t *o;
uint32_t status = NT_STATUS_UNSUCCESSFUL;
ASSERT(RW_READ_HELD(&node->n_ofile_list.ll_lock));
ASSERT(MUTEX_HELD(&node->n_oplock.ol_mutex));
ASSERT(sr->fid_ofile == NULL);
FOREACH_NODE_OFILE(node, o) {
if (o->f_lease != lease)
continue;
if (o != lease->ls_oplock_ofile)
continue;
if (smb_ofile_hold_olbrk(o)) {
sr->fid_ofile = o;
status = NT_STATUS_SUCCESS;
break;
}
}
return (status);
}
smb_sdrc_t
smb2_lease_break_ack(smb_request_t *sr)
{
smb_arg_olbrk_t *olbrk = &sr->arg.olbrk;
smb_lease_t *lease;
smb_node_t *node;
smb_ofile_t *ofile;
uint32_t LeaseState;
uint32_t status;
int rc = 0;
if (sr->session->dialect < SMB_VERS_2_1)
return (SDRC_ERROR);
rc = smb_mbc_decodef(
&sr->smb_data, "6.#cl8.",
UUID_LEN,
olbrk->LeaseKey,
&olbrk->NewLevel);
if (rc != 0)
return (SDRC_ERROR);
LeaseState = olbrk->NewLevel;
lease = lease_lookup(sr, olbrk->LeaseKey);
if (lease == NULL) {
status = NT_STATUS_OBJECT_NAME_NOT_FOUND;
smb2sr_put_error(sr, status);
return (SDRC_SUCCESS);
}
node = lease->ls_node;
smb_llist_enter(&node->n_ofile_list, RW_READER);
mutex_enter(&node->n_oplock.ol_mutex);
status = lease_find_oplock(sr, lease);
DTRACE_SMB2_START(op__OplockBreak, smb_request_t *, sr);
if (status != 0) {
goto errout;
}
ofile = sr->fid_ofile;
if (lease->ls_breaking == B_FALSE) {
status = NT_STATUS_UNSUCCESSFUL;
goto errout;
}
if ((LeaseState & ~(lease->ls_breakto)) != 0) {
status = NT_STATUS_REQUEST_NOT_ACCEPTED;
goto errout;
}
ofile->f_oplock.og_breaking = B_FALSE;
cv_broadcast(&ofile->f_oplock.og_ack_cv);
lease->ls_breaking = B_FALSE;
cv_broadcast(&lease->ls_ack_cv);
LeaseState |= OPLOCK_LEVEL_GRANULAR;
status = smb_oplock_ack_break(sr, ofile, &LeaseState);
ofile->f_oplock.og_state = LeaseState;
lease->ls_state = LeaseState;
if (ofile->dh_persist)
smb2_dh_update_oplock(sr, ofile);
errout:
sr->smb2_status = status;
DTRACE_SMB2_DONE(op__OplockBreak, smb_request_t *, sr);
mutex_exit(&node->n_oplock.ol_mutex);
smb_llist_exit(&node->n_ofile_list);
smb2_lease_rele(lease);
if (status) {
smb2sr_put_error(sr, status);
return (SDRC_SUCCESS);
}
LeaseState &= OPLOCK_LEVEL_CACHE_MASK;
(void) smb_mbc_encodef(
&sr->reply, "w6.#cl8.",
SSZ_LEASE_ACK,
UUID_LEN,
olbrk->LeaseKey,
LeaseState);
return (SDRC_SUCCESS);
}
static void
smb2_lease_break_notification(smb_request_t *sr,
uint32_t OldLevel, uint32_t NewLevel,
uint16_t Epoch, boolean_t AckReq)
{
smb_lease_t *ls = sr->fid_ofile->f_lease;
uint16_t Flags = 0;
if (AckReq)
Flags = SMB2_NOTIFY_BREAK_LEASE_FLAG_ACK_REQUIRED;
if (ls->ls_version < 2)
Epoch = 0;
OldLevel &= OPLOCK_LEVEL_CACHE_MASK;
NewLevel &= OPLOCK_LEVEL_CACHE_MASK;
sr->smb2_cmd_code = SMB2_OPLOCK_BREAK;
sr->smb2_hdr_flags = SMB2_FLAGS_SERVER_TO_REDIR;
sr->smb_tid = 0;
sr->smb_pid = 0;
sr->smb2_ssnid = 0;
sr->smb2_messageid = UINT64_MAX;
(void) smb2_encode_header(sr, B_FALSE);
(void) smb_mbc_encodef(
&sr->reply, "wwl#cll4.4.4.",
SSZ_LEASE_BRK,
Epoch,
Flags,
SMB_LEASE_KEY_SZ,
ls->ls_key,
OldLevel,
NewLevel);
}
static int
lease_send_any_cn(smb_request_t *sr)
{
smb_ofile_t *o;
smb_ofile_t *ofile = sr->fid_ofile;
smb_lease_t *lease = ofile->f_lease;
smb_node_t *node = ofile->f_node;
int rc = ENOTCONN;
if (sr->session == ofile->f_session) {
rc = smb_session_send(sr->session, 0, &sr->reply);
if (rc == 0)
return (rc);
}
smb_llist_enter(&node->n_ofile_list, RW_READER);
FOREACH_NODE_OFILE(node, o) {
if (o->f_lease != lease)
continue;
if (smb_ofile_hold(o)) {
rc = smb_session_send(o->f_session, 0, &sr->reply);
smb_llist_post(&node->n_ofile_list, o,
smb_ofile_release_LL);
}
if (rc == 0)
break;
}
smb_llist_exit(&node->n_ofile_list);
return (rc);
}
static void
lease_ofile_close_rele(void *arg)
{
smb_ofile_t *of = (smb_ofile_t *)arg;
smb_ofile_close(of, 0);
smb_ofile_release(of);
}
static void
lease_close_notconn(smb_request_t *sr, uint32_t NewLevel)
{
smb_ofile_t *o;
smb_ofile_t *ofile = sr->fid_ofile;
smb_lease_t *lease = ofile->f_lease;
smb_node_t *node = ofile->f_node;
smb_llist_enter(&node->n_ofile_list, RW_READER);
FOREACH_NODE_OFILE(node, o) {
if (o->f_lease != lease)
continue;
if (o->f_oplock_closing)
continue;
if (o->dh_persist)
continue;
if (o->dh_vers == SMB2_RESILIENT)
continue;
if (o->dh_vers == SMB2_NOT_DURABLE ||
(NewLevel & OPLOCK_LEVEL_CACHE_HANDLE) == 0) {
if (smb_ofile_hold_olbrk(o)) {
smb_llist_post(&node->n_ofile_list, o,
lease_ofile_close_rele);
}
}
}
smb_llist_exit(&node->n_ofile_list);
}
void
smb2_lease_send_break(smb_request_t *sr)
{
smb_ofile_t *old_ofile;
smb_ofile_t *ofile = sr->fid_ofile;
smb_node_t *node = ofile->f_node;
smb_lease_t *lease = ofile->f_lease;
smb_arg_olbrk_t *olbrk = &sr->arg.olbrk;
boolean_t AckReq = olbrk->AckRequired;
uint32_t OldLevel = olbrk->OldLevel;
uint32_t NewLevel = olbrk->NewLevel;
uint32_t status;
int rc;
NewLevel |= OPLOCK_LEVEL_GRANULAR;
sr->reply.max_bytes = MLEN;
smb2_lease_break_notification(sr,
OldLevel, NewLevel, lease->ls_epoch, AckReq);
rc = lease_send_any_cn(sr);
if (rc != 0) {
lease_close_notconn(sr, NewLevel);
if (!AckReq)
return;
} else {
if (!AckReq)
return;
status = smb_oplock_wait_ack(sr, NewLevel);
if (status == 0)
return;
DTRACE_PROBE2(wait__ack__failed, smb_request_t *, sr,
uint32_t, status);
NewLevel = OPLOCK_LEVEL_GRANULAR;
}
smb_llist_enter(&node->n_ofile_list, RW_READER);
mutex_enter(&node->n_oplock.ol_mutex);
old_ofile = ofile;
sr->fid_ofile = NULL;
status = lease_find_oplock(sr, lease);
if (status != 0) {
sr->fid_ofile = old_ofile;
goto unlock_out;
}
smb_llist_post(&node->n_ofile_list, old_ofile,
smb_ofile_release_LL);
ofile = sr->fid_ofile;
ofile->f_oplock.og_breaking = B_FALSE;
lease->ls_breaking = B_FALSE;
cv_broadcast(&lease->ls_ack_cv);
status = smb_oplock_ack_break(sr, ofile, &NewLevel);
ofile->f_oplock.og_state = NewLevel;
lease->ls_state = NewLevel;
if (ofile->dh_persist)
smb2_dh_update_oplock(sr, ofile);
unlock_out:
mutex_exit(&node->n_oplock.ol_mutex);
smb_llist_exit(&node->n_ofile_list);
#ifdef DEBUG
if (status != 0) {
cmn_err(CE_NOTE, "clnt %s local oplock ack, status=0x%x",
sr->session->ip_addr_str, status);
}
#endif
}
void
smb2_lease_acquire(smb_request_t *sr)
{
smb_arg_open_t *op = &sr->arg.open;
smb_ofile_t *ofile = sr->fid_ofile;
smb_lease_t *lease = ofile->f_lease;
smb_node_t *node = ofile->f_node;
uint32_t status = NT_STATUS_OPLOCK_NOT_GRANTED;
uint32_t have, want;
boolean_t NewGrant = B_FALSE;
ASSERT((sr->tid_tree->t_res_type & STYPE_MASK) == STYPE_DISKTREE);
if (!smb_node_is_file(ofile->f_node)) {
op->op_oplock_level = SMB2_OPLOCK_LEVEL_NONE;
return;
}
if (!smb_tree_has_feature(sr->tid_tree, SMB_TREE_OPLOCKS)) {
op->op_oplock_level = SMB2_OPLOCK_LEVEL_NONE;
return;
}
ASSERT(op->op_oplock_level == SMB2_OPLOCK_LEVEL_LEASE);
ASSERT(lease != NULL);
if (lease == NULL) {
op->op_oplock_level = SMB2_OPLOCK_LEVEL_NONE;
return;
}
op->op_oplock_state = OPLOCK_LEVEL_GRANULAR |
(op->lease_state & CACHE_RWH);
if (smb_tree_has_feature(sr->tid_tree, SMB_TREE_FORCE_L2_OPLOCK)) {
op->op_oplock_state &= ~WRITE_CACHING;
}
smb_llist_enter(&node->n_ofile_list, RW_READER);
mutex_enter(&node->n_oplock.ol_mutex);
have = lease->ls_state & CACHE_RWH;
want = op->op_oplock_state & CACHE_RWH;
if ((have & ~want) != 0 || lease->ls_breaking) {
op->op_oplock_state = have |
OPLOCK_LEVEL_GRANULAR;
goto done;
}
if ((op->op_oplock_state & WRITE_CACHING) != 0) {
status = smb_oplock_request_LH(sr, ofile,
&op->op_oplock_state);
if (status == NT_STATUS_SUCCESS ||
status == NT_STATUS_OPLOCK_BREAK_IN_PROGRESS) {
NewGrant = B_TRUE;
goto done;
}
if (have != 0) {
op->op_oplock_state = have |
OPLOCK_LEVEL_GRANULAR;
goto done;
}
op->op_oplock_state = OPLOCK_LEVEL_GRANULAR |
(op->lease_state & CACHE_RH);
}
if ((op->op_oplock_state & HANDLE_CACHING) != 0) {
want = op->op_oplock_state & CACHE_RWH;
if ((want & ~have) == 0)
goto done;
status = smb_oplock_request_LH(sr, ofile,
&op->op_oplock_state);
if (status == NT_STATUS_SUCCESS ||
status == NT_STATUS_OPLOCK_BREAK_IN_PROGRESS) {
NewGrant = B_TRUE;
goto done;
}
op->op_oplock_state = OPLOCK_LEVEL_GRANULAR |
(op->lease_state & CACHE_R);
}
if ((op->op_oplock_state & READ_CACHING) != 0) {
want = op->op_oplock_state & CACHE_RWH;
if ((want & ~have) == 0)
goto done;
status = smb_oplock_request_LH(sr, ofile,
&op->op_oplock_state);
if (status == NT_STATUS_SUCCESS ||
status == NT_STATUS_OPLOCK_BREAK_IN_PROGRESS) {
NewGrant = B_TRUE;
goto done;
}
}
op->op_oplock_state = OPLOCK_LEVEL_GRANULAR;
done:
if (NewGrant) {
ofile->f_oplock.og_state = op->op_oplock_state;
ofile->f_oplock.og_breakto = op->op_oplock_state;
ofile->f_oplock.og_breaking = B_FALSE;
lease->ls_oplock_ofile = ofile;
lease->ls_state = ofile->f_oplock.og_state;
lease->ls_breakto = ofile->f_oplock.og_breakto;
lease->ls_breaking = B_FALSE;
lease->ls_epoch++;
if (ofile->dh_persist) {
smb2_dh_update_oplock(sr, ofile);
}
}
op->op_oplock_level = SMB2_OPLOCK_LEVEL_LEASE;
op->lease_state = lease->ls_state & CACHE_RWH;
op->lease_flags = (lease->ls_breaking != 0) ?
SMB2_LEASE_FLAG_BREAK_IN_PROGRESS : 0;
op->lease_epoch = lease->ls_epoch;
op->lease_version = lease->ls_version;
mutex_exit(&node->n_oplock.ol_mutex);
smb_llist_exit(&node->n_ofile_list);
if (status == NT_STATUS_OPLOCK_BREAK_IN_PROGRESS) {
(void) smb2sr_go_async(sr);
(void) smb_oplock_wait_break(sr, ofile->f_node, 0);
}
}
void
smb2_lease_ofile_close(smb_ofile_t *ofile)
{
smb_node_t *node = ofile->f_node;
smb_lease_t *lease = ofile->f_lease;
smb_ofile_t *o;
ASSERT(RW_READ_HELD(&node->n_ofile_list.ll_lock));
ASSERT(MUTEX_HELD(&node->n_oplock.ol_mutex));
#ifdef DEBUG
FOREACH_NODE_OFILE(node, o) {
DTRACE_PROBE1(each_ofile, smb_ofile_t *, o);
}
#endif
if (lease->ls_oplock_ofile != ofile)
return;
FOREACH_NODE_OFILE(node, o) {
if (o == ofile)
continue;
if (o->f_lease != lease)
continue;
if (o->f_oplock_closing)
continue;
mutex_enter(&o->f_mutex);
if (o->f_state == SMB_OFILE_STATE_OPEN) {
smb_oplock_move(node, ofile, o);
lease->ls_oplock_ofile = o;
mutex_exit(&o->f_mutex);
return;
}
mutex_exit(&o->f_mutex);
}
FOREACH_NODE_OFILE(node, o) {
if (o == ofile)
continue;
if (o->f_lease != lease)
continue;
if (o->f_oplock_closing)
continue;
mutex_enter(&o->f_mutex);
switch (o->f_state) {
case SMB_OFILE_STATE_OPEN:
case SMB_OFILE_STATE_SAVE_DH:
case SMB_OFILE_STATE_SAVING:
case SMB_OFILE_STATE_ORPHANED:
case SMB_OFILE_STATE_RECONNECT:
smb_oplock_move(node, ofile, o);
lease->ls_oplock_ofile = o;
mutex_exit(&o->f_mutex);
return;
}
mutex_exit(&o->f_mutex);
}
lease->ls_state = 0;
lease->ls_breakto = 0;
lease->ls_breaking = B_FALSE;
cv_broadcast(&lease->ls_ack_cv);
lease->ls_oplock_ofile = NULL;
}