/* * CDDL HEADER START * * The contents of this file are subject to the terms of the * Common Development and Distribution License (the "License"). * You may not use this file except in compliance with the License. * * You can obtain a copy of the license at usr/src/OPENSOLARIS.LICENSE * or http://www.opensolaris.org/os/licensing. * See the License for the specific language governing permissions * and limitations under the License. * * When distributing Covered Code, include this CDDL HEADER in each * file and include the License file at usr/src/OPENSOLARIS.LICENSE. * If applicable, add the following below this CDDL HEADER, with the * fields enclosed by brackets "[]" replaced with your own identifying * information: Portions Copyright [yyyy] [name of copyright owner] * * CDDL HEADER END */ /* * Copyright (c) 2007, 2010, Oracle and/or its affiliates. All rights reserved. * Copyright 2020 Tintri by DDN, Inc. All rights reserved. * Copyright 2020-2023 RackTop Systems, Inc. */ /* * File Change Notification (FCN) * Common parts shared by SMB1 & SMB2 */ /* * This command notifies the client when the specified directory * has changed, and optionally returns the names of files and * directories that changed, and how they changed. The caller * specifies a "Completion Filter" to select which kinds of * changes they want to know about. * * When a change that's in the CompletionFilter is made to the directory, * the command completes. The names of the files that have changed since * the last time the command was issued are returned to the client. * If too many files have changed since the last time the command was * issued, then zero bytes are returned and an alternate status code * is returned in the Status field of the response. * * The CompletionFilter is a mask created as the sum of any of the * following flags: * * FILE_NOTIFY_CHANGE_FILE_NAME 0x00000001 * FILE_NOTIFY_CHANGE_DIR_NAME 0x00000002 * FILE_NOTIFY_CHANGE_NAME 0x00000003 * FILE_NOTIFY_CHANGE_ATTRIBUTES 0x00000004 * FILE_NOTIFY_CHANGE_SIZE 0x00000008 * FILE_NOTIFY_CHANGE_LAST_WRITE 0x00000010 * FILE_NOTIFY_CHANGE_LAST_ACCESS 0x00000020 * FILE_NOTIFY_CHANGE_CREATION 0x00000040 * FILE_NOTIFY_CHANGE_EA 0x00000080 * FILE_NOTIFY_CHANGE_SECURITY 0x00000100 * FILE_NOTIFY_CHANGE_STREAM_NAME 0x00000200 * FILE_NOTIFY_CHANGE_STREAM_SIZE 0x00000400 * FILE_NOTIFY_CHANGE_STREAM_WRITE 0x00000800 * * * The response contains FILE_NOTIFY_INFORMATION structures, as defined * below. The NextEntryOffset field of the structure specifies the offset, * in bytes, from the start of the current entry to the next entry in the * list. If this is the last entry in the list, this field is zero. Each * entry in the list must be longword aligned, so NextEntryOffset must be a * multiple of four. * * typedef struct { * ULONG NextEntryOffset; * ULONG Action; * ULONG FileNameLength; * WCHAR FileName[1]; * } FILE_NOTIFY_INFORMATION; * * Where Action describes what happened to the file named FileName: * * FILE_ACTION_ADDED 0x00000001 * FILE_ACTION_REMOVED 0x00000002 * FILE_ACTION_MODIFIED 0x00000003 * FILE_ACTION_RENAMED_OLD_NAME 0x00000004 * FILE_ACTION_RENAMED_NEW_NAME 0x00000005 * FILE_ACTION_ADDED_STREAM 0x00000006 * FILE_ACTION_REMOVED_STREAM 0x00000007 * FILE_ACTION_MODIFIED_STREAM 0x00000008 * * The internal interface between SMB1 and/or SMB2 protocol handlers * and this module has some sophistication to allow for: * (1) code sharing between SMB1 and SMB2(+) * (2) efficient handling of non-blocking scenarios * (3) long blocking calls without tying up a thread * * The interface has three calls (like a three act play) * * smb_notify_act1: * Validate parameters, setup ofile buffer. * If data already available, return it, all done. * (In the "all done" case, skip act2 & act3.) * If no data available, return a special error * ("STATUS_PENDING") to tell the caller they must * proceed with calls to act2 & act3. * * smb_notify_act2: * Arrange wakeup after event delivery or cancellation. * Return leaving the SR with no worker thread. * * smb_notify_act3: * New taskq work thread runs this after the wakeup * or cancellation arranged in act2 happens. This * returns the notification data and retires the SR. * * In the SMB2 notify handler, we call act1 during the initial * synchronous handling of the request. If that returns anything * other than STATUS_PENDING, that request is fully complete. * If act1 returns STATUS_PENDING, SMB2 calls act2 as it's * "go async" handler, which arranges to call act3 later. * * In the SMB1 notify handler there is not separate sync. & async * handler so act1 and (if necessary) act2 are both called during * the initial handling of the request. * * About notify event buffering: * * An important (and poorly documented) feature of SMB notify is * that once a notify call has happened on a given directory handle, * the system CONTINUES to post events to the notify event buffer * for the handle, even when SMB notify calls are NOT running. * When the client next comes back with a notify call, we return * any events that were posted while they were "away". This is * how clients track directory changes without missing events. * * About simultaneous notify calls: * * Note that SMB "notify" calls are destructive to events, much like * reading data from a pipe. It therefore makes little sense to * allow multiple simultaneous callers. However, we permit it * (like Windows does) as follows: When multiple notify calls * are waiting for events, the next event wakes them all, and * only the last one out clears the event buffer. They all get * whatever events are pending at the time they woke up. * * About NT_STATUS_NOTIFY_ENUM_DIR * * One more caution about NT_STATUS_NOTIFY_ENUM_DIR: Some clients * are stupid about re-reading the directory almost continuously when * there are changes happening in the directory. We want to bound * the rate of such directory re-reading, so before returning an * NT_STATUS_NOTIFY_ENUM_DIR, we delay just a little. The length * of the delay can be adjusted via smb_notify_enum_dir_delay, * though it's not expected that should need to be changed. */ #include #include /* * Length of the short delay we impose before returning * NT_STATUS_NOTIFY_ENUM_DIR (See above) */ int smb_notify_enum_dir_delay = 100; /* mSec. */ static uint32_t smb_notify_get_events(smb_request_t *); static void smb_notify_cancel(smb_request_t *); static void smb_notify_wakeup(smb_request_t *); static void smb_notify_dispatch2(smb_request_t *); static void smb_notify_encode_action(smb_ofile_t *, uint32_t, const char *); /* * smb_notify_act1() * * Check for events and consume, non-blocking. * Special return STATUS_PENDING means: * No events; caller must call "act2" next. * * See overall design notes, top of file. */ uint32_t smb_notify_act1(smb_request_t *sr, uint32_t buflen, uint32_t filter) { smb_ofile_t *of; smb_node_t *node; smb_notify_t *nc; uint32_t status; /* * Validate parameters */ if ((of = sr->fid_ofile) == NULL) return (NT_STATUS_INVALID_HANDLE); nc = &of->f_notify; node = of->f_node; if (node == NULL || !smb_node_is_dir(node)) { /* Notify change is only valid on directories. */ return (NT_STATUS_INVALID_PARAMETER); } if ((of->f_granted_access & FILE_LIST_DIRECTORY) == 0) return (NT_STATUS_ACCESS_DENIED); mutex_enter(&of->f_mutex); /* * It's possible this ofile has started closing, in which case * we must not subscribe it for events etc. */ if (of->f_state != SMB_OFILE_STATE_OPEN) { mutex_exit(&of->f_mutex); return (NT_STATUS_FILE_CLOSED); } /* * On the first FCN call with this ofile, subscribe to * events on the node. The corresponding unsubscribe * happens in smb_ofile_delete(). */ if (nc->nc_subscribed == B_FALSE) { nc->nc_subscribed = B_TRUE; smb_node_fcn_subscribe(node); /* In case this happened before we subscribed. */ if (node->flags & NODE_FLAGS_DELETE_ON_CLOSE) { nc->nc_events |= FILE_NOTIFY_CHANGE_EV_DELETE; } /* * Windows only lets you set these on the first call, * so we may as well do the same. */ nc->nc_buffer.max_bytes = buflen; nc->nc_filter = filter; } /* * If we already have events, consume them. */ sr->raw_data.max_bytes = buflen; if (nc->nc_events != 0) { status = smb_notify_get_events(sr); } else { /* Caller will come back for act2 */ status = NT_STATUS_PENDING; } mutex_exit(&of->f_mutex); /* * See: About NT_STATUS_NOTIFY_ENUM_DIR (above) */ if (status == NT_STATUS_NOTIFY_ENUM_DIR && smb_notify_enum_dir_delay > 0) delay(MSEC_TO_TICK(smb_notify_enum_dir_delay)); return (status); } /* * smb_notify_act2() * * Prepare to wait for events after act1 found that none were pending. * Assume the wait may be for a very long time. (hours, days...) * Special return STATUS_PENDING means the SR will later be * scheduled again on a new worker thread, and this thread * MUST NOT touch it any longer (return SDRC_SR_KEPT). * * See overall design notes, top of file. */ uint32_t smb_notify_act2(smb_request_t *sr) { smb_ofile_t *of; smb_notify_t *nc; uint32_t status; /* * Sanity checks. */ if ((of = sr->fid_ofile) == NULL) return (NT_STATUS_INVALID_HANDLE); nc = &of->f_notify; /* * Prepare for a potentially long wait for events. * Normally transition from ACTIVE to WAITING_FCN1. */ mutex_enter(&sr->sr_mutex); switch (sr->sr_state) { case SMB_REQ_STATE_ACTIVE: /* * This sr has no worker thread until smb_notify_act3 * or smb_notify_cancel (later, via taskq_dispatch). */ sr->sr_state = SMB_REQ_STATE_WAITING_FCN1; sr->cancel_method = smb_notify_cancel; sr->sr_worker = NULL; status = NT_STATUS_PENDING; break; case SMB_REQ_STATE_CANCELLED: status = NT_STATUS_CANCELLED; break; default: status = NT_STATUS_INTERNAL_ERROR; break; } mutex_exit(&sr->sr_mutex); /* * Arrange to get smb_notify_wakeup() calls, * and check for any notify change events that * may have arrived before we entered f_mutex * * Note that smb_notify_cancel may run after we drop * the sr_mutex, so sr_state may change to cancelled. * In that case, the smb_notify_wakeup does nothing. * Note that smb_notify_wakeup is exempt from the * "MUST NOT touch" (the SR) rule described above. */ if (status == NT_STATUS_PENDING) { mutex_enter(&of->f_mutex); list_insert_tail(&nc->nc_waiters, sr); if (nc->nc_events != 0) { smb_notify_wakeup(sr); } mutex_exit(&of->f_mutex); } /* Note: Never NT_STATUS_NOTIFY_ENUM_DIR here. */ ASSERT(status != NT_STATUS_NOTIFY_ENUM_DIR); return (status); } /* * smb_notify_act3() * * This runs via the 2nd taskq_dispatch call, after we've either * seen a change notify event, or the request has been cancelled. * Complete it here. This returns to SMB1 or SMB2 code to send * the response and free the request. * * See overall design notes, top of file. */ uint32_t smb_notify_act3(smb_request_t *sr) { smb_ofile_t *of; smb_notify_t *nc; uint32_t status; of = sr->fid_ofile; ASSERT(of != NULL); nc = &of->f_notify; mutex_enter(&sr->sr_mutex); ASSERT3P(sr->sr_worker, ==, NULL); sr->sr_worker = curthread; switch_state: switch (sr->sr_state) { case SMB_REQ_STATE_WAITING_FCN2: /* * Got smb_notify_wakeup. */ sr->sr_state = SMB_REQ_STATE_ACTIVE; status = 0; break; case SMB_REQ_STATE_CANCEL_PENDING: /* cancel_method running. wait. */ cv_wait(&sr->sr_st_cv, &sr->sr_mutex); goto switch_state; case SMB_REQ_STATE_CANCELLED: /* * Got smb_notify_cancel */ status = NT_STATUS_CANCELLED; break; default: status = NT_STATUS_INTERNAL_ERROR; break; } sr->cancel_method = NULL; mutex_exit(&sr->sr_mutex); /* * The actual SMB notify work. */ mutex_enter(&of->f_mutex); list_remove(&nc->nc_waiters, sr); if (status == 0) status = smb_notify_get_events(sr); mutex_exit(&of->f_mutex); /* * See: About NT_STATUS_NOTIFY_ENUM_DIR (above) */ if (status == NT_STATUS_NOTIFY_ENUM_DIR && smb_notify_enum_dir_delay > 0) delay(MSEC_TO_TICK(smb_notify_enum_dir_delay)); return (status); } static uint32_t smb_notify_get_events(smb_request_t *sr) { smb_ofile_t *of; smb_notify_t *nc; uint32_t status; int len; of = sr->fid_ofile; ASSERT(of != NULL); ASSERT(MUTEX_HELD(&of->f_mutex)); nc = &of->f_notify; DTRACE_PROBE2(notify__get__events, smb_request_t *, sr, uint32_t, nc->nc_events); /* * Special events which override other events */ if (nc->nc_events & FILE_NOTIFY_CHANGE_EV_CLOSED) { status = NT_STATUS_NOTIFY_CLEANUP; goto out; } if (nc->nc_events & FILE_NOTIFY_CHANGE_EV_DELETE) { status = NT_STATUS_DELETE_PENDING; goto out; } if (nc->nc_events & FILE_NOTIFY_CHANGE_EV_SUBDIR) { status = NT_STATUS_NOTIFY_ENUM_DIR; goto out; } if (nc->nc_events & FILE_NOTIFY_CHANGE_EV_OVERFLOW) { status = NT_STATUS_NOTIFY_ENUM_DIR; goto out; } /* * Normal events (FILE_NOTIFY_VALID_MASK) * * At this point there should be some, or else * some sort of bug woke us up for nothing. */ if ((nc->nc_events & FILE_NOTIFY_VALID_MASK) == 0) { status = NT_STATUS_INTERNAL_ERROR; goto out; } /* * Many Windows clients call change notify with a * zero-length buffer, expecting all events to be * reported as _ENUM_DIR. Testing max_bytes here * because ROOM_FOR check below says "yes" if both * max_bytes and the amount we ask for are zero. */ if (nc->nc_buffer.max_bytes <= 0) { status = NT_STATUS_NOTIFY_ENUM_DIR; goto out; } /* * Client gave us a non-zero output buffer, and * there was no overflow event (checked above) * so there should be some event data. */ if ((len = nc->nc_buffer.chain_offset) <= 0) { status = NT_STATUS_INTERNAL_ERROR; goto out; } /* * If the current SR has a smaller output buffer * then what was setup by some previous notify, * we could have more data than will fit. */ if (!MBC_ROOM_FOR(&sr->raw_data, len)) { /* Would overflow caller's buffer. */ status = NT_STATUS_NOTIFY_ENUM_DIR; goto out; } /* * Copy the event data to sr->raw_data. In the copy, * zap the NextEntryOffset in the last entry, and * trim any extra bytes at the tail. */ (void) smb_mbc_copy(&sr->raw_data, &nc->nc_buffer, 0, len); (void) smb_mbc_poke(&sr->raw_data, nc->nc_last_off, "l", 0); smb_mbuf_trim(sr->raw_data.chain, len); status = 0; out: /* * If there are no other SRs waiting on this ofile, * mark all events consumed, except for those that * remain until the ofile is closed. That means * clear all bits EXCEPT: _EV_CLOSED, _EV_DELETE * * If there are other waiters (rare) all will get * the currently pending events, and then the * the last one out will clear the events. */ if (list_is_empty(&nc->nc_waiters)) { nc->nc_buffer.chain_offset = 0; nc->nc_events &= (FILE_NOTIFY_CHANGE_EV_CLOSED | FILE_NOTIFY_CHANGE_EV_DELETE); } return (status); } /* * Called by common code after a transition from * state WAITING_FCN1 to state CANCEL_PENDING. */ static void smb_notify_cancel(smb_request_t *sr) { ASSERT3U(sr->sr_state, ==, SMB_REQ_STATE_CANCEL_PENDING); smb_notify_dispatch2(sr); } /* * Called after ofile event delivery to take a waiting smb request * from state FCN1 to state FCN2. This may be called many times * (as events are delivered) but it must (exactly once) schedule * the taskq job to run smb_notify_act3(). Only the event that * takes us from state FCN1 to FCN2 schedules the taskq job. */ static void smb_notify_wakeup(smb_request_t *sr) { boolean_t do_disp = B_FALSE; SMB_REQ_VALID(sr); mutex_enter(&sr->sr_mutex); if (sr->sr_state == SMB_REQ_STATE_WAITING_FCN1) { sr->sr_state = SMB_REQ_STATE_WAITING_FCN2; do_disp = B_TRUE; } mutex_exit(&sr->sr_mutex); if (do_disp) { smb_notify_dispatch2(sr); } } /* * smb_notify_dispatch2() * Schedule a 2nd taskq call to finish up a change notify request; * (smb_notify_act3) either completing it or cancelling it. */ static void smb_notify_dispatch2(smb_request_t *sr) { void (*tq_func)(void *); taskqid_t tqid; /* * Both of these call smb_notify_act3(), returning * to version-specific code to send the response. */ if (sr->session->dialect >= SMB_VERS_2_BASE) tq_func = smb2_change_notify_finish; else tq_func = smb_nt_transact_notify_finish; tqid = taskq_dispatch(sr->sr_server->sv_notify_pool, tq_func, sr, TQ_SLEEP); VERIFY(tqid != TASKQID_INVALID); } /* * What completion filter (masks) apply to each of the * FILE_ACTION_... events. */ static const uint32_t smb_notify_action_mask[] = { 0, /* not used */ /* FILE_ACTION_ADDED */ FILE_NOTIFY_CHANGE_NAME | FILE_NOTIFY_CHANGE_LAST_WRITE, /* FILE_ACTION_REMOVED */ FILE_NOTIFY_CHANGE_NAME | FILE_NOTIFY_CHANGE_LAST_WRITE, /* FILE_ACTION_MODIFIED */ FILE_NOTIFY_CHANGE_ATTRIBUTES | FILE_NOTIFY_CHANGE_SIZE | FILE_NOTIFY_CHANGE_LAST_WRITE | FILE_NOTIFY_CHANGE_LAST_ACCESS | FILE_NOTIFY_CHANGE_CREATION | FILE_NOTIFY_CHANGE_EA | FILE_NOTIFY_CHANGE_SECURITY, /* FILE_ACTION_RENAMED_OLD_NAME */ FILE_NOTIFY_CHANGE_NAME | FILE_NOTIFY_CHANGE_LAST_WRITE, /* FILE_ACTION_RENAMED_NEW_NAME */ FILE_NOTIFY_CHANGE_NAME | FILE_NOTIFY_CHANGE_LAST_WRITE, /* FILE_ACTION_ADDED_STREAM */ FILE_NOTIFY_CHANGE_STREAM_NAME, /* FILE_ACTION_REMOVED_STREAM */ FILE_NOTIFY_CHANGE_STREAM_NAME, /* FILE_ACTION_MODIFIED_STREAM */ FILE_NOTIFY_CHANGE_STREAM_SIZE | FILE_NOTIFY_CHANGE_STREAM_WRITE, /* FILE_ACTION_SUBDIR_CHANGED */ FILE_NOTIFY_CHANGE_EV_SUBDIR, /* FILE_ACTION_DELETE_PENDING */ FILE_NOTIFY_CHANGE_EV_DELETE, /* FILE_ACTION_HANDLE_CLOSED */ FILE_NOTIFY_CHANGE_EV_CLOSED, }; static const int smb_notify_action_nelm = sizeof (smb_notify_action_mask) / sizeof (smb_notify_action_mask[0]); /* * smb_notify_ofile * * Post an event to the change notify buffer for this ofile, * subject to the mask that selects subscribed event types. * If an SR is waiting for events and we've delivered some, * wake the SR. */ void smb_notify_ofile(smb_ofile_t *of, uint_t action, const char *name) { smb_notify_t *nc; smb_request_t *sr; uint32_t filter, events; SMB_OFILE_VALID(of); mutex_enter(&of->f_mutex); nc = &of->f_notify; /* * Compute the filter & event bits for this action, * which determine whether we'll post the event. * Note: always sensitive to: delete, closed. */ filter = nc->nc_filter | FILE_NOTIFY_CHANGE_EV_DELETE | FILE_NOTIFY_CHANGE_EV_CLOSED; VERIFY(action < smb_notify_action_nelm); events = smb_notify_action_mask[action]; if ((filter & events) == 0) goto unlock_out; /* * OK, we're going to post this event. */ switch (action) { case FILE_ACTION_ADDED: case FILE_ACTION_REMOVED: case FILE_ACTION_MODIFIED: case FILE_ACTION_RENAMED_OLD_NAME: case FILE_ACTION_RENAMED_NEW_NAME: case FILE_ACTION_ADDED_STREAM: case FILE_ACTION_REMOVED_STREAM: case FILE_ACTION_MODIFIED_STREAM: /* * Append this event to the buffer. * Also keep track of events seen. */ smb_notify_encode_action(of, action, name); nc->nc_events |= events; break; case FILE_ACTION_SUBDIR_CHANGED: case FILE_ACTION_DELETE_PENDING: case FILE_ACTION_HANDLE_CLOSED: /* * These are "internal" events, and therefore * are not appended to the response buffer. * Just record the event flags and wakeup. */ nc->nc_events |= events; break; default: ASSERT(0); /* bogus action */ break; } sr = list_head(&nc->nc_waiters); while (sr != NULL) { smb_notify_wakeup(sr); sr = list_next(&nc->nc_waiters, sr); } unlock_out: mutex_exit(&of->f_mutex); } /* * Encode a FILE_NOTIFY_INFORMATION struct. */ static void smb_notify_encode_action(smb_ofile_t *of, uint32_t action, const char *fname) { smb_notify_t *nc = &of->f_notify; mbuf_chain_t *mbc; uint32_t namelen, totlen; ASSERT(nc != NULL); ASSERT(FILE_ACTION_ADDED <= action && action <= FILE_ACTION_MODIFIED_STREAM); ASSERT(fname != NULL); ASSERT(MUTEX_HELD(&of->f_mutex)); /* Once we've run out of room, stop trying to append. */ if ((nc->nc_events & FILE_NOTIFY_CHANGE_EV_OVERFLOW) != 0) return; if (fname == NULL) return; namelen = smb_wcequiv_strlen(fname); if (namelen == 0) return; /* * Layout is: 3 DWORDS, Unicode string, pad(4). */ mbc = &nc->nc_buffer; totlen = (12 + namelen + 3) & ~3; if (MBC_ROOM_FOR(mbc, totlen) == 0) { nc->nc_events |= FILE_NOTIFY_CHANGE_EV_OVERFLOW; return; } /* * Keep track of where this entry starts (nc_last_off) * because after we put all entries, we need to zap * the NextEntryOffset field in the last one. */ nc->nc_last_off = mbc->chain_offset; /* * Encode this entry, then 4-byte alignment padding. * * Note that smb_mbc_encodef with a "U" code puts a * Unicode string with a null termination. We don't * want a null, but do want alignment padding. We * get that by encoding with "U.." at the end of the * encoding string, which gets us two bytes for the * Unicode NULL, and two more zeros for the "..". * We then "back up" the chain_offset (finger) so it's * correctly 4-byte aligned. We will sometimes have * written a couple more bytes than needed, but we'll * just overwrite those with the next entry. At the * end, we trim the mbuf chain to the correct length. */ (void) smb_mbc_encodef(mbc, "lllU..", totlen, /* NextEntryOffset */ action, namelen, fname); mbc->chain_offset = nc->nc_last_off + totlen; }