| // Copyright 2019 The Chromium OS Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "smbfs/smb_filesystem.h" |
| |
| #include <utility> |
| #include <vector> |
| |
| #include <base/bind.h> |
| #include <base/callback_helpers.h> |
| #include <base/logging.h> |
| #include <base/posix/safe_strerror.h> |
| #include <base/strings/string_number_conversions.h> |
| #include <base/strings/string_piece.h> |
| #include <base/strings/string_util.h> |
| |
| #include "smbfs/samba_interface_impl.h" |
| #include "smbfs/util.h" |
| |
| namespace smbfs { |
| |
| namespace { |
| |
| constexpr char kSambaThreadName[] = "smbfs-libsmb"; |
| constexpr char kUrlPrefix[] = "smb://"; |
| |
| constexpr double kAttrTimeoutSeconds = 5.0; |
| constexpr mode_t kAllowedFileTypes = S_IFREG | S_IFDIR; |
| constexpr mode_t kFileModeMask = kAllowedFileTypes | 0770; |
| |
| // Cache stat information for the latest 1024 directory entries retrieved. |
| constexpr int kStatCacheSize = 1024; |
| constexpr double kStatCacheTimeoutSeconds = kAttrTimeoutSeconds; |
| |
| bool IsAllowedFileMode(mode_t mode) { |
| return mode & kAllowedFileTypes; |
| } |
| |
| } // namespace |
| |
| SmbFilesystem::Options::Options() = default; |
| |
| SmbFilesystem::Options::~Options() = default; |
| |
| SmbFilesystem::Options::Options(Options&&) = default; |
| |
| SmbFilesystem::Options& SmbFilesystem::Options::operator=(Options&&) = default; |
| |
| SmbFilesystem::SmbFilesystem(Delegate* delegate, Options options) |
| : delegate_(delegate), |
| share_path_(options.share_path), |
| uid_(options.uid), |
| gid_(options.gid), |
| use_kerberos_(options.use_kerberos), |
| samba_thread_(kSambaThreadName), |
| stat_cache_(kStatCacheSize) { |
| DCHECK(delegate_); |
| |
| // Ensure files are not owned by root. |
| CHECK_GT(uid_, 0); |
| CHECK_GT(gid_, 0); |
| |
| CHECK(!share_path_.empty()); |
| CHECK_NE(share_path_.back(), '/'); |
| |
| samba_impl_ = std::make_unique<SambaInterfaceImpl>( |
| std::move(options.credentials), options.allow_ntlm); |
| |
| CHECK(samba_thread_.Start()); |
| } |
| |
| SmbFilesystem::SmbFilesystem(Delegate* delegate, const std::string& share_path) |
| : delegate_(delegate), |
| share_path_(share_path), |
| samba_thread_(kSambaThreadName), |
| stat_cache_(kStatCacheSize) { |
| DCHECK(delegate_); |
| } |
| |
| SmbFilesystem::~SmbFilesystem() { |
| if (samba_impl_) { |
| // Stop the Samba processing thread before destroying the context to avoid a |
| // UAF on the context. |
| samba_thread_.Stop(); |
| } |
| } |
| |
| base::WeakPtr<SmbFilesystem> SmbFilesystem::GetWeakPtr() { |
| return weak_factory_.GetWeakPtr(); |
| } |
| |
| void SmbFilesystem::SetSambaInterface( |
| std::unique_ptr<SambaInterface> samba_interface) { |
| samba_impl_ = std::move(samba_interface); |
| } |
| |
| SmbFilesystem::ConnectError SmbFilesystem::EnsureConnected() { |
| SMBCFILE* dir = nullptr; |
| int err = samba_impl_->OpenDirectory(resolved_share_path_, &dir); |
| if (err) { |
| LOG(INFO) << "EnsureConnected OpenDirectory failed"; |
| switch (err) { |
| case EPERM: |
| case EACCES: |
| return ConnectError::kAccessDenied; |
| case ENODEV: |
| case ENOENT: |
| case ETIMEDOUT: |
| // This means unable to resolve host, in some, but not necessarily all |
| // cases. |
| case EINVAL: |
| // Host unreachable. |
| case EHOSTUNREACH: |
| // Host not listening on SMB port. |
| case ECONNREFUSED: |
| return ConnectError::kNotFound; |
| case ECONNABORTED: |
| return ConnectError::kSmb1Unsupported; |
| default: |
| LOG(WARNING) << "Unexpected error code " << err << ": " |
| << base::safe_strerror(err); |
| return ConnectError::kUnknownError; |
| } |
| } |
| |
| connected_ = true; |
| |
| err = samba_impl_->CloseDirectory(dir); |
| LOG_IF(WARNING, err) << "CloseDirectory during EnsureConnected failed: " |
| << base::safe_strerror(err); |
| |
| return ConnectError::kOk; |
| } |
| |
| void SmbFilesystem::SetResolvedAddress(const std::vector<uint8_t>& ip_address) { |
| base::AutoLock l(lock_); |
| |
| if (ip_address.empty()) { |
| resolved_share_path_ = share_path_; |
| return; |
| } else if (ip_address.size() != 4) { |
| // TODO(crbug.com/1051291): Support IPv6. |
| LOG(ERROR) << "Invalid IP address"; |
| return; |
| } |
| |
| std::string address_str = IpAddressToString(ip_address); |
| DCHECK(!address_str.empty()); |
| |
| const base::StringPiece prefix(kUrlPrefix); |
| DCHECK(base::StartsWith(share_path_, prefix, base::CompareCase::SENSITIVE)); |
| std::string::size_type host_end = share_path_.find('/', prefix.size()); |
| DCHECK_NE(host_end, std::string::npos); |
| resolved_share_path_ = |
| std::string(prefix) + address_str + share_path_.substr(host_end); |
| } |
| |
| struct stat SmbFilesystem::MakeStat(ino_t inode, |
| const struct stat& in_stat) const { |
| struct stat stat = {0}; |
| stat.st_ino = inode; |
| stat.st_mode = MakeStatModeBits(in_stat.st_mode); |
| stat.st_uid = uid_; |
| stat.st_gid = gid_; |
| stat.st_nlink = 1; |
| stat.st_size = in_stat.st_size; |
| stat.st_atim = in_stat.st_atim; |
| stat.st_ctim = in_stat.st_ctim; |
| stat.st_mtim = in_stat.st_mtim; |
| return stat; |
| } |
| |
| mode_t SmbFilesystem::MakeStatModeBits(mode_t in_mode) const { |
| mode_t mode = in_mode; |
| |
| // Clear any "other" permission bits. |
| mode &= kFileModeMask; |
| |
| // If the entry is a directory, it must have the execute bit set. |
| if (in_mode & S_IFDIR) { |
| mode |= S_IXUSR; |
| } else { |
| mode &= ~S_IXUSR; |
| } |
| |
| // Propagate user bits to group bits. |
| mode &= ~S_IRWXG; |
| if (mode & S_IRUSR) { |
| mode |= S_IRGRP; |
| } |
| if (mode & S_IWUSR) { |
| mode |= S_IWGRP; |
| } |
| if (mode & S_IXUSR) { |
| mode |= S_IXGRP; |
| } |
| |
| return mode; |
| } |
| |
| std::string SmbFilesystem::MakeShareFilePath(const base::FilePath& path) const { |
| std::string base_share_path; |
| { |
| base::AutoLock l(lock_); |
| DCHECK(!resolved_share_path_.empty()); |
| base_share_path = resolved_share_path_; |
| } |
| |
| if (path == base::FilePath("/")) { |
| return base_share_path; |
| } |
| |
| // Paths are constructed and not passed directly over FUSE. Therefore, these |
| // two properties should always hold. |
| DCHECK(path.IsAbsolute()); |
| DCHECK(!path.EndsWithSeparator()); |
| return base_share_path + path.value(); |
| } |
| |
| std::string SmbFilesystem::ShareFilePathFromInode(ino_t inode) const { |
| const base::FilePath file_path = inode_map_.GetPath(inode); |
| CHECK(!file_path.empty()) << "Path lookup for invalid inode: " << inode; |
| return MakeShareFilePath(file_path); |
| } |
| |
| uint64_t SmbFilesystem::AddOpenFile(SMBCFILE* file) { |
| uint64_t handle = open_files_seq_++; |
| // Disallow wrap around. |
| CHECK(handle); |
| open_files_[handle] = file; |
| return handle; |
| } |
| |
| void SmbFilesystem::RemoveOpenFile(uint64_t handle) { |
| auto it = open_files_.find(handle); |
| if (it == open_files_.end()) { |
| NOTREACHED() << "File handle not found"; |
| return; |
| } |
| open_files_.erase(it); |
| } |
| |
| SMBCFILE* SmbFilesystem::LookupOpenFile(uint64_t handle) const { |
| const auto it = open_files_.find(handle); |
| if (it == open_files_.end()) { |
| return nullptr; |
| } |
| return it->second; |
| } |
| |
| void SmbFilesystem::MaybeUpdateCredentials(int error) { |
| if (use_kerberos_) { |
| // If Kerberos is being used, it is assumed a valid user/workgroup has |
| // already been provided, and password is always ignored. |
| return; |
| } else if (connected_) { |
| // If a connection has already been made successfully, assume the |
| // existing credentials are correct. |
| return; |
| } |
| |
| if (error == EPERM || error == EACCES) { |
| // Delegate calls must always be made on the constructor thread. |
| main_task_runner_->PostTask( |
| FROM_HERE, base::BindOnce(&SmbFilesystem::RequestCredentialUpdate, |
| base::Unretained(this))); |
| } |
| } |
| |
| void SmbFilesystem::RequestCredentialUpdate() { |
| DCHECK(main_task_runner_->BelongsToCurrentThread()); |
| |
| if (requesting_credentials_) { |
| // Do nothing if a credential request is already in progress. |
| return; |
| } |
| |
| requesting_credentials_ = true; |
| delegate_->RequestCredentials( |
| base::Bind(&SmbFilesystem::OnRequestCredentialsDone, GetWeakPtr())); |
| } |
| |
| void SmbFilesystem::OnRequestCredentialsDone( |
| std::unique_ptr<SmbCredential> credentials) { |
| requesting_credentials_ = false; |
| if (!credentials) { |
| return; |
| } |
| |
| samba_impl_->UpdateCredentials(std::move(credentials)); |
| } |
| |
| void SmbFilesystem::StatFs(std::unique_ptr<StatFsRequest> request, |
| fuse_ino_t inode) { |
| samba_thread_.task_runner()->PostTask( |
| FROM_HERE, |
| base::BindOnce(&SmbFilesystem::StatFsInternal, base::Unretained(this), |
| std::move(request), inode)); |
| } |
| |
| void SmbFilesystem::StatFsInternal(std::unique_ptr<StatFsRequest> request, |
| fuse_ino_t inode) { |
| if (request->IsInterrupted()) { |
| return; |
| } |
| |
| std::string share_file_path = ShareFilePathFromInode(inode); |
| struct statvfs smb_statvfs = {0}; |
| int error = samba_impl_->StatVfs(share_file_path, &smb_statvfs); |
| if (error) { |
| VLOG(1) << "StatVfs path: " << share_file_path |
| << " failed: " << base::safe_strerror(error); |
| request->ReplyError(error); |
| return; |
| } |
| |
| request->ReplyStatFs(smb_statvfs); |
| } |
| |
| void SmbFilesystem::Lookup(std::unique_ptr<EntryRequest> request, |
| fuse_ino_t parent_inode, |
| const std::string& name) { |
| samba_thread_.task_runner()->PostTask( |
| FROM_HERE, |
| base::BindOnce(&SmbFilesystem::LookupInternal, base::Unretained(this), |
| std::move(request), parent_inode, name)); |
| } |
| |
| void SmbFilesystem::LookupInternal(std::unique_ptr<EntryRequest> request, |
| fuse_ino_t parent_inode, |
| const std::string& name) { |
| if (request->IsInterrupted()) { |
| return; |
| } |
| |
| const base::FilePath parent_path = inode_map_.GetPath(parent_inode); |
| CHECK(!parent_path.empty()) |
| << "Lookup on invalid parent inode: " << parent_inode; |
| const base::FilePath file_path = parent_path.Append(name); |
| const std::string share_file_path = MakeShareFilePath(file_path); |
| |
| ino_t inode = inode_map_.IncInodeRef(file_path); |
| struct stat smb_stat = {0}; |
| if (!GetCachedInodeStat(inode, &smb_stat)) { |
| int error = samba_impl_->Stat(share_file_path, &smb_stat); |
| if (error) { |
| VLOG(1) << "Stat path: " << share_file_path |
| << " failed: " << base::safe_strerror(error); |
| request->ReplyError(error); |
| inode_map_.Forget(inode, 1); |
| return; |
| } else if (!IsAllowedFileMode(smb_stat.st_mode)) { |
| VLOG(1) << "Disallowed file mode " << smb_stat.st_mode << " for path " |
| << share_file_path; |
| request->ReplyError(EACCES); |
| inode_map_.Forget(inode, 1); |
| return; |
| } |
| } |
| |
| struct stat entry_stat = MakeStat(inode, smb_stat); |
| fuse_entry_param entry = {0}; |
| entry.ino = inode; |
| entry.generation = 1; |
| entry.attr = entry_stat; |
| entry.attr_timeout = kAttrTimeoutSeconds; |
| entry.entry_timeout = kAttrTimeoutSeconds; |
| request->ReplyEntry(entry); |
| } |
| |
| void SmbFilesystem::Forget(fuse_ino_t inode, uint64_t count) { |
| samba_thread_.task_runner()->PostTask( |
| FROM_HERE, base::BindOnce(&SmbFilesystem::ForgetInternal, |
| base::Unretained(this), inode, count)); |
| } |
| |
| void SmbFilesystem::ForgetInternal(fuse_ino_t inode, uint64_t count) { |
| if (inode_map_.Forget(inode, count)) { |
| // The inode was removed, invalidate any cached stat information. |
| EraseCachedInodeStat(inode); |
| } |
| } |
| |
| void SmbFilesystem::GetAttr(std::unique_ptr<AttrRequest> request, |
| fuse_ino_t inode) { |
| samba_thread_.task_runner()->PostTask( |
| FROM_HERE, |
| base::BindOnce(&SmbFilesystem::GetAttrInternal, base::Unretained(this), |
| std::move(request), inode)); |
| } |
| |
| void SmbFilesystem::GetAttrInternal(std::unique_ptr<AttrRequest> request, |
| fuse_ino_t inode) { |
| if (request->IsInterrupted()) { |
| return; |
| } |
| |
| struct stat smb_stat = {0}; |
| const std::string share_file_path = ShareFilePathFromInode(inode); |
| |
| if (!GetCachedInodeStat(inode, &smb_stat)) { |
| int error = samba_impl_->Stat(share_file_path, &smb_stat); |
| if (error) { |
| VLOG(1) << "Stat path: " << share_file_path |
| << " failed: " << base::safe_strerror(error); |
| |
| if (inode == FUSE_ROOT_ID) { |
| MaybeUpdateCredentials(error); |
| } |
| |
| request->ReplyError(error); |
| return; |
| } |
| } |
| |
| if (!IsAllowedFileMode(smb_stat.st_mode)) { |
| VLOG(1) << "Disallowed file mode " << smb_stat.st_mode << " for path " |
| << share_file_path; |
| request->ReplyError(EACCES); |
| return; |
| } |
| |
| connected_ = true; |
| struct stat reply_stat = MakeStat(inode, smb_stat); |
| request->ReplyAttr(reply_stat, kAttrTimeoutSeconds); |
| } |
| |
| void SmbFilesystem::SetAttr(std::unique_ptr<AttrRequest> request, |
| fuse_ino_t inode, |
| base::Optional<uint64_t> file_handle, |
| const struct stat& attr, |
| int to_set) { |
| samba_thread_.task_runner()->PostTask( |
| FROM_HERE, base::BindOnce(&SmbFilesystem::SetAttrInternal, |
| base::Unretained(this), std::move(request), |
| inode, std::move(file_handle), attr, to_set)); |
| } |
| |
| void SmbFilesystem::SetAttrInternal(std::unique_ptr<AttrRequest> request, |
| fuse_ino_t inode, |
| base::Optional<uint64_t> file_handle, |
| const struct stat& attr, |
| int to_set) { |
| if (request->IsInterrupted()) { |
| return; |
| } |
| |
| // Currently, only setting size (ie. O_TRUC, ftruncate()) or times (ie. |
| // utime(), utimensat()) is supported. |
| const int kSupportedAttrs = |
| FUSE_SET_ATTR_SIZE | FUSE_SET_ATTR_ATIME | FUSE_SET_ATTR_MTIME; |
| if (to_set & ~kSupportedAttrs) { |
| LOG(WARNING) << "Unsupported |to_set| flags on setattr: " << to_set; |
| request->ReplyError(ENOTSUP); |
| return; |
| } |
| if (!to_set) { |
| VLOG(1) << "No supported |to_set| flags set on setattr: " << to_set; |
| request->ReplyError(EINVAL); |
| return; |
| } |
| |
| const std::string share_file_path = ShareFilePathFromInode(inode); |
| |
| struct stat smb_stat = {0}; |
| int error = samba_impl_->Stat(share_file_path, &smb_stat); |
| if (error) { |
| VLOG(1) << "Stat path: " << share_file_path |
| << " failed: " << base::safe_strerror(error); |
| request->ReplyError(error); |
| return; |
| } |
| struct stat reply_stat = MakeStat(inode, smb_stat); |
| |
| // SetAttrInternal supports changing multiple attributes simultaneously but |
| // this is not atomic: all changes must succeed for the request to succeed but |
| // a partial failure will not be unapplied. |
| if (to_set & FUSE_SET_ATTR_SIZE) { |
| error = SetFileSizeInternal(share_file_path, file_handle, attr.st_size, |
| smb_stat, &reply_stat); |
| if (error) { |
| request->ReplyError(error); |
| return; |
| } |
| } |
| |
| if (to_set & (FUSE_SET_ATTR_ATIME | FUSE_SET_ATTR_MTIME)) { |
| error = SetUtimesInternal(share_file_path, to_set, attr.st_atim, |
| attr.st_mtim, smb_stat, &reply_stat); |
| if (error) { |
| request->ReplyError(error); |
| return; |
| } |
| } |
| |
| // Modifying the attributes invalidates any cached inode we have. |
| EraseCachedInodeStat(inode); |
| |
| request->ReplyAttr(reply_stat, kAttrTimeoutSeconds); |
| } |
| |
| int SmbFilesystem::SetFileSizeInternal(const std::string& share_file_path, |
| base::Optional<uint64_t> file_handle, |
| off_t size, |
| const struct stat& current_stat, |
| struct stat* reply_stat) { |
| DCHECK(reply_stat); |
| |
| if (current_stat.st_mode & S_IFDIR) { |
| return EISDIR; |
| } else if (!(current_stat.st_mode & S_IFREG)) { |
| VLOG(1) << "Disallowed file mode " << current_stat.st_mode << " for path " |
| << share_file_path; |
| return EACCES; |
| } |
| |
| SMBCFILE* file = nullptr; |
| int error = 0; |
| base::ScopedClosureRunner file_closer; |
| if (file_handle) { |
| file = LookupOpenFile(*file_handle); |
| if (!file) { |
| return EBADF; |
| } |
| } else { |
| error = samba_impl_->OpenFile(share_file_path, O_WRONLY, 0, &file); |
| if (error) { |
| VLOG(1) << "OpenFile path: " << share_file_path |
| << " failed: " << base::safe_strerror(error); |
| return error; |
| } |
| |
| file_closer.ReplaceClosure(base::BindOnce( |
| [](SambaInterface* samba_impl, SMBCFILE* file) { |
| int error = samba_impl->CloseFile(file); |
| if (error) { |
| LOG(ERROR) |
| << "CloseFile failed on temporary SetFileSizeInternal file: " |
| << base::safe_strerror(error); |
| } |
| }, |
| samba_impl_.get(), file)); |
| } |
| |
| error = samba_impl_->TruncateFile(file, size); |
| if (error) { |
| VLOG(1) << "TruncateFile size: " << size |
| << " failed: " << base::safe_strerror(error); |
| return error; |
| } |
| reply_stat->st_size = size; |
| |
| return 0; |
| } |
| |
| int SmbFilesystem::SetUtimesInternal(const std::string& share_file_path, |
| int to_set, |
| const struct timespec& atime, |
| const struct timespec& mtime, |
| const struct stat& current_stat, |
| struct stat* reply_stat) { |
| DCHECK(to_set & (FUSE_SET_ATTR_ATIME | FUSE_SET_ATTR_MTIME)); |
| DCHECK(reply_stat); |
| |
| struct timespec requested_atime = current_stat.st_atim; |
| struct timespec requested_mtime = current_stat.st_mtim; |
| |
| if (to_set & FUSE_SET_ATTR_ATIME) { |
| requested_atime = atime; |
| } |
| if (to_set & FUSE_SET_ATTR_MTIME) { |
| requested_mtime = mtime; |
| } |
| |
| int error = |
| samba_impl_->SetUtimes(share_file_path, requested_atime, requested_mtime); |
| if (error) { |
| VLOG(1) << "SetUtimes path: " << share_file_path |
| << " failed: " << base::safe_strerror(error); |
| return error; |
| } |
| |
| reply_stat->st_atim = requested_atime; |
| reply_stat->st_mtim = requested_mtime; |
| |
| return 0; |
| } |
| |
| void SmbFilesystem::Open(std::unique_ptr<OpenRequest> request, |
| fuse_ino_t inode, |
| int flags) { |
| samba_thread_.task_runner()->PostTask( |
| FROM_HERE, |
| base::BindOnce(&SmbFilesystem::OpenInternal, base::Unretained(this), |
| std::move(request), inode, flags)); |
| } |
| |
| void SmbFilesystem::OpenInternal(std::unique_ptr<OpenRequest> request, |
| fuse_ino_t inode, |
| int flags) { |
| if (request->IsInterrupted()) { |
| return; |
| } |
| |
| if (inode == FUSE_ROOT_ID) { |
| request->ReplyError(EISDIR); |
| return; |
| } |
| |
| const std::string share_file_path = ShareFilePathFromInode(inode); |
| SMBCFILE* file = nullptr; |
| int error = samba_impl_->OpenFile(share_file_path, flags, 0, &file); |
| if (error) { |
| VLOG(1) << "OpenFile path " << share_file_path |
| << " failed: " << base::safe_strerror(error); |
| request->ReplyError(error); |
| return; |
| } |
| |
| request->ReplyOpen(AddOpenFile(file)); |
| } |
| |
| void SmbFilesystem::Create(std::unique_ptr<CreateRequest> request, |
| fuse_ino_t parent_inode, |
| const std::string& name, |
| mode_t mode, |
| int flags) { |
| samba_thread_.task_runner()->PostTask( |
| FROM_HERE, |
| base::BindOnce(&SmbFilesystem::CreateInternal, base::Unretained(this), |
| std::move(request), parent_inode, name, mode, flags)); |
| } |
| |
| void SmbFilesystem::CreateInternal(std::unique_ptr<CreateRequest> request, |
| fuse_ino_t parent_inode, |
| const std::string& name, |
| mode_t mode, |
| int flags) { |
| if (request->IsInterrupted()) { |
| return; |
| } |
| |
| flags |= O_CREAT; |
| mode &= 0777; |
| |
| const base::FilePath parent_path = inode_map_.GetPath(parent_inode); |
| CHECK(!parent_path.empty()) |
| << "Lookup on invalid parent inode: " << parent_inode; |
| const base::FilePath file_path = parent_path.Append(name); |
| const std::string share_file_path = MakeShareFilePath(file_path); |
| |
| // NOTE: |mode| appears to be ignored by libsmbclient. |
| SMBCFILE* file = nullptr; |
| int error = samba_impl_->OpenFile(share_file_path, flags, mode, &file); |
| if (error) { |
| VLOG(1) << "OpenFile path: " << share_file_path |
| << " failed: " << base::safe_strerror(error); |
| request->ReplyError(error); |
| return; |
| } |
| |
| uint64_t handle = AddOpenFile(file); |
| |
| ino_t inode = inode_map_.IncInodeRef(file_path); |
| struct stat entry_stat = MakeStat(inode, {0}); |
| entry_stat.st_mode = S_IFREG | mode; |
| fuse_entry_param entry = {0}; |
| entry.ino = inode; |
| entry.generation = 1; |
| entry.attr = entry_stat; |
| // Force readers to see coherent user / group permission bits by not caching |
| // stat structure. |
| entry.attr_timeout = 0; |
| entry.entry_timeout = kAttrTimeoutSeconds; |
| request->ReplyCreate(entry, handle); |
| } |
| |
| void SmbFilesystem::Read(std::unique_ptr<BufRequest> request, |
| fuse_ino_t inode, |
| uint64_t file_handle, |
| size_t size, |
| off_t offset) { |
| samba_thread_.task_runner()->PostTask( |
| FROM_HERE, |
| base::BindOnce(&SmbFilesystem::ReadInternal, base::Unretained(this), |
| std::move(request), inode, file_handle, size, offset)); |
| } |
| |
| void SmbFilesystem::ReadInternal(std::unique_ptr<BufRequest> request, |
| fuse_ino_t inode, |
| uint64_t file_handle, |
| size_t size, |
| off_t offset) { |
| if (request->IsInterrupted()) { |
| return; |
| } |
| |
| SMBCFILE* file = LookupOpenFile(file_handle); |
| if (!file) { |
| request->ReplyError(EBADF); |
| return; |
| } |
| |
| int error = samba_impl_->SeekFile(file, offset, SEEK_SET); |
| if (error) { |
| VLOG(1) << "SeekFile path: " << ShareFilePathFromInode(inode) |
| << ", offset: " << offset |
| << " failed: " << base::safe_strerror(error); |
| request->ReplyError(error); |
| return; |
| } |
| |
| std::vector<char> buf(size); |
| size_t bytes_read = 0; |
| error = samba_impl_->ReadFile(file, buf.data(), size, &bytes_read); |
| if (error) { |
| VLOG(1) << "ReadFile path: " << ShareFilePathFromInode(inode) |
| << " offset: " << offset << ", size: " << size |
| << " failed: " << base::safe_strerror(error); |
| request->ReplyError(error); |
| return; |
| } |
| |
| request->ReplyBuf(buf.data(), bytes_read); |
| } |
| |
| void SmbFilesystem::Write(std::unique_ptr<WriteRequest> request, |
| fuse_ino_t inode, |
| uint64_t file_handle, |
| const char* buf, |
| size_t size, |
| off_t offset) { |
| samba_thread_.task_runner()->PostTask( |
| FROM_HERE, |
| base::BindOnce(&SmbFilesystem::WriteInternal, base::Unretained(this), |
| std::move(request), inode, file_handle, |
| std::vector<char>(buf, buf + size), offset)); |
| } |
| |
| void SmbFilesystem::WriteInternal(std::unique_ptr<WriteRequest> request, |
| fuse_ino_t inode, |
| uint64_t file_handle, |
| const std::vector<char>& buf, |
| off_t offset) { |
| if (request->IsInterrupted()) { |
| return; |
| } |
| |
| SMBCFILE* file = LookupOpenFile(file_handle); |
| if (!file) { |
| request->ReplyError(EBADF); |
| return; |
| } |
| |
| int error = samba_impl_->SeekFile(file, offset, SEEK_SET); |
| if (error) { |
| VLOG(1) << "SeekFile path: " << ShareFilePathFromInode(inode) |
| << ", offset: " << offset |
| << " failed: " << base::safe_strerror(error); |
| request->ReplyError(error); |
| return; |
| } |
| |
| size_t bytes_written = 0; |
| error = samba_impl_->WriteFile(file, buf.data(), buf.size(), &bytes_written); |
| if (error) { |
| VLOG(1) << "WriteFile path: " << ShareFilePathFromInode(inode) |
| << " offset: " << offset << ", size: " << buf.size() |
| << " failed: " << base::safe_strerror(error); |
| request->ReplyError(error); |
| return; |
| } |
| |
| // Modifying the file invalidates any cached inode we have. |
| EraseCachedInodeStat(inode); |
| |
| request->ReplyWrite(bytes_written); |
| } |
| |
| void SmbFilesystem::Release(std::unique_ptr<SimpleRequest> request, |
| fuse_ino_t inode, |
| uint64_t file_handle) { |
| samba_thread_.task_runner()->PostTask( |
| FROM_HERE, |
| base::BindOnce(&SmbFilesystem::ReleaseInternal, base::Unretained(this), |
| std::move(request), inode, file_handle)); |
| } |
| |
| void SmbFilesystem::ReleaseInternal(std::unique_ptr<SimpleRequest> request, |
| fuse_ino_t inode, |
| uint64_t file_handle) { |
| if (request->IsInterrupted()) { |
| return; |
| } |
| |
| SMBCFILE* file = LookupOpenFile(file_handle); |
| if (!file) { |
| request->ReplyError(EBADF); |
| return; |
| } |
| |
| int error = samba_impl_->CloseFile(file); |
| if (error) { |
| request->ReplyError(error); |
| return; |
| } |
| |
| RemoveOpenFile(file_handle); |
| request->ReplyOk(); |
| } |
| |
| void SmbFilesystem::Rename(std::unique_ptr<SimpleRequest> request, |
| fuse_ino_t old_parent_inode, |
| const std::string& old_name, |
| fuse_ino_t new_parent_inode, |
| const std::string& new_name) { |
| samba_thread_.task_runner()->PostTask( |
| FROM_HERE, |
| base::BindOnce(&SmbFilesystem::RenameInternal, base::Unretained(this), |
| std::move(request), old_parent_inode, old_name, |
| new_parent_inode, new_name)); |
| } |
| |
| void SmbFilesystem::RenameInternal(std::unique_ptr<SimpleRequest> request, |
| fuse_ino_t old_parent_inode, |
| const std::string& old_name, |
| fuse_ino_t new_parent_inode, |
| const std::string& new_name) { |
| if (request->IsInterrupted()) { |
| return; |
| } |
| |
| const base::FilePath old_parent_path = inode_map_.GetPath(old_parent_inode); |
| CHECK(!old_parent_path.empty()) |
| << "Lookup on invalid old parent inode: " << old_parent_inode; |
| const base::FilePath new_parent_path = inode_map_.GetPath(new_parent_inode); |
| CHECK(!new_parent_path.empty()) |
| << "Lookup on invalid new parent inode: " << new_parent_inode; |
| |
| const base::FilePath old_path = old_parent_path.Append(old_name); |
| const std::string old_share_path = MakeShareFilePath(old_path); |
| const base::FilePath new_path = new_parent_path.Append(new_name); |
| const std::string new_share_path = MakeShareFilePath(new_path); |
| |
| if (inode_map_.PathExists(new_path)) { |
| // This is posix-violating behaviour since rename() is supposed to replace |
| // new_path if it exists. However, this is currently complicated by the need |
| // to maintain a consistent mapping between inodes and paths. |
| VLOG(1) << "Rename failed since new path already exists, new_path: " |
| << new_share_path; |
| request->ReplyError(EEXIST); |
| return; |
| } |
| |
| int error = samba_impl_->Rename(old_share_path, new_share_path); |
| if (error) { |
| VLOG(1) << "Rename old_path: " << old_share_path |
| << " new_path: " << new_share_path |
| << " failed: " << base::safe_strerror(error); |
| request->ReplyError(error); |
| return; |
| } |
| |
| // A rename only moves the directory entry and doesn't change the underlying |
| // inode. So update our synthesized inode to point to the new location. This |
| // is safe since there's no support for hardlinks, and we have a 1:1 mapping |
| // between path and inode. |
| ino_t inode = inode_map_.IncInodeRef(old_path); |
| inode_map_.UpdatePath(inode, new_path); |
| // Unref the inode so we don't go out of sync with the kernel's refcount. |
| inode_map_.Forget(inode, 1); |
| |
| // The SMB server might update attributes for the destination path (eg. |
| // modification time). Invalidate our cached stat and force a fetch from the |
| // server. |
| EraseCachedInodeStat(inode); |
| |
| request->ReplyOk(); |
| } |
| |
| void SmbFilesystem::Unlink(std::unique_ptr<SimpleRequest> request, |
| fuse_ino_t parent_inode, |
| const std::string& name) { |
| samba_thread_.task_runner()->PostTask( |
| FROM_HERE, |
| base::BindOnce(&SmbFilesystem::UnlinkInternal, base::Unretained(this), |
| std::move(request), parent_inode, name)); |
| } |
| |
| void SmbFilesystem::UnlinkInternal(std::unique_ptr<SimpleRequest> request, |
| fuse_ino_t parent_inode, |
| const std::string& name) { |
| if (request->IsInterrupted()) { |
| return; |
| } |
| |
| const base::FilePath parent_path = inode_map_.GetPath(parent_inode); |
| CHECK(!parent_path.empty()) |
| << "Lookup on invalid parent inode: " << parent_inode; |
| const std::string share_file_path = |
| MakeShareFilePath(parent_path.Append(name)); |
| |
| int error = samba_impl_->UnlinkFile(share_file_path); |
| if (error) { |
| VLOG(1) << "Unlink path: " << share_file_path |
| << " failed: " << base::safe_strerror(error); |
| request->ReplyError(error); |
| return; |
| } |
| |
| request->ReplyOk(); |
| } |
| |
| void SmbFilesystem::OpenDir(std::unique_ptr<OpenRequest> request, |
| fuse_ino_t inode, |
| int flags) { |
| samba_thread_.task_runner()->PostTask( |
| FROM_HERE, |
| base::BindOnce(&SmbFilesystem::OpenDirInternal, base::Unretained(this), |
| std::move(request), inode, flags)); |
| } |
| |
| void SmbFilesystem::OpenDirInternal(std::unique_ptr<OpenRequest> request, |
| fuse_ino_t inode, |
| int flags) { |
| if (request->IsInterrupted()) { |
| return; |
| } |
| |
| if ((flags & O_ACCMODE) != O_RDONLY) { |
| request->ReplyError(EACCES); |
| return; |
| } |
| |
| const std::string share_dir_path = ShareFilePathFromInode(inode); |
| SMBCFILE* dir = nullptr; |
| int error = samba_impl_->OpenDirectory(share_dir_path, &dir); |
| if (error) { |
| VLOG(1) << "OpenDirectory path: " << share_dir_path |
| << " failed: " << base::safe_strerror(error); |
| request->ReplyError(error); |
| return; |
| } |
| |
| request->ReplyOpen(AddOpenFile(dir)); |
| } |
| |
| void SmbFilesystem::ReadDir(std::unique_ptr<DirentryRequest> request, |
| fuse_ino_t inode, |
| uint64_t file_handle, |
| off_t offset) { |
| samba_thread_.task_runner()->PostTask( |
| FROM_HERE, |
| base::BindOnce(&SmbFilesystem::ReadDirInternal, base::Unretained(this), |
| std::move(request), inode, file_handle, offset)); |
| } |
| |
| void SmbFilesystem::ReadDirInternal(std::unique_ptr<DirentryRequest> request, |
| fuse_ino_t inode, |
| uint64_t file_handle, |
| off_t offset) { |
| if (request->IsInterrupted()) { |
| return; |
| } |
| |
| SMBCFILE* dir = LookupOpenFile(file_handle); |
| if (!dir) { |
| request->ReplyError(EBADF); |
| return; |
| } |
| const base::FilePath dir_path = inode_map_.GetPath(inode); |
| CHECK(!dir_path.empty()) << "Inode not found: " << inode; |
| |
| int error = samba_impl_->SeekDirectory(dir, offset); |
| if (error) { |
| VLOG(1) << "SeekDirectory path: " << dir_path.value() |
| << ", offset: " << offset |
| << " failed: " << base::safe_strerror(error); |
| request->ReplyError(error); |
| return; |
| } |
| |
| while (true) { |
| const struct libsmb_file_info* dirent_info = nullptr; |
| struct stat inode_stat = {0}; |
| |
| error = samba_impl_->ReadDirectory(dir, &dirent_info, &inode_stat); |
| if (error) { |
| VLOG(1) << "ReadDirectory path: " << dir_path.value() |
| << " failed: " << base::safe_strerror(error); |
| request->ReplyError(error); |
| return; |
| } |
| if (!dirent_info) { |
| // EOF. |
| break; |
| } |
| off_t next_offset = 0; |
| error = samba_impl_->TellDirectory(dir, &next_offset); |
| if (error) { |
| VLOG(1) << "TellDirectory path: " << dir_path.value() |
| << " failed: " << base::safe_strerror(error); |
| request->ReplyError(error); |
| return; |
| } |
| |
| base::StringPiece filename(dirent_info->name); |
| if (filename == "." || filename == "..") { |
| // Ignore . and .. since FUSE already takes care of these. |
| continue; |
| } |
| CHECK(!filename.empty()); |
| CHECK_EQ(filename.find("/"), base::StringPiece::npos); |
| |
| // Ensure mode bits are appropriately cleaned and propagated. |
| inode_stat.st_mode = MakeStatModeBits(inode_stat.st_mode); |
| |
| const base::FilePath entry_path = dir_path.Append(filename); |
| ino_t entry_inode = inode_map_.GetWeakInode(entry_path); |
| if (!request->AddEntry(filename, entry_inode, inode_stat.st_mode, |
| next_offset)) { |
| // Response buffer full. |
| break; |
| } |
| |
| inode_stat = MakeStat(entry_inode, inode_stat); |
| AddCachedInodeStat(inode_stat); |
| } |
| |
| request->ReplyDone(); |
| } |
| |
| void SmbFilesystem::ReleaseDir(std::unique_ptr<SimpleRequest> request, |
| fuse_ino_t inode, |
| uint64_t file_handle) { |
| samba_thread_.task_runner()->PostTask( |
| FROM_HERE, |
| base::BindOnce(&SmbFilesystem::ReleaseDirInternal, base::Unretained(this), |
| std::move(request), inode, file_handle)); |
| } |
| |
| void SmbFilesystem::ReleaseDirInternal(std::unique_ptr<SimpleRequest> request, |
| fuse_ino_t inode, |
| uint64_t file_handle) { |
| if (request->IsInterrupted()) { |
| return; |
| } |
| |
| SMBCFILE* dir = LookupOpenFile(file_handle); |
| if (!dir) { |
| request->ReplyError(EBADF); |
| return; |
| } |
| |
| int error = samba_impl_->CloseDirectory(dir); |
| if (error) { |
| VLOG(1) << "CloseDirectory failed: " << base::safe_strerror(error); |
| request->ReplyError(error); |
| return; |
| } |
| |
| RemoveOpenFile(file_handle); |
| request->ReplyOk(); |
| } |
| |
| void SmbFilesystem::MkDir(std::unique_ptr<EntryRequest> request, |
| fuse_ino_t parent_inode, |
| const std::string& name, |
| mode_t mode) { |
| samba_thread_.task_runner()->PostTask( |
| FROM_HERE, |
| base::BindOnce(&SmbFilesystem::MkDirInternal, base::Unretained(this), |
| std::move(request), parent_inode, name, mode)); |
| } |
| |
| void SmbFilesystem::MkDirInternal(std::unique_ptr<EntryRequest> request, |
| fuse_ino_t parent_inode, |
| const std::string& name, |
| mode_t mode) { |
| if (request->IsInterrupted()) { |
| return; |
| } |
| |
| const base::FilePath parent_path = inode_map_.GetPath(parent_inode); |
| CHECK(!parent_path.empty()) |
| << "Lookup on invalid parent inode: " << parent_inode; |
| const base::FilePath file_path = parent_path.Append(name); |
| const std::string share_file_path = MakeShareFilePath(file_path); |
| |
| int error = samba_impl_->CreateDirectory(share_file_path, mode); |
| if (error) { |
| VLOG(1) << "CreateDirectory path: " << share_file_path |
| << " failed: " << base::safe_strerror(error); |
| request->ReplyError(error); |
| return; |
| } |
| |
| ino_t inode = inode_map_.IncInodeRef(file_path); |
| struct stat entry_stat = MakeStat(inode, {0}); |
| entry_stat.st_mode = S_IFDIR | mode; |
| fuse_entry_param entry = {0}; |
| entry.ino = inode; |
| entry.generation = 1; |
| entry.attr = entry_stat; |
| // Force readers to see coherent user / group permission bits by not caching |
| // stat structure. |
| entry.attr_timeout = 0; |
| entry.entry_timeout = kAttrTimeoutSeconds; |
| request->ReplyEntry(entry); |
| } |
| |
| void SmbFilesystem::RmDir(std::unique_ptr<SimpleRequest> request, |
| fuse_ino_t parent_inode, |
| const std::string& name) { |
| samba_thread_.task_runner()->PostTask( |
| FROM_HERE, |
| base::BindOnce(&SmbFilesystem::RmDirinternal, base::Unretained(this), |
| std::move(request), parent_inode, name)); |
| } |
| |
| void SmbFilesystem::RmDirinternal(std::unique_ptr<SimpleRequest> request, |
| fuse_ino_t parent_inode, |
| const std::string& name) { |
| if (request->IsInterrupted()) { |
| return; |
| } |
| |
| const base::FilePath parent_path = inode_map_.GetPath(parent_inode); |
| CHECK(!parent_path.empty()) |
| << "Lookup on invalid parent inode: " << parent_inode; |
| const base::FilePath file_path = parent_path.Append(name); |
| const std::string share_file_path = MakeShareFilePath(file_path); |
| |
| int error = samba_impl_->RemoveDirectory(share_file_path); |
| if (error) { |
| VLOG(1) << "RemoveDirectory path: " << share_file_path |
| << " failed: " << base::safe_strerror(error); |
| request->ReplyError(error); |
| return; |
| } |
| |
| request->ReplyOk(); |
| } |
| |
| void SmbFilesystem::DeleteRecursively( |
| const base::FilePath& path, |
| RecursiveDeleteOperation::CompletionCallback callback) { |
| samba_thread_.task_runner()->PostTask( |
| FROM_HERE, |
| base::BindOnce(&SmbFilesystem::DeleteRecursivelyInternal, |
| base::Unretained(this), path, std::move(callback))); |
| } |
| |
| void SmbFilesystem::DeleteRecursivelyInternal( |
| const base::FilePath& path, |
| RecursiveDeleteOperation::CompletionCallback callback) { |
| if (recursive_delete_operation_) { |
| VLOG(1) |
| << "Can't start a recursive delete operation whilst one is in progress"; |
| main_task_runner_->PostTask( |
| FROM_HERE, |
| base::BindOnce(std::move(callback), |
| mojom::DeleteRecursivelyError::kOperationInProgress)); |
| return; |
| } |
| |
| recursive_delete_operation_.reset(new RecursiveDeleteOperation( |
| samba_impl_.get(), MakeShareFilePath(base::FilePath("/")), path, |
| base::BindOnce(&SmbFilesystem::OnDeleteRecursivelyDone, |
| base::Unretained(this), std::move(callback)))); |
| recursive_delete_operation_->Start(); |
| } |
| |
| void SmbFilesystem::OnDeleteRecursivelyDone( |
| RecursiveDeleteOperation::CompletionCallback callback, |
| mojom::DeleteRecursivelyError error) { |
| CHECK(recursive_delete_operation_); |
| recursive_delete_operation_.reset(); |
| main_task_runner_->PostTask(FROM_HERE, |
| base::BindOnce(std::move(callback), error)); |
| } |
| |
| std::ostream& operator<<(std::ostream& out, SmbFilesystem::ConnectError error) { |
| switch (error) { |
| case SmbFilesystem::ConnectError::kOk: |
| return out << "kOk"; |
| case SmbFilesystem::ConnectError::kNotFound: |
| return out << "kNotFound"; |
| case SmbFilesystem::ConnectError::kAccessDenied: |
| return out << "kAccessDenied"; |
| case SmbFilesystem::ConnectError::kSmb1Unsupported: |
| return out << "kSmb1Unsupported"; |
| case SmbFilesystem::ConnectError::kUnknownError: |
| return out << "kUnknownError"; |
| default: |
| NOTREACHED(); |
| return out << "INVALID_ERROR"; |
| } |
| } |
| |
| void SmbFilesystem::AddCachedInodeStat(const struct stat& inode_stat) { |
| DCHECK(inode_stat.st_ino); |
| |
| StatCacheItem item; |
| |
| item.inode_stat = inode_stat; |
| item.expires_at = base::Time::Now() + |
| base::TimeDelta::FromSecondsD(kStatCacheTimeoutSeconds); |
| |
| stat_cache_.Put(inode_stat.st_ino, item); |
| } |
| |
| void SmbFilesystem::EraseCachedInodeStat(ino_t inode) { |
| auto iter = stat_cache_.Peek(inode); |
| if (iter != stat_cache_.end()) { |
| stat_cache_.Erase(iter); |
| } |
| } |
| |
| bool SmbFilesystem::GetCachedInodeStat(ino_t inode, struct stat* out_stat) { |
| DCHECK(out_stat); |
| auto iter = stat_cache_.Get(inode); |
| if (iter == stat_cache_.end()) { |
| return false; |
| } |
| |
| StatCacheItem item = iter->second; |
| if (item.expires_at < base::Time::Now()) { |
| stat_cache_.Erase(iter); |
| return false; |
| } |
| |
| *out_stat = item.inode_stat; |
| return true; |
| } |
| |
| } // namespace smbfs |