/* Unix SMB/CIFS implementation. POSIX NTVFS backend - filename resolution Copyright (C) Andrew Tridgell 2004 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 . */ /* this is the core code for converting a filename from the format as given by a client to a posix filename, including any case-matching required, and checks for legal characters */ #include "includes.h" #include "vfs_posix.h" #include "system/dir.h" #include "param/param.h" /** compare two filename components. This is where the name mangling hook will go */ static int component_compare(struct pvfs_state *pvfs, const char *comp, const char *name) { int ret; ret = strcasecmp_m(comp, name); if (ret != 0) { char *shortname = pvfs_short_name_component(pvfs, name); if (shortname) { ret = strcasecmp_m(comp, shortname); talloc_free(shortname); } } return ret; } /* search for a filename in a case insensitive fashion TODO: add a cache for previously resolved case-insensitive names TODO: add mangled name support */ static NTSTATUS pvfs_case_search(struct pvfs_state *pvfs, struct pvfs_filename *name, unsigned int flags) { /* break into a series of components */ int num_components; char **components; char *p, *partial_name; int i; /* break up the full name info pathname components */ num_components=2; p = name->full_name + strlen(pvfs->base_directory) + 1; for (;*p;p++) { if (*p == '/') { num_components++; } } components = talloc_array(name, char *, num_components); p = name->full_name + strlen(pvfs->base_directory); *p++ = 0; components[0] = name->full_name; for (i=1;ist) == 0) { if (ist.st_mode)) { return NT_STATUS_OBJECT_PATH_NOT_FOUND; } talloc_free(partial_name); partial_name = test_name; if (i == num_components - 1) { name->exists = true; } continue; } /* the filesystem might be case insensitive, in which case a search is pointless unless the name is mangled */ if ((pvfs->flags & PVFS_FLAG_CI_FILESYSTEM) && !pvfs_is_mangled_component(pvfs, components[i])) { if (i < num_components-1) { return NT_STATUS_OBJECT_PATH_NOT_FOUND; } partial_name = test_name; continue; } dir = opendir(partial_name); if (!dir) { return pvfs_map_errno(pvfs, errno); } while ((de = readdir(dir))) { if (component_compare(pvfs, components[i], de->d_name) == 0) { break; } } if (!de) { if (i < num_components-1) { closedir(dir); return NT_STATUS_OBJECT_PATH_NOT_FOUND; } } else { components[i] = talloc_strdup(name, de->d_name); } test_name = talloc_asprintf(name, "%s/%s", partial_name, components[i]); talloc_free(partial_name); partial_name = test_name; closedir(dir); } if (!name->exists) { if (stat(partial_name, &name->st) == 0) { name->exists = true; } } talloc_free(name->full_name); name->full_name = partial_name; if (name->exists) { return pvfs_fill_dos_info(pvfs, name, flags, -1); } return NT_STATUS_OK; } /* parse a alternate data stream name */ static NTSTATUS parse_stream_name(struct pvfs_filename *name, const char *s) { char *p, *stream_name; if (s[1] == '\0') { return NT_STATUS_OBJECT_NAME_INVALID; } name->stream_name = stream_name = talloc_strdup(name, s+1); if (name->stream_name == NULL) { return NT_STATUS_NO_MEMORY; } p = stream_name; while (*p) { size_t c_size; codepoint_t c = next_codepoint(p, &c_size); switch (c) { case '/': case '\\': return NT_STATUS_OBJECT_NAME_INVALID; case ':': *p= 0; p++; if (*p == '\0') { return NT_STATUS_OBJECT_NAME_INVALID; } if (strcasecmp_m(p, "$DATA") != 0) { if (strchr_m(p, ':')) { return NT_STATUS_OBJECT_NAME_INVALID; } return NT_STATUS_INVALID_PARAMETER; } c_size = 0; p--; break; } p += c_size; } if (strcmp(name->stream_name, "") == 0) { /* * we don't set stream_name to NULL, here * as this would be wrong for directories * * pvfs_fill_dos_info() will set it to NULL * if it's not a directory. */ name->stream_id = 0; } else { name->stream_id = pvfs_name_hash(name->stream_name, strlen(name->stream_name)); } return NT_STATUS_OK; } /* convert a CIFS pathname to a unix pathname. Note that this does NOT take into account case insensitivity, and in fact does not access the filesystem at all. It is merely a reformatting and charset checking routine. errors are returned if the filename is illegal given the flags */ static NTSTATUS pvfs_unix_path(struct pvfs_state *pvfs, const char *cifs_name, unsigned int flags, struct pvfs_filename *name) { char *ret, *p, *p_start; NTSTATUS status; name->original_name = talloc_strdup(name, cifs_name); /* remove any :$DATA */ p = strrchr(name->original_name, ':'); if (p && strcasecmp_m(p, ":$DATA") == 0) { if (p > name->original_name && p[-1] == ':') { p--; } *p = 0; } name->stream_name = NULL; name->stream_id = 0; name->has_wildcard = false; while (*cifs_name == '\\') { cifs_name++; } if (*cifs_name == 0) { name->full_name = talloc_asprintf(name, "%s/.", pvfs->base_directory); if (name->full_name == NULL) { return NT_STATUS_NO_MEMORY; } return NT_STATUS_OK; } ret = talloc_asprintf(name, "%s/%s", pvfs->base_directory, cifs_name); if (ret == NULL) { return NT_STATUS_NO_MEMORY; } p = ret + strlen(pvfs->base_directory) + 1; /* now do an in-place conversion of '\' to '/', checking for legal characters */ p_start = p; while (*p) { size_t c_size; codepoint_t c = next_codepoint(p, &c_size); if (c <= 0x1F) { return NT_STATUS_OBJECT_NAME_INVALID; } switch (c) { case '\\': if (name->has_wildcard) { /* wildcards are only allowed in the last part of a name */ return NT_STATUS_OBJECT_NAME_INVALID; } if (p > p_start && (p[1] == '\\' || p[1] == '\0')) { /* see if it is definately a "\\" or * a trailing "\". If it is then fail here, * and let the next layer up try again after * pvfs_reduce_name() if it wants to. This is * much more efficient on average than always * scanning for these separately */ return NT_STATUS_OBJECT_PATH_SYNTAX_BAD; } else { *p = '/'; } break; case ':': if (!(flags & PVFS_RESOLVE_STREAMS)) { return NT_STATUS_OBJECT_NAME_INVALID; } if (name->has_wildcard) { return NT_STATUS_OBJECT_NAME_INVALID; } status = parse_stream_name(name, p); if (!NT_STATUS_IS_OK(status)) { return status; } *p-- = 0; break; case '*': case '>': case '<': case '?': case '"': if (!(flags & PVFS_RESOLVE_WILDCARD)) { return NT_STATUS_OBJECT_NAME_INVALID; } name->has_wildcard = true; break; case '/': case '|': return NT_STATUS_OBJECT_NAME_INVALID; case '.': /* see if it is definately a .. or . component. If it is then fail here, and let the next layer up try again after pvfs_reduce_name() if it wants to. This is much more efficient on average than always scanning for these separately */ if (p[1] == '.' && (p[2] == 0 || p[2] == '\\') && (p == p_start || p[-1] == '/')) { return NT_STATUS_OBJECT_PATH_SYNTAX_BAD; } if ((p[1] == 0 || p[1] == '\\') && (p == p_start || p[-1] == '/')) { return NT_STATUS_OBJECT_PATH_SYNTAX_BAD; } break; } p += c_size; } name->full_name = ret; return NT_STATUS_OK; } /* reduce a name that contains .. components or repeated \ separators return NULL if it can't be reduced */ static NTSTATUS pvfs_reduce_name(TALLOC_CTX *mem_ctx, const char **fname, unsigned int flags) { codepoint_t c; size_t c_size, len; int i, num_components, err_count; char **components; char *p, *s, *ret; s = talloc_strdup(mem_ctx, *fname); if (s == NULL) return NT_STATUS_NO_MEMORY; for (num_components=1, p=s; *p; p += c_size) { c = next_codepoint(p, &c_size); if (c == '\\') num_components++; } components = talloc_array(s, char *, num_components+1); if (components == NULL) { talloc_free(s); return NT_STATUS_NO_MEMORY; } components[0] = s; for (i=0, p=s; *p; p += c_size) { c = next_codepoint(p, &c_size); if (c == '\\') { *p = 0; components[++i] = p+1; } } components[i+1] = NULL; /* rather bizarre! '.' components are not allowed, but the rules for what error code to give don't seem to make sense. This is a close approximation. */ for (err_count=i=0;components[i];i++) { if (strcmp(components[i], "") == 0) { continue; } if (ISDOT(components[i]) || err_count) { err_count++; } } if (err_count) { if (flags & PVFS_RESOLVE_WILDCARD) err_count--; if (err_count==1) { return NT_STATUS_OBJECT_NAME_INVALID; } else { return NT_STATUS_OBJECT_PATH_NOT_FOUND; } } /* remove any null components */ for (i=0;components[i];i++) { if (strcmp(components[i], "") == 0) { memmove(&components[i], &components[i+1], sizeof(char *)*(num_components-i)); i--; continue; } if (ISDOTDOT(components[i])) { if (i < 1) return NT_STATUS_OBJECT_PATH_SYNTAX_BAD; memmove(&components[i-1], &components[i+1], sizeof(char *)*(num_components-i)); i -= 2; continue; } } if (components[0] == NULL) { talloc_free(s); *fname = talloc_strdup(mem_ctx, "\\"); return NT_STATUS_OK; } for (len=i=0;components[i];i++) { len += strlen(components[i]) + 1; } /* rebuild the name */ ret = talloc_array(mem_ctx, char, len+1); if (ret == NULL) { talloc_free(s); return NT_STATUS_NO_MEMORY; } for (len=0,i=0;components[i];i++) { size_t len1 = strlen(components[i]); ret[len] = '\\'; memcpy(ret+len+1, components[i], len1); len += len1 + 1; } ret[len] = 0; talloc_set_name_const(ret, ret); talloc_free(s); *fname = ret; return NT_STATUS_OK; } /* resolve a name from relative client format to a struct pvfs_filename the memory for the filename is made as a talloc child of 'name' flags include: PVFS_RESOLVE_NO_WILDCARD = wildcards are considered illegal characters PVFS_RESOLVE_STREAMS = stream names are allowed TODO: ../ collapsing, and outside share checking */ NTSTATUS pvfs_resolve_name(struct pvfs_state *pvfs, struct ntvfs_request *req, const char *cifs_name, unsigned int flags, struct pvfs_filename **name) { NTSTATUS status; *name = talloc(req, struct pvfs_filename); if (*name == NULL) { return NT_STATUS_NO_MEMORY; } (*name)->exists = false; (*name)->stream_exists = false; (*name)->allow_override = false; if (!(pvfs->fs_attribs & FS_ATTR_NAMED_STREAMS)) { flags &= ~PVFS_RESOLVE_STREAMS; } /* SMB2 doesn't allow a leading slash */ if (req->ctx->protocol >= PROTOCOL_SMB2_02 && *cifs_name == '\\') { return NT_STATUS_INVALID_PARAMETER; } /* do the basic conversion to a unix formatted path, also checking for allowable characters */ status = pvfs_unix_path(pvfs, cifs_name, flags, *name); if (NT_STATUS_EQUAL(status, NT_STATUS_OBJECT_PATH_SYNTAX_BAD)) { /* it might contain .. components which need to be reduced */ status = pvfs_reduce_name(*name, &cifs_name, flags); if (!NT_STATUS_IS_OK(status)) { return status; } status = pvfs_unix_path(pvfs, cifs_name, flags, *name); } if (!NT_STATUS_IS_OK(status)) { return status; } /* if it has a wildcard then no point doing a stat() of the full name. Instead We need check if the directory exists */ if ((*name)->has_wildcard) { const char *p; char *dir_name, *saved_name; p = strrchr((*name)->full_name, '/'); if (p == NULL) { /* root directory wildcard is OK */ return NT_STATUS_OK; } dir_name = talloc_strndup(*name, (*name)->full_name, (p-(*name)->full_name)); if (stat(dir_name, &(*name)->st) == 0) { talloc_free(dir_name); return NT_STATUS_OK; } /* we need to search for a matching name */ saved_name = (*name)->full_name; (*name)->full_name = dir_name; status = pvfs_case_search(pvfs, *name, flags); if (!NT_STATUS_IS_OK(status)) { /* the directory doesn't exist */ (*name)->full_name = saved_name; return status; } /* it does exist, but might need a case change */ if (dir_name != (*name)->full_name) { (*name)->full_name = talloc_asprintf(*name, "%s%s", (*name)->full_name, p); NT_STATUS_HAVE_NO_MEMORY((*name)->full_name); } else { (*name)->full_name = saved_name; talloc_free(dir_name); } return NT_STATUS_OK; } /* if we can stat() the full name now then we are done */ if (stat((*name)->full_name, &(*name)->st) == 0) { (*name)->exists = true; return pvfs_fill_dos_info(pvfs, *name, flags, -1); } /* search for a matching filename */ status = pvfs_case_search(pvfs, *name, flags); return status; } /* do a partial resolve, returning a pvfs_filename structure given a base path and a relative component. It is an error if the file does not exist. No case-insensitive matching is done. this is used in places like directory searching where we need a pvfs_filename to pass to a function, but already know the unix base directory and component */ NTSTATUS pvfs_resolve_partial(struct pvfs_state *pvfs, TALLOC_CTX *mem_ctx, const char *unix_dir, const char *fname, unsigned int flags, struct pvfs_filename **name) { NTSTATUS status; *name = talloc(mem_ctx, struct pvfs_filename); if (*name == NULL) { return NT_STATUS_NO_MEMORY; } (*name)->full_name = talloc_asprintf(*name, "%s/%s", unix_dir, fname); if ((*name)->full_name == NULL) { return NT_STATUS_NO_MEMORY; } if (stat((*name)->full_name, &(*name)->st) == -1) { return NT_STATUS_OBJECT_NAME_NOT_FOUND; } (*name)->exists = true; (*name)->stream_exists = true; (*name)->has_wildcard = false; (*name)->original_name = talloc_strdup(*name, fname); (*name)->stream_name = NULL; (*name)->stream_id = 0; (*name)->allow_override = false; status = pvfs_fill_dos_info(pvfs, *name, flags, -1); return status; } /* fill in the pvfs_filename info for an open file, given the current info for a (possibly) non-open file. This is used by places that need to update the pvfs_filename stat information, and by pvfs_open() */ NTSTATUS pvfs_resolve_name_fd(struct pvfs_state *pvfs, int fd, struct pvfs_filename *name, unsigned int flags) { dev_t device = (dev_t)0; ino_t inode = 0; if (name->exists) { device = name->st.st_dev; inode = name->st.st_ino; } if (fd == -1) { if (stat(name->full_name, &name->st) == -1) { return NT_STATUS_INVALID_HANDLE; } } else { if (fstat(fd, &name->st) == -1) { return NT_STATUS_INVALID_HANDLE; } } if (name->exists && (device != name->st.st_dev || inode != name->st.st_ino)) { /* the file we are looking at has changed! this could be someone trying to exploit a race condition. Certainly we don't want to continue operating on this file */ DEBUG(0,("pvfs: WARNING: file '%s' changed during resolve - failing\n", name->full_name)); return NT_STATUS_UNEXPECTED_IO_ERROR; } name->exists = true; return pvfs_fill_dos_info(pvfs, name, flags, fd); } /* fill in the pvfs_filename info for an open file, given the current info for a (possibly) non-open file. This is used by places that need to update the pvfs_filename stat information, and the path after a possible rename on a different handle. */ NTSTATUS pvfs_resolve_name_handle(struct pvfs_state *pvfs, struct pvfs_file_handle *h) { NTSTATUS status; if (h->have_opendb_entry) { struct odb_lock *lck; char *name = NULL; lck = odb_lock(h, h->pvfs->odb_context, &h->odb_locking_key); if (lck == NULL) { DEBUG(0,("%s: failed to lock file '%s' in opendb\n", __FUNCTION__, h->name->full_name)); /* we were supposed to do a blocking lock, so something is badly wrong! */ return NT_STATUS_INTERNAL_DB_CORRUPTION; } status = odb_get_path(lck, (const char **) &name); if (NT_STATUS_IS_OK(status)) { /* * This relies an the fact that * renames of open files are only * allowed by setpathinfo() and setfileinfo() * and there're only renames within the same * directory supported */ if (strcmp(h->name->full_name, name) != 0) { const char *orig_dir; const char *new_file; char *new_orig; char *delim; delim = strrchr(name, '/'); if (!delim) { talloc_free(lck); return NT_STATUS_INTERNAL_ERROR; } new_file = delim + 1; delim = strrchr(h->name->original_name, '\\'); if (delim) { delim[0] = '\0'; orig_dir = h->name->original_name; new_orig = talloc_asprintf(h->name, "%s\\%s", orig_dir, new_file); if (!new_orig) { talloc_free(lck); return NT_STATUS_NO_MEMORY; } } else { new_orig = talloc_strdup(h->name, new_file); if (!new_orig) { talloc_free(lck); return NT_STATUS_NO_MEMORY; } } talloc_free(h->name->original_name); talloc_free(h->name->full_name); h->name->full_name = talloc_steal(h->name, name); h->name->original_name = new_orig; } } talloc_free(lck); } /* * TODO: pass PVFS_RESOLVE_NO_OPENDB and get * the write time from odb_lock() above. */ status = pvfs_resolve_name_fd(pvfs, h->fd, h->name, 0); NT_STATUS_NOT_OK_RETURN(status); if (!null_nttime(h->write_time.close_time)) { h->name->dos.write_time = h->write_time.close_time; } return NT_STATUS_OK; } /* resolve the parent of a given name */ NTSTATUS pvfs_resolve_parent(struct pvfs_state *pvfs, TALLOC_CTX *mem_ctx, const struct pvfs_filename *child, struct pvfs_filename **name) { NTSTATUS status; char *p; *name = talloc(mem_ctx, struct pvfs_filename); if (*name == NULL) { return NT_STATUS_NO_MEMORY; } (*name)->full_name = talloc_strdup(*name, child->full_name); if ((*name)->full_name == NULL) { return NT_STATUS_NO_MEMORY; } p = strrchr_m((*name)->full_name, '/'); if (p == NULL) { return NT_STATUS_OBJECT_PATH_SYNTAX_BAD; } /* this handles the root directory */ if (p == (*name)->full_name) { p[1] = 0; } else { p[0] = 0; } if (stat((*name)->full_name, &(*name)->st) == -1) { return NT_STATUS_OBJECT_NAME_NOT_FOUND; } (*name)->exists = true; (*name)->stream_exists = true; (*name)->has_wildcard = false; /* we can't get the correct 'original_name', but for the purposes of this call this is close enough */ (*name)->original_name = talloc_strdup(*name, child->original_name); if ((*name)->original_name == NULL) { return NT_STATUS_NO_MEMORY; } (*name)->stream_name = NULL; (*name)->stream_id = 0; (*name)->allow_override = false; status = pvfs_fill_dos_info(pvfs, *name, PVFS_RESOLVE_NO_OPENDB, -1); return status; }