blob: 2f9e39dc10dde2a7579e8147595798da4e18d1f6 [file] [log] [blame]
// 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