/* Unix SMB/CIFS implementation. filename handling routines Copyright (C) Andrew Tridgell 1992-1998 Copyright (C) Jeremy Allison 1999-2007 Copyright (C) Ying Chen 2000 Copyright (C) Volker Lendecke 2007 This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ /* * New hash table stat cache code added by Ying Chen. */ #include "includes.h" #include "system/filesys.h" #include "fake_file.h" #include "smbd/smbd.h" #include "smbd/globals.h" #include "lib/util/memcache.h" uint32_t ucf_flags_from_smb_request(struct smb_request *req) { uint32_t ucf_flags = 0; if (req != NULL) { if (req->posix_pathnames) { ucf_flags |= UCF_POSIX_PATHNAMES; } if (req->flags2 & FLAGS2_DFS_PATHNAMES) { ucf_flags |= UCF_DFS_PATHNAME; } if (req->flags2 & FLAGS2_REPARSE_PATH) { ucf_flags |= UCF_GMT_PATHNAME; } } return ucf_flags; } uint32_t filename_create_ucf_flags(struct smb_request *req, uint32_t create_disposition) { uint32_t ucf_flags = 0; ucf_flags |= ucf_flags_from_smb_request(req); switch (create_disposition) { case FILE_OPEN: case FILE_OVERWRITE: break; case FILE_SUPERSEDE: case FILE_CREATE: case FILE_OPEN_IF: case FILE_OVERWRITE_IF: ucf_flags |= UCF_PREP_CREATEFILE; break; } return ucf_flags; } /**************************************************************************** Mangle the 2nd name and check if it is then equal to the first name. ****************************************************************************/ static bool mangled_equal(const char *name1, const char *name2, const struct share_params *p) { char mname[13]; if (!name_to_8_3(name2, mname, False, p)) { return False; } return strequal(name1, mname); } static bool find_snapshot_token( const char *filename, const char **_start, const char **_next_component, NTTIME *twrp) { const char *start = NULL; const char *end = NULL; struct tm tm; time_t t; start = strstr_m(filename, "@GMT-"); if (start == NULL) { return false; } if ((start > filename) && (start[-1] != '/')) { /* the GMT-token does not start a path-component */ return false; } end = strptime(start, GMT_FORMAT, &tm); if (end == NULL) { /* Not a valid timestring. */ return false; } if ((end[0] != '\0') && (end[0] != '/')) { /* * It is not a complete path component, i.e. the path * component continues after the gmt-token. */ return false; } tm.tm_isdst = -1; t = timegm(&tm); unix_to_nt_time(twrp, t); DBG_DEBUG("Extracted @GMT-Timestamp %s\n", nt_time_string(talloc_tos(), *twrp)); *_start = start; if (end[0] == '/') { end += 1; } *_next_component = end; return true; } bool extract_snapshot_token(char *fname, NTTIME *twrp) { const char *start = NULL; const char *next = NULL; size_t remaining; bool found; found = find_snapshot_token(fname, &start, &next, twrp); if (!found) { return false; } remaining = strlen(next); memmove(discard_const_p(char, start), next, remaining+1); return true; } /* * Strip a valid @GMT-token from any incoming filename path, * adding any NTTIME encoded in the pathname into the * twrp field of the passed in smb_fname. * * Valid @GMT-tokens look like @GMT-YYYY-MM-DD-HH-MM-SS * at the *start* of a pathname component. * * If twrp is passed in then smb_fname->twrp is set to that * value, and the @GMT-token part of the filename is removed * and does not change the stored smb_fname->twrp. * */ NTSTATUS canonicalize_snapshot_path(struct smb_filename *smb_fname, uint32_t ucf_flags, NTTIME twrp) { bool found; if (twrp != 0) { smb_fname->twrp = twrp; } if (!(ucf_flags & UCF_GMT_PATHNAME)) { return NT_STATUS_OK; } found = extract_snapshot_token(smb_fname->base_name, &twrp); if (!found) { return NT_STATUS_OK; } if (smb_fname->twrp == 0) { smb_fname->twrp = twrp; } return NT_STATUS_OK; } static bool strnorm(char *s, int case_default) { if (case_default == CASE_UPPER) return strupper_m(s); else return strlower_m(s); } /* * Utility function to normalize case on an incoming client filename * if required on this connection struct. * Performs an in-place case conversion guaranteed to stay the same size. */ static NTSTATUS normalize_filename_case(connection_struct *conn, char *filename, uint32_t ucf_flags) { bool ok; if (ucf_flags & UCF_POSIX_PATHNAMES) { /* * POSIX never normalizes filename case. */ return NT_STATUS_OK; } if (!conn->case_sensitive) { return NT_STATUS_OK; } if (conn->case_preserve) { return NT_STATUS_OK; } if (conn->short_case_preserve) { return NT_STATUS_OK; } ok = strnorm(filename, lp_default_case(SNUM(conn))); if (!ok) { return NT_STATUS_INVALID_PARAMETER; } return NT_STATUS_OK; } /**************************************************************************** Check if two filenames are equal. This needs to be careful about whether we are case sensitive. ****************************************************************************/ static bool fname_equal(const char *name1, const char *name2, bool case_sensitive) { /* Normal filename handling */ if (case_sensitive) { return(strcmp(name1,name2) == 0); } return(strequal(name1,name2)); } static bool sname_equal(const char *name1, const char *name2, bool case_sensitive) { bool match; const char *s1 = NULL; const char *s2 = NULL; size_t n1; size_t n2; const char *e1 = NULL; const char *e2 = NULL; char *c1 = NULL; char *c2 = NULL; match = fname_equal(name1, name2, case_sensitive); if (match) { return true; } if (name1[0] != ':') { return false; } if (name2[0] != ':') { return false; } s1 = &name1[1]; e1 = strchr(s1, ':'); if (e1 == NULL) { n1 = strlen(s1); } else { n1 = PTR_DIFF(e1, s1); } s2 = &name2[1]; e2 = strchr(s2, ':'); if (e2 == NULL) { n2 = strlen(s2); } else { n2 = PTR_DIFF(e2, s2); } /* Normal filename handling */ if (case_sensitive) { return (strncmp(s1, s2, n1) == 0); } /* * We can't use strnequal() here * as it takes the number of codepoints * and not the number of bytes. * * So we make a copy before calling * strequal(). * * Note that we TALLOC_FREE() in reverse order * in order to avoid memory fragmentation. */ c1 = talloc_strndup(talloc_tos(), s1, n1); c2 = talloc_strndup(talloc_tos(), s2, n2); if (c1 == NULL || c2 == NULL) { TALLOC_FREE(c2); TALLOC_FREE(c1); return (strncmp(s1, s2, n1) == 0); } match = strequal(c1, c2); TALLOC_FREE(c2); TALLOC_FREE(c1); return match; } /**************************************************************************** Scan a directory to find a filename, matching without case sensitivity. If the name looks like a mangled name then try via the mangling functions ****************************************************************************/ NTSTATUS get_real_filename_full_scan_at(struct files_struct *dirfsp, const char *name, bool mangled, TALLOC_CTX *mem_ctx, char **found_name) { struct connection_struct *conn = dirfsp->conn; struct smb_Dir *cur_dir = NULL; const char *dname = NULL; char *talloced = NULL; char *unmangled_name = NULL; long curpos; NTSTATUS status; /* If we have a case-sensitive filesystem, it doesn't do us any * good to search for a name. If a case variation of the name was * there, then the original stat(2) would have found it. */ if (!mangled && !(conn->fs_capabilities & FILE_CASE_SENSITIVE_SEARCH)) { return NT_STATUS_OBJECT_NAME_NOT_FOUND; } /* * The incoming name can be mangled, and if we de-mangle it * here it will not compare correctly against the filename (name2) * read from the directory and then mangled by the name_to_8_3() * call. We need to mangle both names or neither. * (JRA). * * Fix for bug found by Dina Fine. If in case sensitive mode then * the mangle cache is no good (3 letter extension could be wrong * case - so don't demangle in this case - leave as mangled and * allow the mangling of the directory entry read (which is done * case insensitively) to match instead. This will lead to more * false positive matches but we fail completely without it. JRA. */ if (mangled && !conn->case_sensitive) { mangled = !mangle_lookup_name_from_8_3(talloc_tos(), name, &unmangled_name, conn->params); if (!mangled) { /* Name is now unmangled. */ name = unmangled_name; } } /* open the directory */ status = OpenDir_from_pathref(talloc_tos(), dirfsp, NULL, 0, &cur_dir); if (!NT_STATUS_IS_OK(status)) { DBG_NOTICE("scan dir didn't open dir [%s]: %s\n", fsp_str_dbg(dirfsp), nt_errstr(status)); TALLOC_FREE(unmangled_name); return status; } /* now scan for matching names */ curpos = 0; while ((dname = ReadDirName(cur_dir, &curpos, NULL, &talloced))) { /* Is it dot or dot dot. */ if (ISDOT(dname) || ISDOTDOT(dname)) { TALLOC_FREE(talloced); continue; } /* * At this point dname is the unmangled name. * name is either mangled or not, depending on the state * of the "mangled" variable. JRA. */ /* * Check mangled name against mangled name, or unmangled name * against unmangled name. */ if ((mangled && mangled_equal(name,dname,conn->params)) || fname_equal(name, dname, conn->case_sensitive)) { /* we've found the file, change it's name and return */ *found_name = talloc_strdup(mem_ctx, dname); TALLOC_FREE(unmangled_name); TALLOC_FREE(cur_dir); if (!*found_name) { TALLOC_FREE(talloced); return NT_STATUS_NO_MEMORY; } TALLOC_FREE(talloced); return NT_STATUS_OK; } TALLOC_FREE(talloced); } TALLOC_FREE(unmangled_name); TALLOC_FREE(cur_dir); return NT_STATUS_OBJECT_NAME_NOT_FOUND; } /**************************************************************************** Wrapper around the vfs get_real_filename and the full directory scan fallback. ****************************************************************************/ NTSTATUS get_real_filename_at(struct files_struct *dirfsp, const char *name, TALLOC_CTX *mem_ctx, char **found_name) { struct connection_struct *conn = dirfsp->conn; NTSTATUS status; bool mangled; mangled = mangle_is_mangled(name, conn->params); if (mangled) { status = get_real_filename_full_scan_at( dirfsp, name, mangled, mem_ctx, found_name); return status; } /* Try the vfs first to take advantage of case-insensitive stat. */ status = SMB_VFS_GET_REAL_FILENAME_AT( dirfsp->conn, dirfsp, name, mem_ctx, found_name); /* * If the case-insensitive stat was successful, or returned an error * other than EOPNOTSUPP then there is no need to fall back on the * full directory scan. */ if (NT_STATUS_IS_OK(status) || !NT_STATUS_EQUAL(status, NT_STATUS_NOT_SUPPORTED)) { return status; } status = get_real_filename_full_scan_at( dirfsp, name, mangled, mem_ctx, found_name); return status; } /* * Create the memcache-key for GETREALFILENAME_CACHE: This supplements * the stat cache for the last component to be looked up. Cache * contents is the correctly capitalized translation of the parameter * "name" as it exists on disk. This is indexed by inode of the dirfsp * and name, and contrary to stat_cahce_lookup() it does not * vfs_stat() the last component. This will be taken care of by an * attempt to do a openat_pathref_fsp(). */ static bool get_real_filename_cache_key( TALLOC_CTX *mem_ctx, struct files_struct *dirfsp, const char *name, DATA_BLOB *_key) { struct file_id fid = vfs_file_id_from_sbuf( dirfsp->conn, &dirfsp->fsp_name->st); char *upper = NULL; uint8_t *key = NULL; size_t namelen, keylen; upper = talloc_strdup_upper(mem_ctx, name); if (upper == NULL) { return false; } namelen = talloc_get_size(upper); keylen = namelen + sizeof(fid); if (keylen < sizeof(fid)) { TALLOC_FREE(upper); return false; } key = talloc_size(mem_ctx, keylen); if (key == NULL) { TALLOC_FREE(upper); return false; } memcpy(key, &fid, sizeof(fid)); memcpy(key + sizeof(fid), upper, namelen); TALLOC_FREE(upper); *_key = (DATA_BLOB) { .data = key, .length = keylen, }; return true; } /* * Lightweight function to just get last component * for rename / enumerate directory calls. */ char *get_original_lcomp(TALLOC_CTX *ctx, connection_struct *conn, const char *filename_in, uint32_t ucf_flags) { char *last_slash = NULL; char *orig_lcomp; NTSTATUS status; last_slash = strrchr(filename_in, '/'); if (last_slash != NULL) { orig_lcomp = talloc_strdup(ctx, last_slash+1); } else { orig_lcomp = talloc_strdup(ctx, filename_in); } if (orig_lcomp == NULL) { return NULL; } status = normalize_filename_case(conn, orig_lcomp, ucf_flags); if (!NT_STATUS_IS_OK(status)) { TALLOC_FREE(orig_lcomp); return NULL; } return orig_lcomp; } /* * Deal with the SMB1 semantics of sending a pathname with a * wildcard as the terminal component for a SMB1search or * trans2 findfirst. */ NTSTATUS filename_convert_smb1_search_path(TALLOC_CTX *ctx, connection_struct *conn, char *name_in, uint32_t ucf_flags, struct files_struct **_dirfsp, struct smb_filename **_smb_fname_out, char **_mask_out) { NTSTATUS status; char *p = NULL; char *mask = NULL; struct smb_filename *smb_fname = NULL; NTTIME twrp = 0; *_smb_fname_out = NULL; *_dirfsp = NULL; *_mask_out = NULL; DBG_DEBUG("name_in: %s\n", name_in); if (ucf_flags & UCF_GMT_PATHNAME) { extract_snapshot_token(name_in, &twrp); ucf_flags &= ~UCF_GMT_PATHNAME; } if (ucf_flags & UCF_DFS_PATHNAME) { /* * We've been given a raw DFS pathname. */ char *pathname = NULL; DBG_DEBUG("Before dfs_filename_convert name_in: %s\n", name_in); status = dfs_filename_convert(ctx, conn, ucf_flags, name_in, &pathname); if (!NT_STATUS_IS_OK(status)) { DBG_DEBUG("dfs_filename_convert " "failed for name %s with %s\n", name_in, nt_errstr(status)); return status; } ucf_flags &= ~UCF_DFS_PATHNAME; name_in = pathname; DBG_DEBUG("After dfs_filename_convert name_in: %s\n", name_in); } /* Get the original lcomp. */ mask = get_original_lcomp(ctx, conn, name_in, ucf_flags); if (mask == NULL) { return NT_STATUS_NO_MEMORY; } if (mask[0] == '\0') { /* Windows and OS/2 systems treat search on the root as * */ TALLOC_FREE(mask); mask = talloc_strdup(ctx, "*"); if (mask == NULL) { return NT_STATUS_NO_MEMORY; } } DBG_DEBUG("mask = %s\n", mask); /* * Remove the terminal component so * filename_convert_dirfsp never sees the mask. */ p = strrchr_m(name_in,'/'); if (p == NULL) { /* filename_convert_dirfsp handles a '\0' name. */ name_in[0] = '\0'; } else { *p = '\0'; } DBG_DEBUG("For filename_convert_dirfsp: name_in = %s\n", name_in); /* Convert the parent directory path. */ status = filename_convert_dirfsp(ctx, conn, name_in, ucf_flags, twrp, _dirfsp, &smb_fname); if (!NT_STATUS_IS_OK(status)) { DBG_DEBUG("filename_convert error for %s: %s\n", name_in, nt_errstr(status)); } *_smb_fname_out = talloc_move(ctx, &smb_fname); *_mask_out = talloc_move(ctx, &mask); return status; } /* * Get the correct capitalized stream name hanging off * base_fsp. Equivalent of get_real_filename(), but for streams. */ static NTSTATUS get_real_stream_name( TALLOC_CTX *mem_ctx, struct files_struct *base_fsp, const char *stream_name, char **_found) { unsigned int i, num_streams = 0; struct stream_struct *streams = NULL; NTSTATUS status; status = vfs_fstreaminfo( base_fsp, talloc_tos(), &num_streams, &streams); if (!NT_STATUS_IS_OK(status)) { return status; } for (i=0; icase_preserve || (mangle_is_8_3(name, false, conn->params) && !conn->short_case_preserve)) { char *normalized = talloc_strdup(mem_ctx, name); if (normalized == NULL) { return NT_STATUS_NO_MEMORY; } strnorm(normalized, lp_default_case(SNUM(conn))); name = normalized; } if (mangle_is_mangled(name, conn->params)) { bool found; char *unmangled = NULL; found = mangle_lookup_name_from_8_3( mem_ctx, name, &unmangled, conn->params); if (found) { name = unmangled; } } if (name != name_in) { *_normalized = name; } return NT_STATUS_OK; } /* * Open smb_fname_rel->fsp as a pathref fsp with a case insensitive * fallback using GETREALFILENAME_CACHE and get_real_filename_at() if * the first attempt based on the filename sent by the client gives * ENOENT. */ static NTSTATUS openat_pathref_fsp_case_insensitive( struct files_struct *dirfsp, struct smb_filename *smb_fname_rel, uint32_t ucf_flags) { const bool posix = (ucf_flags & UCF_POSIX_PATHNAMES); DATA_BLOB cache_key = { .data = NULL, }; char *found_name = NULL; NTSTATUS status; bool ok; SET_STAT_INVALID(smb_fname_rel->st); /* Check veto files - only looks at last component. */ if (IS_VETO_PATH(dirfsp->conn, smb_fname_rel->base_name)) { DBG_DEBUG("veto files rejecting last component %s\n", smb_fname_str_dbg(smb_fname_rel)); return NT_STATUS_NETWORK_OPEN_RESTRICTION; } status = openat_pathref_fsp(dirfsp, smb_fname_rel); if (NT_STATUS_IS_OK(status)) { return NT_STATUS_OK; } if (VALID_STAT(smb_fname_rel->st)) { /* * We got an error although the object existed. Might * be a symlink we don't want. */ return status; } if (!NT_STATUS_EQUAL(status, NT_STATUS_OBJECT_NAME_NOT_FOUND)) { /* * Only retry on ENOENT */ return status; } if (posix || dirfsp->conn->case_sensitive) { /* * Only return case insensitive if required */ return status; } if (lp_stat_cache()) { char *base_name = smb_fname_rel->base_name; DATA_BLOB value = { .data = NULL }; ok = get_real_filename_cache_key( talloc_tos(), dirfsp, base_name, &cache_key); if (!ok) { /* * probably ENOMEM, just bail */ return status; } DO_PROFILE_INC(statcache_lookups); ok = memcache_lookup( NULL, GETREALFILENAME_CACHE, cache_key, &value); if (!ok) { DO_PROFILE_INC(statcache_misses); goto lookup; } DO_PROFILE_INC(statcache_hits); TALLOC_FREE(smb_fname_rel->base_name); smb_fname_rel->base_name = talloc_memdup( smb_fname_rel, value.data, value.length); if (smb_fname_rel->base_name == NULL) { TALLOC_FREE(cache_key.data); return NT_STATUS_NO_MEMORY; } if (IS_VETO_PATH(dirfsp->conn, smb_fname_rel->base_name)) { DBG_DEBUG("veto files rejecting last component %s\n", smb_fname_str_dbg(smb_fname_rel)); TALLOC_FREE(cache_key.data); return NT_STATUS_NETWORK_OPEN_RESTRICTION; } status = openat_pathref_fsp(dirfsp, smb_fname_rel); if (NT_STATUS_IS_OK(status)) { TALLOC_FREE(cache_key.data); return NT_STATUS_OK; } memcache_delete(NULL, GETREALFILENAME_CACHE, cache_key); } lookup: status = get_real_filename_at( dirfsp, smb_fname_rel->base_name, smb_fname_rel, &found_name); if (NT_STATUS_EQUAL(status, NT_STATUS_ACCESS_DENIED) && (ucf_flags & UCF_PREP_CREATEFILE)) { /* * dropbox */ status = NT_STATUS_OBJECT_NAME_NOT_FOUND; } if (NT_STATUS_IS_OK(status)) { TALLOC_FREE(smb_fname_rel->base_name); smb_fname_rel->base_name = found_name; if (IS_VETO_PATH(dirfsp->conn, smb_fname_rel->base_name)) { DBG_DEBUG("veto files rejecting last component %s\n", smb_fname_str_dbg(smb_fname_rel)); return NT_STATUS_NETWORK_OPEN_RESTRICTION; } status = openat_pathref_fsp(dirfsp, smb_fname_rel); } if (NT_STATUS_IS_OK(status) && (cache_key.data != NULL)) { const char *slash = strchr_m(smb_fname_rel->base_name, '/'); if (slash == NULL) { DATA_BLOB value = { .data = (uint8_t *)smb_fname_rel->base_name, .length = strlen(smb_fname_rel->base_name) + 1, }; memcache_add( NULL, GETREALFILENAME_CACHE, cache_key, value); } } TALLOC_FREE(cache_key.data); return status; } /* * Split up name_in as sent by the client into a directory pathref fsp * and a relative smb_filename. */ static const char *previous_slash(const char *name_in, const char *slash) { const char *prev = name_in; while (true) { const char *next = strchr_m(prev, '/'); SMB_ASSERT(next != NULL); /* we have at least one slash */ if (next == slash) { break; } prev = next+1; }; if (prev == name_in) { /* no previous slash */ return NULL; } return prev; } static char *symlink_target_path( TALLOC_CTX *mem_ctx, const char *name_in, const char *substitute, size_t unparsed) { size_t name_in_len = strlen(name_in); const char *p_unparsed = NULL; const char *parent = NULL; char *ret; SMB_ASSERT(unparsed <= name_in_len); p_unparsed = name_in + (name_in_len - unparsed); if (substitute[0] == '/') { ret = talloc_asprintf(mem_ctx, "%s%s", substitute, p_unparsed); return ret; } if (unparsed == 0) { parent = strrchr_m(name_in, '/'); } else { parent = previous_slash(name_in, p_unparsed); } if (parent == NULL) { /* no previous slash */ parent = name_in; } ret = talloc_asprintf( mem_ctx, "%.*s%s%s", (int)(parent - name_in), name_in, substitute, p_unparsed); return ret; } /* * Split up name_in as sent by the client into a directory pathref fsp * and a relative smb_filename. */ static NTSTATUS filename_convert_dirfsp_nosymlink( TALLOC_CTX *mem_ctx, connection_struct *conn, const char *name_in, uint32_t ucf_flags, NTTIME twrp, struct files_struct **_dirfsp, struct smb_filename **_smb_fname, char **_substitute, size_t *_unparsed) { struct smb_filename *smb_dirname = NULL; struct smb_filename *smb_fname_rel = NULL; struct smb_filename *smb_fname = NULL; const bool posix = (ucf_flags & UCF_POSIX_PATHNAMES); char *dirname = NULL; const char *fname_rel = NULL; const char *streamname = NULL; char *saved_streamname = NULL; struct files_struct *base_fsp = NULL; bool ok; NTSTATUS status = NT_STATUS_UNSUCCESSFUL; if (ucf_flags & UCF_DFS_PATHNAME) { /* * We've been given a raw DFS pathname. */ char *pathname = NULL; DBG_DEBUG("Before dfs_filename_convert name_in: %s\n", name_in); status = dfs_filename_convert(mem_ctx, conn, ucf_flags, name_in, &pathname); if (!NT_STATUS_IS_OK(status)) { DBG_DEBUG("dfs_filename_convert " "failed for name %s with %s\n", name_in, nt_errstr(status)); return status; } ucf_flags &= ~UCF_DFS_PATHNAME; name_in = pathname; DBG_DEBUG("After dfs_filename_convert name_in: %s\n", name_in); } if (is_fake_file_path(name_in)) { smb_fname = synthetic_smb_fname_split(mem_ctx, name_in, posix); if (smb_fname == NULL) { return NT_STATUS_NO_MEMORY; } smb_fname->st = (SMB_STRUCT_STAT) { .st_ex_nlink = 1 }; smb_fname->st.st_ex_btime = (struct timespec){0, SAMBA_UTIME_OMIT}; smb_fname->st.st_ex_atime = (struct timespec){0, SAMBA_UTIME_OMIT}; smb_fname->st.st_ex_mtime = (struct timespec){0, SAMBA_UTIME_OMIT}; smb_fname->st.st_ex_ctime = (struct timespec){0, SAMBA_UTIME_OMIT}; *_dirfsp = conn->cwd_fsp; *_smb_fname = smb_fname; return NT_STATUS_OK; } /* * Catch an invalid path of "." before we * call filename_split_lcomp(). We need to * do this as filename_split_lcomp() will * use "." for the missing relative component * when an empty name_in path is sent by * the client. */ if (ISDOT(name_in)) { status = NT_STATUS_OBJECT_NAME_INVALID; goto fail; } ok = filename_split_lcomp( talloc_tos(), name_in, posix, &dirname, &fname_rel, &streamname); if (!ok) { status = NT_STATUS_NO_MEMORY; goto fail; } if ((streamname != NULL) && ((conn->fs_capabilities & FILE_NAMED_STREAMS) == 0)) { status = NT_STATUS_OBJECT_NAME_INVALID; goto fail; } if (!posix) { bool name_has_wild = ms_has_wild(dirname); name_has_wild |= ms_has_wild(fname_rel); if (name_has_wild) { status = NT_STATUS_OBJECT_NAME_INVALID; goto fail; } } if (dirname[0] == '\0') { status = synthetic_pathref( mem_ctx, conn->cwd_fsp, ".", NULL, NULL, 0, posix ? SMB_FILENAME_POSIX_PATH : 0, &smb_dirname); } else { char *substitute = NULL; size_t unparsed = 0; status = normalize_filename_case(conn, dirname, ucf_flags); if (!NT_STATUS_IS_OK(status)) { DBG_ERR("normalize_filename_case %s failed: %s\n", dirname, nt_errstr(status)); goto fail; } status = openat_pathref_dirfsp_nosymlink( mem_ctx, conn, dirname, 0, &smb_dirname, &unparsed, &substitute); if (NT_STATUS_EQUAL(status, NT_STATUS_STOPPED_ON_SYMLINK)) { size_t name_in_len = strlen(name_in); size_t dirname_len = strlen(dirname); SMB_ASSERT(name_in_len >= dirname_len); *_substitute = substitute; *_unparsed = unparsed + (name_in_len - dirname_len); goto fail; } } if (!NT_STATUS_IS_OK(status)) { DBG_DEBUG("opening directory %s failed: %s\n", dirname, nt_errstr(status)); TALLOC_FREE(dirname); if (NT_STATUS_EQUAL(status, NT_STATUS_PATH_NOT_COVERED)) { /* MS-DFS error must propagate back out. */ goto fail; } if (!NT_STATUS_EQUAL(status, NT_STATUS_ACCESS_DENIED)) { /* * Except ACCESS_DENIED, everything else leads * to PATH_NOT_FOUND. */ status = NT_STATUS_OBJECT_PATH_NOT_FOUND; } goto fail; } if (!VALID_STAT_OF_DIR(smb_dirname->st)) { status = NT_STATUS_OBJECT_PATH_NOT_FOUND; goto fail; } /* * Only look at bad last component values * once we know we have a valid directory. That * way we won't confuse error messages from * opening the directory path with error * messages from a bad last component. */ /* Relative filename can't be empty */ if (fname_rel[0] == '\0') { status = NT_STATUS_OBJECT_NAME_INVALID; goto fail; } /* Relative filename can't be ".." */ if (ISDOTDOT(fname_rel)) { status = NT_STATUS_OBJECT_NAME_INVALID; goto fail; } /* Relative name can only be dot if directory is empty. */ if (ISDOT(fname_rel) && dirname[0] != '\0') { status = NT_STATUS_OBJECT_NAME_INVALID; goto fail; } TALLOC_FREE(dirname); smb_fname_rel = synthetic_smb_fname( mem_ctx, fname_rel, streamname, NULL, twrp, posix ? SMB_FILENAME_POSIX_PATH : 0); if (smb_fname_rel == NULL) { status = NT_STATUS_NO_MEMORY; goto fail; } if ((conn->fs_capabilities & FILE_NAMED_STREAMS) && is_named_stream(smb_fname_rel)) { /* * Find the base_fsp first without the stream. */ saved_streamname = smb_fname_rel->stream_name; smb_fname_rel->stream_name = NULL; } status = normalize_filename_case( conn, smb_fname_rel->base_name, ucf_flags); if (!NT_STATUS_IS_OK(status)) { DBG_ERR("normalize_filename_case %s failed: %s\n", smb_fname_rel->base_name, nt_errstr(status)); goto fail; } status = openat_pathref_fsp_case_insensitive( smb_dirname->fsp, smb_fname_rel, ucf_flags); if (NT_STATUS_EQUAL(status, NT_STATUS_OBJECT_NAME_NOT_FOUND)) { char *normalized = NULL; if (VALID_STAT(smb_fname_rel->st)) { /* * If we're on an MSDFS share, see if this is * an MSDFS link. */ if (lp_host_msdfs() && lp_msdfs_root(SNUM(conn)) && S_ISLNK(smb_fname_rel->st.st_ex_mode) && is_msdfs_link(smb_dirname->fsp, smb_fname_rel)) { status = NT_STATUS_PATH_NOT_COVERED; goto fail; } #if defined(WITH_SMB1SERVER) /* * In SMB1 posix mode, if this is a symlink, * allow access to the name with a NULL smb_fname->fsp. */ if (!conn->sconn->using_smb2 && posix && S_ISLNK(smb_fname_rel->st.st_ex_mode)) { SMB_ASSERT(smb_fname_rel->fsp == NULL); SMB_ASSERT(streamname == NULL); smb_fname = full_path_from_dirfsp_atname( mem_ctx, smb_dirname->fsp, smb_fname_rel); if (smb_fname == NULL) { status = NT_STATUS_NO_MEMORY; goto fail; } goto done; } #endif /* * NT_STATUS_OBJECT_NAME_NOT_FOUND is * misleading: The object exists but might be * a symlink pointing outside the share. */ goto fail; } /* * Creating a new file */ status = filename_convert_normalize_new( smb_fname_rel, conn, smb_fname_rel->base_name, &normalized); if (!NT_STATUS_IS_OK(status)) { DBG_DEBUG("filename_convert_normalize_new failed: " "%s\n", nt_errstr(status)); goto fail; } if (normalized != NULL) { smb_fname_rel->base_name = normalized; } smb_fname_rel->stream_name = saved_streamname; smb_fname = full_path_from_dirfsp_atname( mem_ctx, smb_dirname->fsp, smb_fname_rel); if (smb_fname == NULL) { status = NT_STATUS_NO_MEMORY; goto fail; } goto done; } if (NT_STATUS_EQUAL(status, NT_STATUS_NETWORK_OPEN_RESTRICTION)) { /* A vetoed file, pretend it's not there */ status = NT_STATUS_OBJECT_NAME_NOT_FOUND; } if (!NT_STATUS_IS_OK(status)) { goto fail; } if (saved_streamname == NULL) { /* smb_fname must be allocated off mem_ctx. */ smb_fname = cp_smb_filename(mem_ctx, smb_fname_rel->fsp->fsp_name); if (smb_fname == NULL) { goto fail; } status = move_smb_fname_fsp_link(smb_fname, smb_fname_rel); if (!NT_STATUS_IS_OK(status)) { goto fail; } goto done; } base_fsp = smb_fname_rel->fsp; smb_fname_fsp_unlink(smb_fname_rel); SET_STAT_INVALID(smb_fname_rel->st); smb_fname_rel->stream_name = saved_streamname; status = open_stream_pathref_fsp(&base_fsp, smb_fname_rel); if (NT_STATUS_EQUAL(status, NT_STATUS_OBJECT_NAME_NOT_FOUND) && !conn->case_sensitive) { char *found = NULL; status = get_real_stream_name( smb_fname_rel, base_fsp, smb_fname_rel->stream_name, &found); if (NT_STATUS_IS_OK(status)) { smb_fname_rel->stream_name = found; found = NULL; status = open_stream_pathref_fsp( &base_fsp, smb_fname_rel); } } if (NT_STATUS_IS_OK(status)) { /* smb_fname must be allocated off mem_ctx. */ smb_fname = cp_smb_filename(mem_ctx, smb_fname_rel->fsp->fsp_name); if (smb_fname == NULL) { goto fail; } status = move_smb_fname_fsp_link(smb_fname, smb_fname_rel); if (!NT_STATUS_IS_OK(status)) { goto fail; } goto done; } if (NT_STATUS_EQUAL(status, NT_STATUS_OBJECT_NAME_NOT_FOUND)) { /* * Creating a new stream * * We should save the already-open base fsp for * create_file_unixpath() somehow. */ smb_fname = full_path_from_dirfsp_atname( mem_ctx, smb_dirname->fsp, smb_fname_rel); if (smb_fname == NULL) { status = NT_STATUS_NO_MEMORY; goto fail; } /* * When open_stream_pathref_fsp() returns * NT_STATUS_OBJECT_NAME_NOT_FOUND, smb_fname_rel->fsp * has been set to NULL, so we must free base_fsp separately * to prevent fd-leaks when opening a stream that doesn't * exist. */ fd_close(base_fsp); file_free(NULL, base_fsp); base_fsp = NULL; goto done; } if (!NT_STATUS_IS_OK(status)) { goto fail; } done: *_dirfsp = smb_dirname->fsp; *_smb_fname = smb_fname; smb_fname_fsp_unlink(smb_fname_rel); TALLOC_FREE(smb_fname_rel); return NT_STATUS_OK; fail: /* * If open_stream_pathref_fsp() returns an error, smb_fname_rel->fsp * has been set to NULL, so we must free base_fsp separately * to prevent fd-leaks when opening a stream that doesn't * exist. */ if (base_fsp != NULL) { fd_close(base_fsp); file_free(NULL, base_fsp); base_fsp = NULL; } TALLOC_FREE(dirname); TALLOC_FREE(smb_dirname); TALLOC_FREE(smb_fname_rel); return status; } NTSTATUS filename_convert_dirfsp( TALLOC_CTX *mem_ctx, connection_struct *conn, const char *name_in, uint32_t ucf_flags, NTTIME twrp, struct files_struct **_dirfsp, struct smb_filename **_smb_fname) { char *substitute = NULL; const char *relative = NULL; size_t unparsed = 0; NTSTATUS status; char *target = NULL; char *abs_target = NULL; char *abs_target_canon = NULL; size_t symlink_redirects = 0; bool in_share; next: if (symlink_redirects > 40) { return NT_STATUS_OBJECT_PATH_NOT_FOUND; } status = filename_convert_dirfsp_nosymlink( mem_ctx, conn, name_in, ucf_flags, twrp, _dirfsp, _smb_fname, &substitute, &unparsed); if (!NT_STATUS_EQUAL(status, NT_STATUS_STOPPED_ON_SYMLINK)) { return status; } if (!lp_follow_symlinks(SNUM(conn))) { return NT_STATUS_OBJECT_PATH_NOT_FOUND; } /* * Right now, SMB2 and SMB1 always traverse symlinks * within the share. SMB1+POSIX traverses non-terminal * symlinks within the share. * * When we add SMB2+POSIX we need to return * a NT_STATUS_STOPPED_ON_SYMLINK error here, using the * symlink target data read below if SMB2+POSIX has * UCF_POSIX_PATHNAMES set to cause the client to * resolve all symlinks locally. */ target = symlink_target_path(mem_ctx, name_in, substitute, unparsed); if (target == NULL) { return NT_STATUS_NO_MEMORY; } DBG_DEBUG("name_in: %s, substitute: %s, unparsed: %zu, target=%s\n", name_in, substitute, unparsed, target); if (target[0] == '/') { abs_target = target; } else { abs_target = talloc_asprintf( mem_ctx, "%s/%s", conn->connectpath, target); if (abs_target == NULL) { return NT_STATUS_NO_MEMORY; } } abs_target_canon = canonicalize_absolute_path(mem_ctx, abs_target); if (abs_target_canon == NULL) { return NT_STATUS_NO_MEMORY; } DBG_DEBUG("abs_target_canon=%s\n", abs_target_canon); in_share = subdir_of( conn->connectpath, strlen(conn->connectpath), abs_target_canon, &relative); if (!in_share) { DBG_DEBUG("wide link to %s\n", abs_target_canon); return NT_STATUS_OBJECT_PATH_NOT_FOUND; } name_in = talloc_strdup(mem_ctx, relative); symlink_redirects += 1; goto next; } /* * Build the full path from a dirfsp and dirfsp relative name */ struct smb_filename *full_path_from_dirfsp_atname( TALLOC_CTX *mem_ctx, const struct files_struct *dirfsp, const struct smb_filename *atname) { struct smb_filename *fname = NULL; char *path = NULL; if (dirfsp == dirfsp->conn->cwd_fsp || ISDOT(dirfsp->fsp_name->base_name) || atname->base_name[0] == '/') { path = talloc_strdup(mem_ctx, atname->base_name); } else { path = talloc_asprintf(mem_ctx, "%s/%s", dirfsp->fsp_name->base_name, atname->base_name); } if (path == NULL) { return NULL; } fname = synthetic_smb_fname(mem_ctx, path, atname->stream_name, &atname->st, atname->twrp, atname->flags); TALLOC_FREE(path); if (fname == NULL) { return NULL; } return fname; }