/* * 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 2022 RackTop Systems, Inc. */ /* * SMB Node State Machine * ---------------------- * * * +----------- Creation/Allocation * | * | T0 * | * v * +----------------------------+ * | SMB_NODE_STATE_AVAILABLE | * +----------------------------+ * | * | T1 * | * v * +-----------------------------+ * | SMB_NODE_STATE_DESTROYING | * +-----------------------------+ * | * | * | T2 * | * +----------> Deletion/Free * * Transition T0 * * This transition occurs in smb_node_lookup(). If the node looked for is * not found in the has table a new node is created. The reference count is * initialized to 1 and the state initialized to SMB_NODE_STATE_AVAILABLE. * * Transition T1 * * This transition occurs in smb_node_release(). If the reference count * drops to zero the state is moved to SMB_NODE_STATE_DESTROYING and no more * reference count will be given out for that node. * * Transition T2 * * This transition occurs in smb_node_release(). The structure is deleted. * * Comments * -------- * * The reason the smb node has 2 states is the following synchronization * rule: * * There's a mutex embedded in the node used to protect its fields and * there's a lock embedded in the bucket of the hash table the node belongs * to. To increment or to decrement the reference count the mutex must be * entered. To insert the node into the bucket and to remove it from the * bucket the lock must be entered in RW_WRITER mode. When both (mutex and * lock) have to be entered, the lock has always to be entered first then * the mutex. This prevents a deadlock between smb_node_lookup() and * smb_node_release() from occurring. However, in smb_node_release() when the * reference count drops to zero and triggers the deletion of the node, the * mutex has to be released before entering the lock of the bucket (to * remove the node). This creates a window during which the node that is * about to be freed could be given out by smb_node_lookup(). To close that * window the node is moved to the state SMB_NODE_STATE_DESTROYING before * releasing the mutex. That way, even if smb_node_lookup() finds it, the * state will indicate that the node should be treated as non existent (of * course the state of the node should be tested/updated under the * protection of the mutex). */ #include #include #include #include #include #include #include #include #include uint32_t smb_is_executable(char *); static void smb_node_create_audit_buf(smb_node_t *, int); static void smb_node_destroy_audit_buf(smb_node_t *); static void smb_node_audit(smb_node_t *); static smb_node_t *smb_node_alloc(char *, vnode_t *, smb_llist_t *, uint32_t); static void smb_node_free(smb_node_t *); static int smb_node_constructor(void *, void *, int); static void smb_node_destructor(void *, void *); static smb_llist_t *smb_node_get_hash(fsid_t *, smb_attr_t *, uint32_t *); static void smb_node_init_reparse(smb_node_t *, smb_attr_t *); static void smb_node_init_system(smb_node_t *); #define VALIDATE_DIR_NODE(_dir_, _node_) \ ASSERT((_dir_)->n_magic == SMB_NODE_MAGIC); \ ASSERT(((_dir_)->vp->v_xattrdir) || ((_dir_)->vp->v_type == VDIR)); \ ASSERT((_dir_)->n_dnode != (_node_)); /* round sz to DEV_BSIZE block */ #define SMB_ALLOCSZ(sz) (((sz) + DEV_BSIZE-1) & ~(DEV_BSIZE-1)) static kmem_cache_t *smb_node_cache = NULL; static smb_llist_t smb_node_hash_table[SMBND_HASH_MASK+1]; static smb_node_t *smb_root_node; /* * smb_node_init * * Initialization of the SMB node layer. * * This function is not multi-thread safe. The caller must make sure only one * thread makes the call. */ void smb_node_init(void) { smb_attr_t attr; smb_llist_t *node_hdr; smb_node_t *node; uint32_t hashkey; int i; if (smb_node_cache != NULL) return; smb_node_cache = kmem_cache_create(SMBSRV_KSTAT_NODE_CACHE, sizeof (smb_node_t), 8, smb_node_constructor, smb_node_destructor, NULL, NULL, NULL, 0); for (i = 0; i <= SMBND_HASH_MASK; i++) { smb_llist_constructor(&smb_node_hash_table[i], sizeof (smb_node_t), offsetof(smb_node_t, n_lnd)); } /* * The node cache is shared by all zones, so the smb_root_node * must represent the real (global zone) rootdir. * Note intentional use of kcred here. */ attr.sa_mask = SMB_AT_ALL; VERIFY0(smb_vop_getattr(rootdir, NULL, &attr, 0, kcred)); node_hdr = smb_node_get_hash(&rootdir->v_vfsp->vfs_fsid, &attr, &hashkey); node = smb_node_alloc("/", rootdir, node_hdr, hashkey); smb_llist_enter(node_hdr, RW_WRITER); smb_llist_insert_head(node_hdr, node); smb_llist_exit(node_hdr); smb_root_node = node; /* smb_node_release in smb_node_fini */ } /* * smb_node_fini * * This function is not multi-thread safe. The caller must make sure only one * thread makes the call. */ void smb_node_fini(void) { int i; if (smb_root_node != NULL) { smb_node_release(smb_root_node); smb_root_node = NULL; } if (smb_node_cache == NULL) return; for (i = 0; i <= SMBND_HASH_MASK; i++) { smb_llist_t *bucket; smb_node_t *node; /* * The SMB node hash table should be empty at this point. * If the hash table is not empty, clean it up. * * The reason why SMB nodes might remain in this table is * generally forgotten references somewhere, perhaps on * open files, etc. Those are defects. */ bucket = &smb_node_hash_table[i]; node = smb_llist_head(bucket); while (node != NULL) { #ifdef DEBUG cmn_err(CE_NOTE, "leaked node: 0x%p %s", (void *)node, node->od_name); cmn_err(CE_NOTE, "...bucket: 0x%p", bucket); debug_enter("leaked_node"); #endif smb_llist_remove(bucket, node); node = smb_llist_head(bucket); } } for (i = 0; i <= SMBND_HASH_MASK; i++) { smb_llist_destructor(&smb_node_hash_table[i]); } kmem_cache_destroy(smb_node_cache); smb_node_cache = NULL; } /* * smb_node_lookup() * * NOTE: This routine should only be called by the file system interface layer, * and not by SMB. * * smb_node_lookup() is called upon successful lookup, mkdir, and create * (for both non-streams and streams). In each of these cases, a held vnode is * passed into this routine. If a new smb_node is created it will take its * own hold on the vnode. The caller's hold therefore still belongs to, and * should be released by, the caller. * * A reference is taken on the smb_node whether found in the hash table * or newly created. * * If an smb_node needs to be created, a reference is also taken on the * dnode (if passed in). * * See smb_node_release() for details on the release of these references. */ /*ARGSUSED*/ smb_node_t * smb_node_lookup( struct smb_request *sr, struct open_param *op, cred_t *cred, vnode_t *vp, char *od_name, smb_node_t *dnode, smb_node_t *unode) { smb_llist_t *node_hdr; smb_node_t *node; smb_attr_t attr; uint32_t hashkey = 0; fsid_t fsid; int error; krw_t lock_mode; vnode_t *unnamed_vp = NULL; /* * smb_vop_getattr() is called here instead of smb_fsop_getattr(), * because the node may not yet exist. We also do not want to call * it with the list lock held. */ if (unode) unnamed_vp = unode->vp; /* * This getattr is performed on behalf of the server * that's why kcred is used not the user's cred */ attr.sa_mask = SMB_AT_ALL; error = smb_vop_getattr(vp, unnamed_vp, &attr, 0, zone_kcred()); if (error) return (NULL); if (sr && sr->tid_tree) { /* * The fsid for a file is that of the tree, even * if the file resides in a different mountpoint * under the share. */ fsid = SMB_TREE_FSID(sr->tid_tree); } else { /* * This should be getting executed only for the * tree root smb_node. */ fsid = vp->v_vfsp->vfs_fsid; } node_hdr = smb_node_get_hash(&fsid, &attr, &hashkey); lock_mode = RW_READER; smb_llist_enter(node_hdr, lock_mode); for (;;) { node = list_head(&node_hdr->ll_list); while (node) { ASSERT(node->n_magic == SMB_NODE_MAGIC); ASSERT(node->n_hash_bucket == node_hdr); if ((node->n_hashkey == hashkey) && (node->vp == vp)) { mutex_enter(&node->n_mutex); DTRACE_PROBE1(smb_node_lookup_hit, smb_node_t *, node); switch (node->n_state) { case SMB_NODE_STATE_AVAILABLE: /* The node was found. */ node->n_refcnt++; if ((node->n_dnode == NULL) && (dnode != NULL) && (node != dnode) && (strcmp(od_name, "..") != 0) && (strcmp(od_name, ".") != 0)) { VALIDATE_DIR_NODE(dnode, node); node->n_dnode = dnode; smb_node_ref(dnode); } smb_node_audit(node); mutex_exit(&node->n_mutex); smb_llist_exit(node_hdr); return (node); case SMB_NODE_STATE_DESTROYING: /* * Although the node exists it is about * to be destroyed. We act as it hasn't * been found. */ mutex_exit(&node->n_mutex); break; default: /* * Although the node exists it is in an * unknown state. We act as it hasn't * been found. */ ASSERT(0); mutex_exit(&node->n_mutex); break; } } node = smb_llist_next(node_hdr, node); } if ((lock_mode == RW_READER) && smb_llist_upgrade(node_hdr)) { lock_mode = RW_WRITER; continue; } break; } node = smb_node_alloc(od_name, vp, node_hdr, hashkey); smb_node_init_reparse(node, &attr); if (op) node->flags |= smb_is_executable(op->fqi.fq_last_comp); if (dnode) { smb_node_ref(dnode); node->n_dnode = dnode; ASSERT(dnode->n_dnode != node); ASSERT((dnode->vp->v_xattrdir) || (dnode->vp->v_type == VDIR)); } if (unode) { smb_node_ref(unode); node->n_unode = unode; } smb_node_init_system(node); DTRACE_PROBE1(smb_node_lookup_miss, smb_node_t *, node); smb_node_audit(node); smb_llist_insert_head(node_hdr, node); smb_llist_exit(node_hdr); return (node); } /* * smb_stream_node_lookup() * * Note: stream_name (the name that will be stored in the "od_name" field * of a stream's smb_node) is the same as the on-disk name for the stream * except that it does not have SMB_STREAM_PREFIX prepended. */ smb_node_t * smb_stream_node_lookup(smb_request_t *sr, cred_t *cr, smb_node_t *fnode, vnode_t *xattrdirvp, vnode_t *vp, char *stream_name) { smb_node_t *xattrdir_node; smb_node_t *snode; xattrdir_node = smb_node_lookup(sr, NULL, cr, xattrdirvp, XATTR_DIR, fnode, NULL); if (xattrdir_node == NULL) return (NULL); snode = smb_node_lookup(sr, NULL, cr, vp, stream_name, xattrdir_node, fnode); (void) smb_node_release(xattrdir_node); return (snode); } /* * This function should be called whenever a reference is needed on an * smb_node pointer. The copy of an smb_node pointer from one non-local * data structure to another requires a reference to be taken on the smb_node * (unless the usage is localized). Each data structure deallocation routine * will call smb_node_release() on its smb_node pointers. * * In general, an smb_node pointer residing in a structure should never be * stale. A node pointer may be NULL, however, and care should be taken * prior to calling smb_node_ref(), which ASSERTs that the pointer is valid. * Care also needs to be taken with respect to racing deallocations of a * structure. */ void smb_node_ref(smb_node_t *node) { SMB_NODE_VALID(node); mutex_enter(&node->n_mutex); switch (node->n_state) { case SMB_NODE_STATE_AVAILABLE: node->n_refcnt++; ASSERT(node->n_refcnt); DTRACE_PROBE1(smb_node_ref_exit, smb_node_t *, node); smb_node_audit(node); break; default: SMB_PANIC(); } mutex_exit(&node->n_mutex); } /* * smb_node_lookup() takes a hold on an smb_node, whether found in the * hash table or newly created. This hold is expected to be released * in the following manner. * * smb_node_lookup() takes an address of an smb_node pointer. This should * be getting passed down via a lookup (whether path name or component), mkdir, * create. If the original smb_node pointer resides in a data structure, then * the deallocation routine for the data structure is responsible for calling * smb_node_release() on the smb_node pointer. Alternatively, * smb_node_release() can be called as soon as the smb_node pointer is no longer * needed. In this case, callers are responsible for setting an embedded * pointer to NULL if it is known that the last reference is being released. * * If the passed-in address of the smb_node pointer belongs to a local variable, * then the caller with the local variable should call smb_node_release() * directly. * * smb_node_release() itself will call smb_node_release() on a node's n_dnode, * as smb_node_lookup() takes a hold on dnode. */ void smb_node_release(smb_node_t *node) { SMB_NODE_VALID(node); mutex_enter(&node->n_mutex); ASSERT(node->n_refcnt); DTRACE_PROBE1(smb_node_release, smb_node_t *, node); if (--node->n_refcnt == 0) { switch (node->n_state) { case SMB_NODE_STATE_AVAILABLE: node->n_state = SMB_NODE_STATE_DESTROYING; /* * While we still hold n_mutex, * make sure FEM hooks are gone. */ if (node->n_fcn_count > 0) { DTRACE_PROBE1(fem__fcn__dangles, smb_node_t *, node); node->n_fcn_count = 0; (void) smb_fem_fcn_uninstall(node); } mutex_exit(&node->n_mutex); /* * Out of caution, make sure FEM hooks * used by oplocks are also gone. */ mutex_enter(&node->n_oplock.ol_mutex); ASSERT(node->n_oplock.ol_fem == B_FALSE); if (node->n_oplock.ol_fem == B_TRUE) { smb_fem_oplock_uninstall(node); node->n_oplock.ol_fem = B_FALSE; } mutex_exit(&node->n_oplock.ol_mutex); smb_llist_enter(node->n_hash_bucket, RW_WRITER); smb_llist_remove(node->n_hash_bucket, node); smb_llist_exit(node->n_hash_bucket); /* * Check if the file was deleted */ if (node->flags & NODE_FLAGS_DELETE_ON_CLOSE) { smb_node_delete_on_close(node); } if (node->n_dnode) { ASSERT(node->n_dnode->n_magic == SMB_NODE_MAGIC); smb_node_release(node->n_dnode); } if (node->n_unode) { ASSERT(node->n_unode->n_magic == SMB_NODE_MAGIC); smb_node_release(node->n_unode); } smb_node_free(node); return; default: SMB_PANIC(); } } smb_node_audit(node); mutex_exit(&node->n_mutex); } void smb_node_delete_on_close(smb_node_t *node) { smb_node_t *d_snode; int rc = 0; uint32_t flags = 0; d_snode = node->n_dnode; ASSERT((node->flags & NODE_FLAGS_DELETE_ON_CLOSE) != 0); node->flags &= ~NODE_FLAGS_DELETE_ON_CLOSE; node->flags |= NODE_FLAGS_DELETE_COMMITTED; flags = node->n_delete_on_close_flags; ASSERT(node->od_name != NULL); if (smb_node_is_dir(node)) rc = smb_fsop_rmdir(0, node->delete_on_close_cred, d_snode, node->od_name, flags); else rc = smb_fsop_remove(0, node->delete_on_close_cred, d_snode, node->od_name, flags); crfree(node->delete_on_close_cred); node->delete_on_close_cred = NULL; if (rc != 0) cmn_err(CE_WARN, "File %s could not be removed, rc=%d\n", node->od_name, rc); DTRACE_PROBE2(smb_node_delete_on_close, int, rc, smb_node_t *, node); } /* * smb_node_rename() * */ void smb_node_rename( smb_node_t *from_dnode, smb_node_t *ret_node, smb_node_t *to_dnode, char *to_name) { SMB_NODE_VALID(from_dnode); SMB_NODE_VALID(to_dnode); SMB_NODE_VALID(ret_node); smb_node_ref(to_dnode); mutex_enter(&ret_node->n_mutex); switch (ret_node->n_state) { case SMB_NODE_STATE_AVAILABLE: ret_node->n_dnode = to_dnode; mutex_exit(&ret_node->n_mutex); ASSERT(to_dnode->n_dnode != ret_node); ASSERT((to_dnode->vp->v_xattrdir) || (to_dnode->vp->v_type == VDIR)); smb_node_release(from_dnode); (void) strcpy(ret_node->od_name, to_name); /* * XXX Need to update attributes? */ break; default: SMB_PANIC(); } } /* * Find/create an SMB node for the root of this zone and store it * in *svrootp. Also create nodes leading to this directory. */ int smb_node_root_init(smb_server_t *sv, smb_node_t **svrootp) { zone_t *zone = curzone; int error; ASSERT(zone->zone_id == sv->sv_zid); if (smb_root_node == NULL) return (ENOENT); /* * We're getting smb nodes below the zone root here, * so need to use kcred, not zone_kcred(). */ error = smb_pathname(NULL, zone->zone_rootpath, 0, smb_root_node, smb_root_node, NULL, svrootp, kcred, NULL); return (error); } /* * Helper function for smb_node_set_delete_on_close(). Assumes node is a dir. * Return 0 if this is an empty dir. Otherwise return a NT_STATUS code. * Unfortunately, to find out if a directory is empty, we have to read it * and check for anything other than "." or ".." in the readdir buf. */ static uint32_t smb_rmdir_possible(smb_node_t *n) { ASSERT(n->vp->v_type == VDIR); char *buf; char *bufptr; struct dirent64 *dp; uint32_t status = NT_STATUS_SUCCESS; int bsize = SMB_ODIR_BUFSIZE; int eof = 0; buf = kmem_alloc(SMB_ODIR_BUFSIZE, KM_SLEEP); /* Flags zero: no edirent, no ABE wanted here */ if (smb_vop_readdir(n->vp, 0, buf, &bsize, &eof, 0, zone_kcred())) { status = NT_STATUS_INTERNAL_ERROR; goto out; } bufptr = buf; while (bsize > 0) { /* LINTED pointer alignment */ dp = (struct dirent64 *)bufptr; bufptr += dp->d_reclen; bsize -= dp->d_reclen; if (bsize < 0) { /* partial record */ status = NT_STATUS_DIRECTORY_NOT_EMPTY; break; } if (strcmp(dp->d_name, ".") != 0 && strcmp(dp->d_name, "..") != 0) { status = NT_STATUS_DIRECTORY_NOT_EMPTY; break; } } out: kmem_free(buf, SMB_ODIR_BUFSIZE); return (status); } /* * When DeleteOnClose is set on an smb_node, the common open code will * reject subsequent open requests for the file. Observation of Windows * 2000 indicates that subsequent opens should be allowed (assuming * there would be no sharing violation) until the file is closed using * the fid on which the DeleteOnClose was requested. * * If there are multiple opens with delete-on-close create options, * whichever the first file handle is closed will trigger the node to be * marked as delete-on-close. The credentials of that ofile will be used * as the delete-on-close credentials of the node. * * Note that "read-only" tests have already happened before this call. */ uint32_t smb_node_set_delete_on_close(smb_node_t *node, cred_t *cr, uint32_t flags) { uint32_t status; /* * If the directory is not empty we should fail setting del-on-close * with STATUS_DIRECTORY_NOT_EMPTY. see MS's * "File System Behavior Overview" doc section 4.3.2 */ if (smb_node_is_dir(node)) { status = smb_rmdir_possible(node); if (status != 0) { return (status); } } /* Dataset roots can't be deleted, so don't set DOC */ if ((node->flags & NODE_FLAGS_VFSROOT) != 0) { return (NT_STATUS_CANNOT_DELETE); } mutex_enter(&node->n_mutex); if (node->flags & NODE_FLAGS_DELETE_ON_CLOSE) { /* It was already marked. We're done. */ mutex_exit(&node->n_mutex); return (NT_STATUS_SUCCESS); } crhold(cr); node->delete_on_close_cred = cr; node->n_delete_on_close_flags = flags; node->flags |= NODE_FLAGS_DELETE_ON_CLOSE; mutex_exit(&node->n_mutex); /* * Tell any change notify calls to close their handles * and get out of the way. FILE_ACTION_DELETE_PENDING * is a special, internal-only action for this purpose. */ smb_node_notify_change(node, FILE_ACTION_DELETE_PENDING, NULL); return (NT_STATUS_SUCCESS); } void smb_node_reset_delete_on_close(smb_node_t *node) { mutex_enter(&node->n_mutex); if (node->flags & NODE_FLAGS_DELETE_ON_CLOSE) { node->flags &= ~NODE_FLAGS_DELETE_ON_CLOSE; crfree(node->delete_on_close_cred); node->delete_on_close_cred = NULL; node->n_delete_on_close_flags = 0; } mutex_exit(&node->n_mutex); } /* * smb_node_open_check * * check file sharing rules for current open request * against all existing opens for a file. * * Returns NT_STATUS_SHARING_VIOLATION if there is any * sharing conflict, otherwise returns NT_STATUS_SUCCESS. */ uint32_t smb_node_open_check(smb_node_t *node, uint32_t desired_access, uint32_t share_access) { smb_ofile_t *of; uint32_t status; SMB_NODE_VALID(node); smb_llist_enter(&node->n_ofile_list, RW_READER); of = smb_llist_head(&node->n_ofile_list); while (of) { status = smb_ofile_open_check(of, desired_access, share_access); switch (status) { case NT_STATUS_INVALID_HANDLE: case NT_STATUS_SUCCESS: of = smb_llist_next(&node->n_ofile_list, of); break; default: ASSERT(status == NT_STATUS_SHARING_VIOLATION); DTRACE_PROBE3(conflict3, smb_ofile_t *, of, uint32_t, desired_access, uint32_t, share_access); smb_llist_exit(&node->n_ofile_list); return (status); } } smb_llist_exit(&node->n_ofile_list); return (NT_STATUS_SUCCESS); } uint32_t smb_node_rename_check(smb_node_t *node) { smb_ofile_t *of; uint32_t status; SMB_NODE_VALID(node); /* * Intra-CIFS check */ smb_llist_enter(&node->n_ofile_list, RW_READER); of = smb_llist_head(&node->n_ofile_list); while (of) { status = smb_ofile_rename_check(of); switch (status) { case NT_STATUS_INVALID_HANDLE: case NT_STATUS_SUCCESS: of = smb_llist_next(&node->n_ofile_list, of); break; default: ASSERT(status == NT_STATUS_SHARING_VIOLATION); DTRACE_PROBE1(conflict1, smb_ofile_t *, of); smb_llist_exit(&node->n_ofile_list); return (status); } } smb_llist_exit(&node->n_ofile_list); return (NT_STATUS_SUCCESS); } uint32_t smb_node_delete_check(smb_node_t *node) { smb_ofile_t *of; uint32_t status; SMB_NODE_VALID(node); if (smb_node_is_dir(node)) return (NT_STATUS_SUCCESS); if (smb_node_is_reparse(node)) return (NT_STATUS_ACCESS_DENIED); /* * intra-CIFS check */ smb_llist_enter(&node->n_ofile_list, RW_READER); of = smb_llist_head(&node->n_ofile_list); while (of) { status = smb_ofile_delete_check(of); switch (status) { case NT_STATUS_INVALID_HANDLE: case NT_STATUS_SUCCESS: of = smb_llist_next(&node->n_ofile_list, of); break; default: ASSERT(status == NT_STATUS_SHARING_VIOLATION); DTRACE_PROBE1(conflict1, smb_ofile_t *, of); smb_llist_exit(&node->n_ofile_list); return (status); } } smb_llist_exit(&node->n_ofile_list); return (NT_STATUS_SUCCESS); } /* * smb_node_share_check * * Returns: TRUE - ofiles have non-zero share access * B_FALSE - ofile with share access NONE. */ boolean_t smb_node_share_check(smb_node_t *node) { smb_ofile_t *of; boolean_t status = B_TRUE; SMB_NODE_VALID(node); smb_llist_enter(&node->n_ofile_list, RW_READER); of = smb_llist_head(&node->n_ofile_list); if (of) status = smb_ofile_share_check(of); smb_llist_exit(&node->n_ofile_list); return (status); } /* * SMB Change Notification */ void smb_node_fcn_subscribe(smb_node_t *node) { mutex_enter(&node->n_mutex); if (node->n_fcn_count == 0) (void) smb_fem_fcn_install(node); node->n_fcn_count++; mutex_exit(&node->n_mutex); } void smb_node_fcn_unsubscribe(smb_node_t *node) { mutex_enter(&node->n_mutex); node->n_fcn_count--; if (node->n_fcn_count == 0) { VERIFY0(smb_fem_fcn_uninstall(node)); } mutex_exit(&node->n_mutex); } void smb_node_notify_change(smb_node_t *node, uint_t action, const char *name) { smb_ofile_t *of; SMB_NODE_VALID(node); smb_llist_enter(&node->n_ofile_list, RW_READER); of = smb_llist_head(&node->n_ofile_list); while (of) { /* * We'd rather deliver events only to ofiles that have * subscribed. There's no explicit synchronization with * where this flag is set, but other actions cause this * value to reach visibility soon enough for events to * start arriving by the time we need them to start. * Once nc_subscribed is set, it stays set for the * life of the ofile. */ if (of->f_notify.nc_subscribed) smb_notify_ofile(of, action, name); of = smb_llist_next(&node->n_ofile_list, of); } smb_llist_exit(&node->n_ofile_list); /* * After changes that add or remove a name, * we know the directory attributes changed, * and we can tell the immediate parent. */ switch (action) { case FILE_ACTION_ADDED: case FILE_ACTION_REMOVED: case FILE_ACTION_RENAMED_NEW_NAME: /* * Note: FILE_ACTION_RENAMED_OLD_NAME is intentionally * omitted, because it's always followed by another * event with FILE_ACTION_RENAMED_NEW_NAME posted to * the same directory, and we only need/want one. */ if (node->n_dnode != NULL) { smb_node_notify_change(node->n_dnode, FILE_ACTION_MODIFIED, node->od_name); } break; } /* * If we wanted to support recursive notify events * (where a notify call on some directory receives * events from all objects below that directory), * we might deliver _SUBDIR_CHANGED to all our * parents, grandparents etc, here. However, we * don't currently subscribe to changes on all the * child (and grandchild) objects that would be * needed to make that work. It's prohibitively * expensive to do that, and support for recursive * notify is optional anyway, so don't bother. */ } /* * Change notify modified differs for stream vs regular file. * Changes to a stream give a notification on the "unnamed" node, * which is the parent object of the stream. */ void smb_node_notify_modified(smb_node_t *node) { smb_node_t *u_node; u_node = SMB_IS_STREAM(node); if (u_node != NULL) { /* This is a named stream */ if (u_node->n_dnode != NULL) { smb_node_notify_change(u_node->n_dnode, FILE_ACTION_MODIFIED_STREAM, u_node->od_name); } } else { /* regular file or directory */ if (node->n_dnode != NULL) { smb_node_notify_change(node->n_dnode, FILE_ACTION_MODIFIED, node->od_name); } } } /* * smb_node_start_crit() * * Enter critical region for share reservations. * See comments above smb_fsop_shrlock(). */ void smb_node_start_crit(smb_node_t *node, krw_t mode) { rw_enter(&node->n_lock, mode); nbl_start_crit(node->vp, mode); } /* * smb_node_end_crit() * * Exit critical region for share reservations. */ void smb_node_end_crit(smb_node_t *node) { nbl_end_crit(node->vp); rw_exit(&node->n_lock); } int smb_node_in_crit(smb_node_t *node) { return (nbl_in_crit(node->vp) && RW_LOCK_HELD(&node->n_lock)); } void smb_node_rdlock(smb_node_t *node) { rw_enter(&node->n_lock, RW_READER); } void smb_node_wrlock(smb_node_t *node) { rw_enter(&node->n_lock, RW_WRITER); } void smb_node_unlock(smb_node_t *node) { rw_exit(&node->n_lock); } void smb_node_add_ofile(smb_node_t *node, smb_ofile_t *of) { SMB_NODE_VALID(node); smb_llist_enter(&node->n_ofile_list, RW_WRITER); smb_llist_insert_tail(&node->n_ofile_list, of); smb_llist_exit(&node->n_ofile_list); } void smb_node_rem_ofile(smb_node_t *node, smb_ofile_t *of) { SMB_NODE_VALID(node); smb_llist_enter(&node->n_ofile_list, RW_WRITER); smb_llist_remove(&node->n_ofile_list, of); smb_llist_exit(&node->n_ofile_list); } /* * smb_node_inc_open_ofiles */ void smb_node_inc_open_ofiles(smb_node_t *node) { SMB_NODE_VALID(node); atomic_inc_32(&node->n_open_count); } /* * smb_node_dec_open_ofiles * returns new value */ uint32_t smb_node_dec_open_ofiles(smb_node_t *node) { SMB_NODE_VALID(node); return (atomic_dec_32_nv(&node->n_open_count)); } /* * smb_node_inc_opening_count */ void smb_node_inc_opening_count(smb_node_t *node) { SMB_NODE_VALID(node); atomic_inc_32(&node->n_opening_count); } /* * smb_node_dec_opening_count */ void smb_node_dec_opening_count(smb_node_t *node) { SMB_NODE_VALID(node); atomic_dec_32(&node->n_opening_count); } /* * smb_node_getmntpath */ int smb_node_getmntpath(smb_node_t *node, char *buf, uint32_t buflen) { vnode_t *vp, *root_vp; vfs_t *vfsp; int err; ASSERT(node); ASSERT(node->vp); ASSERT(node->vp->v_vfsp); vp = node->vp; vfsp = vp->v_vfsp; if (VFS_ROOT(vfsp, &root_vp)) return (ENOENT); VN_HOLD(vp); /* NULL is passed in as we want to start at "/" */ err = vnodetopath(NULL, root_vp, buf, buflen, zone_kcred()); VN_RELE(vp); VN_RELE(root_vp); return (err); } /* * smb_node_getshrpath * * Determine the absolute pathname of 'node' within the share (tree). * For example if the node represents file "test1.txt" in directory * "dir1" the pathname would be: \dir1\test1.txt */ int smb_node_getshrpath(smb_node_t *node, smb_tree_t *tree, char *buf, uint32_t buflen) { int rc; ASSERT(node); ASSERT(tree); ASSERT(tree->t_snode); rc = smb_node_getpath(node, tree->t_snode->vp, buf, buflen); (void) strsubst(buf, '/', '\\'); return (rc); } /* * smb_node_getpath * * Determine the absolute pathname of 'node' from 'rootvp'. * * Using vnodetopath is only reliable for directory nodes (due to * its reliance on the DNLC for non-directory nodes). Thus, if node * represents a file, construct the pathname for the parent dnode * and append filename. * If node represents a named stream, construct the pathname for the * associated unnamed stream and append the stream name. * * The pathname returned in buf will be '/' separated. */ int smb_node_getpath(smb_node_t *node, vnode_t *rootvp, char *buf, uint32_t buflen) { int rc; vnode_t *vp; smb_node_t *unode, *dnode; cred_t *kcr = zone_kcred(); unode = (SMB_IS_STREAM(node)) ? node->n_unode : node; dnode = (smb_node_is_dir(unode)) ? unode : unode->n_dnode; /* find path to directory node */ vp = dnode->vp; VN_HOLD(vp); if (rootvp) { VN_HOLD(rootvp); rc = vnodetopath(rootvp, vp, buf, buflen, kcr); VN_RELE(rootvp); } else { rc = vnodetopath(NULL, vp, buf, buflen, kcr); } VN_RELE(vp); if (rc != 0) return (rc); /* append filename if necessary */ if (!smb_node_is_dir(unode)) { if (buf[strlen(buf) - 1] != '/') (void) strlcat(buf, "/", buflen); (void) strlcat(buf, unode->od_name, buflen); } /* append named stream name if necessary */ if (SMB_IS_STREAM(node)) (void) strlcat(buf, node->od_name, buflen); return (rc); } /* * smb_node_alloc */ static smb_node_t * smb_node_alloc( char *od_name, vnode_t *vp, smb_llist_t *bucket, uint32_t hashkey) { smb_node_t *node; vnode_t *root_vp; node = kmem_cache_alloc(smb_node_cache, KM_SLEEP); if (node->n_audit_buf != NULL) node->n_audit_buf->anb_index = 0; node->flags = 0; VN_HOLD(vp); node->vp = vp; node->n_refcnt = 1; node->n_hash_bucket = bucket; node->n_hashkey = hashkey; node->n_open_count = 0; node->n_allocsz = 0; node->n_dnode = NULL; node->n_unode = NULL; node->delete_on_close_cred = NULL; node->n_delete_on_close_flags = 0; node->n_oplock.ol_fem = B_FALSE; (void) strlcpy(node->od_name, od_name, sizeof (node->od_name)); if (strcmp(od_name, XATTR_DIR) == 0) node->flags |= NODE_XATTR_DIR; if (VFS_ROOT(vp->v_vfsp, &root_vp) == 0) { if (vp == root_vp) node->flags |= NODE_FLAGS_VFSROOT; VN_RELE(root_vp); } node->n_state = SMB_NODE_STATE_AVAILABLE; node->n_magic = SMB_NODE_MAGIC; return (node); } /* * smb_node_free */ static void smb_node_free(smb_node_t *node) { SMB_NODE_VALID(node); node->n_magic = 0; VERIFY(!list_link_active(&node->n_lnd)); VERIFY(node->n_lock_list.ll_count == 0); VERIFY(node->n_wlock_list.ll_count == 0); VERIFY(node->n_ofile_list.ll_count == 0); VERIFY(node->n_oplock.ol_fem == B_FALSE); VERIFY(MUTEX_NOT_HELD(&node->n_mutex)); VERIFY(!RW_LOCK_HELD(&node->n_lock)); VN_RELE(node->vp); kmem_cache_free(smb_node_cache, node); } /* * smb_node_constructor */ static int smb_node_constructor(void *buf, void *un, int kmflags) { _NOTE(ARGUNUSED(kmflags, un)) smb_node_t *node = (smb_node_t *)buf; bzero(node, sizeof (smb_node_t)); smb_llist_constructor(&node->n_ofile_list, sizeof (smb_ofile_t), offsetof(smb_ofile_t, f_node_lnd)); smb_llist_constructor(&node->n_lock_list, sizeof (smb_lock_t), offsetof(smb_lock_t, l_lnd)); smb_llist_constructor(&node->n_wlock_list, sizeof (smb_lock_t), offsetof(smb_lock_t, l_lnd)); mutex_init(&node->n_oplock.ol_mutex, NULL, MUTEX_DEFAULT, NULL); cv_init(&node->n_oplock.WaitingOpenCV, NULL, CV_DEFAULT, NULL); rw_init(&node->n_lock, NULL, RW_DEFAULT, NULL); mutex_init(&node->n_mutex, NULL, MUTEX_DEFAULT, NULL); smb_node_create_audit_buf(node, kmflags); return (0); } /* * smb_node_destructor */ static void smb_node_destructor(void *buf, void *un) { _NOTE(ARGUNUSED(un)) smb_node_t *node = (smb_node_t *)buf; smb_node_destroy_audit_buf(node); mutex_destroy(&node->n_mutex); rw_destroy(&node->n_lock); cv_destroy(&node->n_oplock.WaitingOpenCV); mutex_destroy(&node->n_oplock.ol_mutex); smb_llist_destructor(&node->n_lock_list); smb_llist_destructor(&node->n_wlock_list); smb_llist_destructor(&node->n_ofile_list); } /* * smb_node_create_audit_buf */ static void smb_node_create_audit_buf(smb_node_t *node, int kmflags) { smb_audit_buf_node_t *abn; if (smb_audit_flags & SMB_AUDIT_NODE) { abn = kmem_zalloc(sizeof (smb_audit_buf_node_t), kmflags); abn->anb_max_index = SMB_AUDIT_BUF_MAX_REC - 1; node->n_audit_buf = abn; } } /* * smb_node_destroy_audit_buf */ static void smb_node_destroy_audit_buf(smb_node_t *node) { if (node->n_audit_buf != NULL) { kmem_free(node->n_audit_buf, sizeof (smb_audit_buf_node_t)); node->n_audit_buf = NULL; } } /* * smb_node_audit * * This function saves the calling stack in the audit buffer of the node passed * in. */ static void smb_node_audit(smb_node_t *node) { #ifdef _KERNEL smb_audit_buf_node_t *abn; smb_audit_record_node_t *anr; if (node->n_audit_buf) { abn = node->n_audit_buf; anr = abn->anb_records; anr += abn->anb_index; abn->anb_index++; abn->anb_index &= abn->anb_max_index; anr->anr_refcnt = node->n_refcnt; anr->anr_depth = getpcstack(anr->anr_stack, SMB_AUDIT_STACK_DEPTH); } #else /* _KERNEL */ _NOTE(ARGUNUSED(node)) #endif /* _KERNEL */ } static smb_llist_t * smb_node_get_hash(fsid_t *fsid, smb_attr_t *attr, uint32_t *phashkey) { uint32_t hashkey; hashkey = fsid->val[0] + attr->sa_vattr.va_nodeid; hashkey += (hashkey >> 24) + (hashkey >> 16) + (hashkey >> 8); *phashkey = hashkey; return (&smb_node_hash_table[(hashkey & SMBND_HASH_MASK)]); } boolean_t smb_node_is_file(smb_node_t *node) { SMB_NODE_VALID(node); return (node->vp->v_type == VREG); } boolean_t smb_node_is_dir(smb_node_t *node) { SMB_NODE_VALID(node); return ((node->vp->v_type == VDIR) || (node->flags & NODE_FLAGS_DFSLINK)); } boolean_t smb_node_is_symlink(smb_node_t *node) { SMB_NODE_VALID(node); return ((node->vp->v_type == VLNK) && ((node->flags & NODE_FLAGS_REPARSE) == 0)); } boolean_t smb_node_is_dfslink(smb_node_t *node) { SMB_NODE_VALID(node); return ((node->vp->v_type == VLNK) && (node->flags & NODE_FLAGS_DFSLINK)); } boolean_t smb_node_is_reparse(smb_node_t *node) { SMB_NODE_VALID(node); return ((node->vp->v_type == VLNK) && (node->flags & NODE_FLAGS_REPARSE)); } boolean_t smb_node_is_vfsroot(smb_node_t *node) { SMB_NODE_VALID(node); return ((node->flags & NODE_FLAGS_VFSROOT) == NODE_FLAGS_VFSROOT); } boolean_t smb_node_is_system(smb_node_t *node) { SMB_NODE_VALID(node); return ((node->flags & NODE_FLAGS_SYSTEM) == NODE_FLAGS_SYSTEM); } /* * smb_node_file_is_readonly * * Checks if the file (which node represents) is marked readonly * in the filesystem. Note that there may be handles open with * modify rights, and those continue to allow access even after * the DOS read-only flag has been set in the file system. */ boolean_t smb_node_file_is_readonly(smb_node_t *node) { smb_attr_t attr; if (node == NULL) return (B_FALSE); /* pipes */ bzero(&attr, sizeof (smb_attr_t)); attr.sa_mask = SMB_AT_DOSATTR; (void) smb_fsop_getattr(NULL, zone_kcred(), node, &attr); return ((attr.sa_dosattr & FILE_ATTRIBUTE_READONLY) != 0); } /* * smb_node_setattr * * The sr may be NULL, for example when closing an ofile. * The ofile may be NULL, for example when a client request * specifies the file by pathname. * * Returns: errno * * Timestamps * * Windows and Unix have different models for timestamp updates. * [MS-FSA 2.1.5.14 Server Requests Setting of File Information] * * An open "handle" in Windows can control whether and when * any timestamp updates happen for that handle. For example, * timestamps set via some handle are no longer updated by I/O * operations on that handle. In Unix we don't really have any * way to avoid the timestamp updates that the file system does. * Therefore, we need to make some compromises, and simulate the * more important parts of the Windows file system semantics. * * For example, when an SMB client sets file times, set those * times in the file system (so the change will be visible to * other clients, at least until they change again) but we also * make those times "sticky" in our open handle, and reapply * those times when the handle is closed. That reapply on close * simulates the Windows behavior where the timestamp updates * would be discontinued after they were set. These "sticky" * attributes are returned in any query on the handle where * they are stored. * * Other than the above, the file system layer takes care of the * normal time stamp updates, such as updating the mtime after a * write, and ctime after an attribute change. * * File allocation size is also simulated, and not persistent. * When the file allocation size is set it is first rounded up * to block size. If the file size is smaller than the allocation * size the file is truncated by setting the filesize to allocsz. */ int smb_node_setattr(smb_request_t *sr, smb_node_t *node, cred_t *cr, smb_ofile_t *of, smb_attr_t *attr) { int rc; uint_t times_mask; smb_attr_t tmp_attr; SMB_NODE_VALID(node); /* set attributes specified in attr */ if (attr->sa_mask == 0) return (0); /* nothing to do (caller bug?) */ /* * Allocation size and EOF position interact. * We don't persistently store the allocation size * but make it look like we do while there are opens. * Note: We update the caller's attr in the cases * where they're setting only one of allocsz|size. */ switch (attr->sa_mask & (SMB_AT_ALLOCSZ | SMB_AT_SIZE)) { case SMB_AT_ALLOCSZ: /* * Setting the allocation size but not EOF position. * Get the current EOF in tmp_attr and (if necessary) * truncate to the (rounded up) allocation size. * Using kcred here because if we don't have access, * we want to fail at setattr below and not here. */ bzero(&tmp_attr, sizeof (smb_attr_t)); tmp_attr.sa_mask = SMB_AT_SIZE; rc = smb_fsop_getattr(NULL, zone_kcred(), node, &tmp_attr); if (rc != 0) return (rc); attr->sa_allocsz = SMB_ALLOCSZ(attr->sa_allocsz); if (tmp_attr.sa_vattr.va_size > attr->sa_allocsz) { /* truncate the file to allocsz */ attr->sa_vattr.va_size = attr->sa_allocsz; attr->sa_mask |= SMB_AT_SIZE; } break; case SMB_AT_SIZE: /* * Setting the EOF position but not allocation size. * If the new EOF position would be greater than * the allocation size, increase the latter. */ if (node->n_allocsz < attr->sa_vattr.va_size) { attr->sa_mask |= SMB_AT_ALLOCSZ; attr->sa_allocsz = SMB_ALLOCSZ(attr->sa_vattr.va_size); } break; case SMB_AT_ALLOCSZ | SMB_AT_SIZE: /* * Setting both. Increase alloc size if needed. */ if (attr->sa_allocsz < attr->sa_vattr.va_size) attr->sa_allocsz = SMB_ALLOCSZ(attr->sa_vattr.va_size); break; default: break; } /* * When operating on an open file, some settable attributes * become "sticky" in the open file object until close. * (see above re. timestamps) */ times_mask = attr->sa_mask & SMB_AT_TIMES; if (of != NULL && times_mask != 0) { smb_attr_t *pa; SMB_OFILE_VALID(of); mutex_enter(&of->f_mutex); pa = &of->f_pending_attr; pa->sa_mask |= times_mask; if (times_mask & SMB_AT_ATIME) pa->sa_vattr.va_atime = attr->sa_vattr.va_atime; if (times_mask & SMB_AT_MTIME) pa->sa_vattr.va_mtime = attr->sa_vattr.va_mtime; if (times_mask & SMB_AT_CTIME) pa->sa_vattr.va_ctime = attr->sa_vattr.va_ctime; if (times_mask & SMB_AT_CRTIME) pa->sa_crtime = attr->sa_crtime; mutex_exit(&of->f_mutex); /* * The f_pending_attr times are reapplied in * smb_ofile_close(). */ /* * If this change is coming directly from a client * (sr != NULL) and it's a persistent handle, save * the "sticky times" in the handle. */ if (sr != NULL && of->dh_persist) { smb2_dh_update_times(sr, of, attr); } } if ((attr->sa_mask & SMB_AT_ALLOCSZ) != 0) { mutex_enter(&node->n_mutex); /* * Simulate n_allocsz persistence only while * there are opens. See smb_node_getattr */ if (node->n_open_count != 0) node->n_allocsz = attr->sa_allocsz; mutex_exit(&node->n_mutex); } rc = smb_fsop_setattr(sr, cr, node, attr); /* * Only generate change notify events for client requests. * Internal operations use sr=NULL */ if (rc == 0 && sr != NULL) smb_node_notify_modified(node); return (rc); } /* * smb_node_getattr * * Get attributes from the file system and apply any smb-specific * overrides for size, dos attributes and timestamps * * Returns: errno */ int smb_node_getattr(smb_request_t *sr, smb_node_t *node, cred_t *cr, smb_ofile_t *of, smb_attr_t *attr) { int rc; uint_t want_mask, pend_mask; boolean_t isdir; SMB_NODE_VALID(node); /* Deal with some interdependencies */ if (attr->sa_mask & SMB_AT_ALLOCSZ) attr->sa_mask |= SMB_AT_SIZE; if (attr->sa_mask & SMB_AT_DOSATTR) attr->sa_mask |= SMB_AT_TYPE; rc = smb_fsop_getattr(sr, cr, node, attr); if (rc != 0) return (rc); isdir = smb_node_is_dir(node); mutex_enter(&node->n_mutex); if (attr->sa_mask & SMB_AT_DOSATTR) { if (attr->sa_dosattr == 0) { attr->sa_dosattr = (isdir) ? FILE_ATTRIBUTE_DIRECTORY: FILE_ATTRIBUTE_NORMAL; } } /* * Also fix-up sa_allocsz, which is not persistent. * When there are no open files, allocsz is faked. * While there are open files, we pretend we have a * persistent allocation size in n_allocsz, and * keep that up-to-date here, increasing it when * we see the file size grow past it. */ if (attr->sa_mask & SMB_AT_ALLOCSZ) { if (isdir) { attr->sa_allocsz = 0; } else if (node->n_open_count == 0) { attr->sa_allocsz = SMB_ALLOCSZ(attr->sa_vattr.va_size); } else { if (node->n_allocsz < attr->sa_vattr.va_size) node->n_allocsz = SMB_ALLOCSZ(attr->sa_vattr.va_size); attr->sa_allocsz = node->n_allocsz; } } mutex_exit(&node->n_mutex); if (isdir) { attr->sa_vattr.va_size = 0; attr->sa_vattr.va_nlink = 1; } /* * getattr with an ofile gets any "pending" times that * might have been previously set via this ofile. * This is what makes these times "sticky". */ want_mask = attr->sa_mask & SMB_AT_TIMES; if (of != NULL && want_mask != 0) { smb_attr_t *pa; SMB_OFILE_VALID(of); mutex_enter(&of->f_mutex); pa = &of->f_pending_attr; pend_mask = pa->sa_mask; if (want_mask & pend_mask & SMB_AT_ATIME) attr->sa_vattr.va_atime = pa->sa_vattr.va_atime; if (want_mask & pend_mask & SMB_AT_MTIME) attr->sa_vattr.va_mtime = pa->sa_vattr.va_mtime; if (want_mask & pend_mask & SMB_AT_CTIME) attr->sa_vattr.va_ctime = pa->sa_vattr.va_ctime; if (want_mask & pend_mask & SMB_AT_CRTIME) attr->sa_crtime = pa->sa_crtime; mutex_exit(&of->f_mutex); } return (0); } #ifndef _KERNEL extern int reparse_vnode_parse(vnode_t *vp, nvlist_t *nvl); #endif /* _KERNEL */ /* * Check to see if the node represents a reparse point. * If yes, whether the reparse point contains a DFS link. */ static void smb_node_init_reparse(smb_node_t *node, smb_attr_t *attr) { nvlist_t *nvl; nvpair_t *rec; char *rec_type; if ((attr->sa_dosattr & FILE_ATTRIBUTE_REPARSE_POINT) == 0) return; if ((nvl = reparse_init()) == NULL) return; if (reparse_vnode_parse(node->vp, nvl) != 0) { reparse_free(nvl); return; } node->flags |= NODE_FLAGS_REPARSE; rec = nvlist_next_nvpair(nvl, NULL); while (rec != NULL) { rec_type = nvpair_name(rec); if ((rec_type != NULL) && (strcasecmp(rec_type, DFS_REPARSE_SVCTYPE) == 0)) { node->flags |= NODE_FLAGS_DFSLINK; break; } rec = nvlist_next_nvpair(nvl, rec); } reparse_free(nvl); } /* * smb_node_init_system * * If the node represents a special system file set NODE_FLAG_SYSTEM. * System files: * - any node whose parent dnode has NODE_FLAG_SYSTEM set * - any node whose associated unnamed stream node (unode) has * NODE_FLAG_SYSTEM set * - .$EXTEND at root of share (quota management) */ static void smb_node_init_system(smb_node_t *node) { smb_node_t *dnode = node->n_dnode; smb_node_t *unode = node->n_unode; if ((dnode) && (dnode->flags & NODE_FLAGS_SYSTEM)) { node->flags |= NODE_FLAGS_SYSTEM; return; } if ((unode) && (unode->flags & NODE_FLAGS_SYSTEM)) { node->flags |= NODE_FLAGS_SYSTEM; return; } if ((dnode) && (smb_node_is_vfsroot(node->n_dnode) && (strcasecmp(node->od_name, ".$EXTEND") == 0))) { node->flags |= NODE_FLAGS_SYSTEM; } }