/* * Copyright (C) 2019 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #define LOG_TAG "incfs" #include "incfs.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "MountRegistry.h" #include "path.h" using namespace std::literals; using namespace android::incfs; using namespace android::sysprop; namespace ab = android::base; struct IncFsControl final { IncFsFd cmd; IncFsFd pendingReads; IncFsFd logs; IncFsFd blocksWritten; constexpr IncFsControl(IncFsFd cmd, IncFsFd pendingReads, IncFsFd logs, IncFsFd blocksWritten) : cmd(cmd), pendingReads(pendingReads), logs(logs), blocksWritten(blocksWritten) {} }; static MountRegistry& registry() { static ab::NoDestructor instance{}; return *instance; } static ab::unique_fd openRaw(std::string_view file) { auto fd = ab::unique_fd(::open(details::c_str(file), O_RDONLY | O_CLOEXEC)); if (fd < 0) { return ab::unique_fd{-errno}; } return fd; } static ab::unique_fd openAt(int fd, std::string_view name, int flags = 0) { auto res = ab::unique_fd( ::openat(fd, details::c_str(name), O_RDONLY | O_CLOEXEC | O_NOFOLLOW | flags)); if (res < 0) { return ab::unique_fd{-errno}; } return res; } static std::string indexPath(std::string_view root, IncFsFileId fileId) { return path::join(root, INCFS_INDEX_NAME, toString(fileId)); } static std::string rootForCmd(int fd) { auto cmdFile = path::fromFd(fd); if (cmdFile.empty()) { LOG(INFO) << __func__ << "(): name empty for " << fd; return {}; } auto res = path::dirName(cmdFile); if (res.empty()) { LOG(INFO) << __func__ << "(): dirname empty for " << cmdFile; return {}; } if (!path::endsWith(cmdFile, INCFS_PENDING_READS_FILENAME)) { LOG(INFO) << __func__ << "(): invalid file name " << cmdFile; return {}; } if (cmdFile.data() == res.data() || cmdFile.starts_with(res)) { cmdFile.resize(res.size()); return cmdFile; } return std::string(res); } static bool isFsAvailable() { static const char kProcFilesystems[] = "/proc/filesystems"; std::string filesystems; if (!ab::ReadFileToString(kProcFilesystems, &filesystems)) { return false; } const auto result = filesystems.find("\t" INCFS_NAME "\n") != std::string::npos; LOG(INFO) << "isFsAvailable: " << (result ? "true" : "false"); return result; } static int getFirstApiLevel() { uint64_t api_level = android::base::GetUintProperty("ro.product.first_api_level", 0); LOG(INFO) << "Initial API level of the device: " << api_level; return api_level; } static std::string_view incFsPropertyValue() { constexpr const int R_API = 30; static const auto kDefaultValue{getFirstApiLevel() > R_API ? "on" : ""}; static const ab::NoDestructor kValue{ IncrementalProperties::enable().value_or(kDefaultValue)}; LOG(INFO) << "ro.incremental.enable: " << *kValue; return *kValue; } static std::pair parseProperty(std::string_view property) { auto boolVal = ab::ParseBool(property); if (boolVal == ab::ParseBoolResult::kTrue) { return {isFsAvailable(), {}}; } if (boolVal == ab::ParseBoolResult::kFalse) { return {false, {}}; } // Don't load the module at once, but instead only check if it is loadable. static const auto kModulePrefix = "module:"sv; if (property.starts_with(kModulePrefix)) { const auto modulePath = property.substr(kModulePrefix.size()); return {::access(details::c_str(modulePath), R_OK | X_OK), modulePath}; } return {false, {}}; } template static IncFsErrorCode forEachFileIn(std::string_view dirPath, Callback cb) { auto dir = path::openDir(details::c_str(dirPath)); if (!dir) { return -EINVAL; } int res = 0; while (auto entry = (errno = 0, ::readdir(dir.get()))) { if (entry->d_type != DT_REG) { continue; } ++res; if (!cb(entry->d_name)) { break; } } if (errno) { return -errno; } return res; } namespace { class IncFsInit { public: IncFsInit() { auto [featureEnabled, moduleName] = parseProperty(incFsPropertyValue()); featureEnabled_ = featureEnabled; moduleName_ = moduleName; loaded_ = featureEnabled_ && isFsAvailable(); } constexpr ~IncFsInit() = default; bool enabled() const { return featureEnabled_; } bool enabledAndReady() const { if (!featureEnabled_) { return false; } if (moduleName_.empty()) { return true; } if (loaded_) { return true; } std::call_once(loadedFlag_, [this] { if (isFsAvailable()) { // Loaded from a different process, I suppose. loaded_ = true; LOG(INFO) << "IncFS is already available, skipped loading"; return; } const ab::unique_fd fd(TEMP_FAILURE_RETRY( ::open(details::c_str(moduleName_), O_RDONLY | O_NOFOLLOW | O_CLOEXEC))); if (fd < 0) { PLOG(ERROR) << "could not open IncFs kernel module \"" << moduleName_ << '"'; return; } const auto rc = syscall(__NR_finit_module, fd.get(), "", 0); if (rc < 0) { PLOG(ERROR) << "finit_module for IncFs \"" << moduleName_ << "\" failed"; return; } if (!isFsAvailable()) { LOG(ERROR) << "loaded IncFs kernel module \"" << moduleName_ << "\" but incremental-fs is still not available"; } loaded_ = true; LOG(INFO) << "successfully loaded IncFs kernel module \"" << moduleName_ << '"'; }); return loaded_; } private: bool featureEnabled_; std::string_view moduleName_; mutable std::once_flag loadedFlag_; mutable bool loaded_; }; } // namespace static IncFsInit& init() { static IncFsInit initer; return initer; } bool IncFs_IsEnabled() { return init().enabled(); } static Features readIncFsFeatures() { init().enabledAndReady(); int res = Features::none | Features::mappingFilesProgressFixed; static const char kSysfsFeaturesDir[] = "/sys/fs/" INCFS_NAME "/features"; const auto dir = path::openDir(kSysfsFeaturesDir); if (!dir) { PLOG(ERROR) << "IncFs_Features: failed to open features dir, assuming v1/none."; return Features(res); } while (auto entry = ::readdir(dir.get())) { if (entry->d_type != DT_REG) { continue; } if (entry->d_name == "corefs"sv) { res |= Features::core; } else if (entry->d_name == "v2"sv || entry->d_name == "report_uid"sv) { res |= Features::v2; } } LOG(INFO) << "IncFs_Features: " << ((res & Features::v2) ? "v2" : "v1"); return Features(res); } IncFsFeatures IncFs_Features() { static const auto features = IncFsFeatures(readIncFsFeatures()); return features; } bool isIncFsFdImpl(int fd) { struct statfs fs = {}; if (::fstatfs(fd, &fs) != 0) { PLOG(WARNING) << __func__ << "(): could not fstatfs fd " << fd; return false; } return fs.f_type == (decltype(fs.f_type))INCFS_MAGIC_NUMBER; } bool isIncFsPathImpl(const char* path) { struct statfs fs = {}; if (::statfs(path, &fs) != 0) { PLOG(WARNING) << __func__ << "(): could not statfs " << path; return false; } return fs.f_type == (decltype(fs.f_type))INCFS_MAGIC_NUMBER; } static int isDir(const char* path) { struct stat st; if (::stat(path, &st) != 0) { return -errno; } if (!S_ISDIR(st.st_mode)) { return -ENOTDIR; } return 0; } static bool isAbsolute(const char* path) { return path && path[0] == '/'; } static int isValidMountTarget(const char* path) { if (!isAbsolute(path)) { return -EINVAL; } if (isIncFsPath(path)) { LOG(ERROR) << "[incfs] mounting over existing incfs mount is not allowed"; return -EINVAL; } if (const auto err = isDir(path); err != 0) { return err; } if (const auto err = path::isEmptyDir(path); err != 0) { return err; } return 0; } static int rmDirContent(int dirFd) { auto dir = path::openDir(dirFd); if (!dir) { return -errno; } while (auto entry = ::readdir(dir.get())) { if (entry->d_name == "."sv || entry->d_name == ".."sv) { continue; } if (entry->d_type == DT_DIR) { auto fd = openAt(dirFd, entry->d_name, O_DIRECTORY); if (!fd.ok()) { return -errno; } if (const auto err = rmDirContent(fd.get())) { return err; } if (::unlinkat(fd.get(), entry->d_name, AT_REMOVEDIR)) { return -errno; } } else { auto fd = openAt(dirFd, entry->d_name); if (!fd.ok()) { return -errno; } if (::unlinkat(fd.get(), entry->d_name, 0)) { return -errno; } } } return 0; } static int rmDirContent(const char* path) { auto fd = openAt(-1, path, O_DIRECTORY); if (!fd.ok()) { return -errno; } return rmDirContent(fd.get()); } static std::string makeMountOptionsString(IncFsMountOptions options) { auto opts = ab::StringPrintf("read_timeout_ms=%u,readahead=0,rlog_pages=%u,rlog_wakeup_cnt=1,", unsigned(options.defaultReadTimeoutMs), unsigned(options.readLogBufferPages < 0 ? INCFS_DEFAULT_PAGE_READ_BUFFER_PAGES : options.readLogBufferPages)); if (features() & Features::v2) { ab::StringAppendF(&opts, "report_uid,"); if (options.sysfsName && *options.sysfsName) { ab::StringAppendF(&opts, "sysfs_name=%s,", options.sysfsName); } } return opts; } static IncFsControl* makeControl(int fd) { auto cmd = openAt(fd, INCFS_PENDING_READS_FILENAME); if (!cmd.ok()) { return nullptr; } ab::unique_fd pendingReads(fcntl(cmd.get(), F_DUPFD_CLOEXEC, cmd.get())); if (!pendingReads.ok()) { return nullptr; } auto logs = openAt(fd, INCFS_LOG_FILENAME); if (!logs.ok()) { return nullptr; } ab::unique_fd blocksWritten; if (features() & Features::v2) { blocksWritten = openAt(fd, INCFS_BLOCKS_WRITTEN_FILENAME); if (!blocksWritten.ok()) { return nullptr; } } auto control = IncFs_CreateControl(cmd.get(), pendingReads.get(), logs.get(), blocksWritten.get()); if (control) { (void)cmd.release(); (void)pendingReads.release(); (void)logs.release(); (void)blocksWritten.release(); } else { errno = ENOMEM; } return control; } static std::string makeCommandPath(std::string_view root, std::string_view item) { auto [itemRoot, subpath] = registry().rootAndSubpathFor(item); if (itemRoot != root) { return {}; } // TODO: add "/.cmd/" if we decide to use a separate control tree. return path::join(itemRoot, subpath); } static void toString(IncFsFileId id, char* out) { // Make sure this function matches the one in the kernel (e.g. same case for a-f digits). static constexpr char kHexChar[] = "0123456789abcdef"; for (auto item = std::begin(id.data); item != std::end(id.data); ++item, out += 2) { out[0] = kHexChar[(*item & 0xf0) >> 4]; out[1] = kHexChar[(*item & 0x0f)]; } } static std::string toStringImpl(IncFsFileId id) { std::string res(kIncFsFileIdStringLength, '\0'); toString(id, res.data()); return res; } static IncFsFileId toFileIdImpl(std::string_view str) { if (str.size() != kIncFsFileIdStringLength) { return kIncFsInvalidFileId; } IncFsFileId res; auto out = (char*)&res; for (auto it = str.begin(); it != str.end(); it += 2, ++out) { static const auto fromChar = [](char src) -> int { if (src >= '0' && src <= '9') { return src - '0'; } if (src >= 'a' && src <= 'f') { return src - 'a' + 10; } return -1; }; const int c[2] = {fromChar(it[0]), fromChar(it[1])}; if (c[0] == -1 || c[1] == -1) { errno = EINVAL; return kIncFsInvalidFileId; } *out = (c[0] << 4) | c[1]; } return res; } int IncFs_FileIdToString(IncFsFileId id, char* out) { if (!out) { return -EINVAL; } toString(id, out); return 0; } IncFsFileId IncFs_FileIdFromString(const char* in) { return toFileIdImpl({in, kIncFsFileIdStringLength}); } IncFsFileId IncFs_FileIdFromMetadata(IncFsSpan metadata) { IncFsFileId id = {}; if (size_t(metadata.size) <= sizeof(id)) { memcpy(&id, metadata.data, metadata.size); } else { uint8_t buffer[SHA_DIGEST_LENGTH]; static_assert(sizeof(buffer) >= sizeof(id)); SHA_CTX ctx; SHA1_Init(&ctx); SHA1_Update(&ctx, metadata.data, metadata.size); SHA1_Final(buffer, &ctx); memcpy(&id, buffer, sizeof(id)); } return id; } static bool restoreconControlFiles(std::string_view targetDir) { static constexpr auto restorecon = [](const char* name) { if (const auto err = selinux_android_restorecon(name, SELINUX_ANDROID_RESTORECON_FORCE); err != 0) { errno = -err; PLOG(ERROR) << "[incfs] Failed to restorecon: " << name; return false; } return true; }; if (!restorecon(path::join(targetDir, INCFS_PENDING_READS_FILENAME).c_str())) { return false; } if (!restorecon(path::join(targetDir, INCFS_LOG_FILENAME).c_str())) { return false; } if ((features() & Features::v2) && !restorecon(path::join(targetDir, INCFS_BLOCKS_WRITTEN_FILENAME).c_str())) { return false; } return true; } IncFsControl* IncFs_Mount(const char* backingPath, const char* targetDir, IncFsMountOptions options) { if (!init().enabledAndReady()) { LOG(WARNING) << "[incfs] Feature is not enabled"; errno = ENOTSUP; return nullptr; } if (auto err = isValidMountTarget(targetDir); err != 0) { errno = -err; return nullptr; } if (!isAbsolute(backingPath)) { errno = EINVAL; return nullptr; } if (options.flags & createOnly) { if (const auto err = path::isEmptyDir(backingPath); err != 0) { errno = -err; return nullptr; } } else if (options.flags & android::incfs::truncate) { if (const auto err = rmDirContent(backingPath); err != 0) { errno = -err; return nullptr; } } const auto opts = makeMountOptionsString(options); if (::mount(backingPath, targetDir, INCFS_NAME, MS_NOSUID | MS_NODEV | MS_NOATIME, opts.c_str())) { PLOG(ERROR) << "[incfs] Failed to mount IncFS filesystem: " << targetDir; return nullptr; } // in case when the path is given in a form of a /proc/.../fd/ link, we need to update // it here: old fd refers to the original empty directory, not to the mount std::string updatedTargetDir; if (path::dirName(targetDir) == path::procfsFdDir) { updatedTargetDir = path::readlink(targetDir); } else { updatedTargetDir = targetDir; } auto rootFd = ab::unique_fd(::open(updatedTargetDir.c_str(), O_PATH | O_CLOEXEC | O_DIRECTORY)); if (updatedTargetDir != targetDir) { // ensure that the new directory is still the same after reopening if (path::fromFd(rootFd) != updatedTargetDir) { errno = EINVAL; return nullptr; } } if (!restoreconControlFiles(path::procfsForFd(rootFd))) { (void)IncFs_Unmount(targetDir); return nullptr; } auto control = makeControl(rootFd); if (control == nullptr) { (void)IncFs_Unmount(targetDir); return nullptr; } return control; } IncFsControl* IncFs_Open(const char* dir) { auto root = registry().rootFor(dir); if (root.empty()) { errno = EINVAL; return nullptr; } auto rootFd = ab::unique_fd(::open(details::c_str(root), O_PATH | O_CLOEXEC | O_DIRECTORY)); return makeControl(rootFd); } IncFsFd IncFs_GetControlFd(const IncFsControl* control, IncFsFdType type) { if (!control) { return -EINVAL; } switch (type) { case CMD: return control->cmd; case PENDING_READS: return control->pendingReads; case LOGS: return control->logs; case BLOCKS_WRITTEN: return control->blocksWritten; default: return -EINVAL; } } IncFsSize IncFs_ReleaseControlFds(IncFsControl* control, IncFsFd out[], IncFsSize outSize) { if (!control || !out) { return -EINVAL; } if (outSize < IncFsFdType::FDS_COUNT) { return -ERANGE; } out[CMD] = std::exchange(control->cmd, -1); out[PENDING_READS] = std::exchange(control->pendingReads, -1); out[LOGS] = std::exchange(control->logs, -1); out[BLOCKS_WRITTEN] = std::exchange(control->blocksWritten, -1); return IncFsFdType::FDS_COUNT; } IncFsControl* IncFs_CreateControl(IncFsFd cmd, IncFsFd pendingReads, IncFsFd logs, IncFsFd blocksWritten) { return new IncFsControl(cmd, pendingReads, logs, blocksWritten); } void IncFs_DeleteControl(IncFsControl* control) { if (control) { if (control->cmd >= 0) { close(control->cmd); } if (control->pendingReads >= 0) { close(control->pendingReads); } if (control->logs >= 0) { close(control->logs); } if (control->blocksWritten >= 0) { close(control->blocksWritten); } delete control; } } IncFsErrorCode IncFs_SetOptions(const IncFsControl* control, IncFsMountOptions options) { if (!control) { return -EINVAL; } auto root = rootForCmd(control->cmd); if (root.empty()) { return -EINVAL; } auto opts = makeMountOptionsString(options); if (::mount(nullptr, root.c_str(), nullptr, MS_REMOUNT | MS_NOSUID | MS_NODEV | MS_NOATIME, opts.c_str()) != 0) { const auto error = errno; PLOG(ERROR) << "[incfs] Failed to remount IncFS filesystem: " << root; return -error; } return 0; } IncFsErrorCode IncFs_Root(const IncFsControl* control, char buffer[], size_t* bufferSize) { if (!control) { return -EINVAL; } std::string result = rootForCmd(control->cmd); if (*bufferSize <= result.size()) { *bufferSize = result.size() + 1; return -EOVERFLOW; } result.copy(buffer, result.size()); buffer[result.size()] = '\0'; *bufferSize = result.size(); return 0; } template std::optional read(IncFsSpan& data) { if (data.size < (int32_t)sizeof(T)) { return {}; } T res; memcpy(&res, data.data, sizeof(res)); data.data += sizeof(res); data.size -= sizeof(res); return res; } static IncFsErrorCode validateSignatureFormat(IncFsSpan signature) { if (signature.data == nullptr && signature.size == 0) { return 0; // it's fine to have unverified files too } if ((signature.data == nullptr) != (signature.size == 0)) { return -EINVAL; } // These structs are here purely for checking the minimum size. Maybe will use them for // parsing later. struct __attribute__((packed)) Hashing { int32_t size; int32_t algorithm; int8_t log2_blocksize; int32_t salt_size; int32_t raw_root_hash_size; }; struct __attribute__((packed)) Signing { int32_t size; int32_t apk_digest_size; int32_t certificate_size; int32_t addl_data_size; int32_t public_key_size; int32_t algorithm; int32_t signature_size; }; struct __attribute__((packed)) MinSignature { int32_t version; Hashing hashing_info; Signing signing_info; }; if (signature.size < (int32_t)sizeof(MinSignature)) { return -ERANGE; } if (signature.size > INCFS_MAX_SIGNATURE_SIZE) { return -ERANGE; } auto version = read(signature); if (version.value_or(-1) != INCFS_SIGNATURE_VERSION) { return -EINVAL; } auto hashSize = read(signature); if (!hashSize || signature.size < *hashSize) { return -EINVAL; } auto hashAlgo = read(signature); if (hashAlgo.value_or(-1) != INCFS_HASH_TREE_SHA256) { return -EINVAL; } auto logBlockSize = read(signature); if (logBlockSize.value_or(-1) != 12 /* 2^12 == 4096 */) { return -EINVAL; } auto saltSize = read(signature); if (saltSize.value_or(-1) != 0) { return -EINVAL; } auto rootHashSize = read(signature); if (rootHashSize.value_or(-1) != INCFS_MAX_HASH_SIZE) { return -EINVAL; } if (signature.size < *rootHashSize) { return -EINVAL; } signature.data += *rootHashSize; signature.size -= *rootHashSize; auto signingSize = read(signature); // everything remaining has to be in the signing info if (signingSize.value_or(-1) != signature.size) { return -EINVAL; } // TODO: validate the signature part too. return 0; } IncFsErrorCode IncFs_MakeFile(const IncFsControl* control, const char* path, int32_t mode, IncFsFileId id, IncFsNewFileParams params) { if (!control) { return -EINVAL; } auto [root, subpath] = registry().rootAndSubpathFor(path); if (root.empty()) { PLOG(WARNING) << "[incfs] makeFile failed for path " << path << ", root is empty."; return -EINVAL; } if (params.size < 0) { LOG(WARNING) << "[incfs] makeFile failed for path " << path << ", size is invalid: " << params.size; return -ERANGE; } const auto [subdir, name] = path::splitDirBase(subpath); incfs_new_file_args args = { .size = (uint64_t)params.size, .mode = (uint16_t)mode, .directory_path = (uint64_t)subdir.data(), .file_name = (uint64_t)name.data(), .file_attr = (uint64_t)params.metadata.data, .file_attr_len = (uint32_t)params.metadata.size, }; static_assert(sizeof(args.file_id.bytes) == sizeof(id.data)); memcpy(args.file_id.bytes, id.data, sizeof(args.file_id.bytes)); if (auto err = validateSignatureFormat(params.signature)) { return err; } args.signature_info = (uint64_t)(uintptr_t)params.signature.data; args.signature_size = (uint64_t)params.signature.size; if (::ioctl(control->cmd, INCFS_IOC_CREATE_FILE, &args)) { PLOG(WARNING) << "[incfs] makeFile failed for " << root << " / " << subdir << " / " << name << " of " << params.size << " bytes"; return -errno; } if (::chmod(path::join(root, subdir, name).c_str(), mode)) { PLOG(WARNING) << "[incfs] couldn't change file mode to 0" << std::oct << mode; } return 0; } IncFsErrorCode IncFs_MakeMappedFile(const IncFsControl* control, const char* path, int32_t mode, IncFsNewMappedFileParams params) { if (!control) { return -EINVAL; } auto [root, subpath] = registry().rootAndSubpathFor(path); if (root.empty()) { PLOG(WARNING) << "[incfs] makeMappedFile failed for path " << path << ", root is empty."; return -EINVAL; } if (params.size < 0) { LOG(WARNING) << "[incfs] makeMappedFile failed for path " << path << ", size is invalid: " << params.size; return -ERANGE; } const auto [subdir, name] = path::splitDirBase(subpath); incfs_create_mapped_file_args args = { .size = (uint64_t)params.size, .mode = (uint16_t)mode, .directory_path = (uint64_t)subdir.data(), .file_name = (uint64_t)name.data(), .source_offset = (uint64_t)params.sourceOffset, }; static_assert(sizeof(args.source_file_id.bytes) == sizeof(params.sourceId.data)); memcpy(args.source_file_id.bytes, params.sourceId.data, sizeof(args.source_file_id.bytes)); if (::ioctl(control->cmd, INCFS_IOC_CREATE_MAPPED_FILE, &args)) { PLOG(WARNING) << "[incfs] makeMappedFile failed for " << root << " / " << subdir << " / " << name << " of " << params.size << " bytes starting at " << params.sourceOffset; return -errno; } if (::chmod(path::join(root, subpath).c_str(), mode)) { PLOG(WARNING) << "[incfs] makeMappedFile error: couldn't change file mode to 0" << std::oct << mode; } return 0; } static IncFsErrorCode makeDir(const char* commandPath, int32_t mode, bool allowExisting) { if (!::mkdir(commandPath, mode)) { if (::chmod(commandPath, mode)) { PLOG(WARNING) << "[incfs] couldn't change directory mode to 0" << std::oct << mode; } return 0; } // don't touch the existing dir's mode - mkdir(1) works that way. return (allowExisting && errno == EEXIST) ? 0 : -errno; } static IncFsErrorCode makeDirs(std::string_view commandPath, std::string_view path, std::string_view root, int32_t mode) { auto commandCPath = details::c_str(commandPath); const auto mkdirRes = makeDir(commandCPath, mode, true); if (!mkdirRes) { return 0; } if (mkdirRes != -ENOENT) { LOG(ERROR) << __func__ << "(): mkdir failed for " << path << " - " << mkdirRes; return mkdirRes; } const auto parent = path::dirName(commandPath); if (!path::startsWith(parent, root)) { // went too far, already out of the root mount return -EINVAL; } if (auto parentMkdirRes = makeDirs(parent, path::dirName(path), root, mode)) { return parentMkdirRes; } return makeDir(commandCPath, mode, true); } IncFsErrorCode IncFs_MakeDir(const IncFsControl* control, const char* path, int32_t mode) { if (!control) { return -EINVAL; } const auto root = rootForCmd(control->cmd); if (root.empty()) { LOG(ERROR) << __func__ << "(): root is empty for " << path; return -EINVAL; } auto commandPath = makeCommandPath(root, path); if (commandPath.empty()) { LOG(ERROR) << __func__ << "(): commandPath is empty for " << path; return -EINVAL; } if (auto res = makeDir(commandPath.c_str(), mode, false)) { LOG(ERROR) << __func__ << "(): mkdir failed for " << commandPath << " - " << res; return res; } return 0; } IncFsErrorCode IncFs_MakeDirs(const IncFsControl* control, const char* path, int32_t mode) { if (!control) { return -EINVAL; } const auto root = rootForCmd(control->cmd); if (root.empty()) { LOG(ERROR) << __func__ << "(): root is empty for " << path; return -EINVAL; } auto commandPath = makeCommandPath(root, path); if (commandPath.empty()) { LOG(ERROR) << __func__ << "(): commandPath is empty for " << path; return -EINVAL; } return makeDirs(commandPath, path, root, mode); } static IncFsErrorCode getMetadata(const char* path, char buffer[], size_t* bufferSize) { const auto res = ::getxattr(path, kMetadataAttrName, buffer, *bufferSize); if (res < 0) { if (errno == ERANGE) { auto neededSize = ::getxattr(path, kMetadataAttrName, buffer, 0); if (neededSize >= 0) { *bufferSize = neededSize; return 0; } } return -errno; } *bufferSize = res; return 0; } IncFsErrorCode IncFs_GetMetadataById(const IncFsControl* control, IncFsFileId fileId, char buffer[], size_t* bufferSize) { if (!control) { return -EINVAL; } const auto root = rootForCmd(control->cmd); if (root.empty()) { return -EINVAL; } auto name = indexPath(root, fileId); return getMetadata(details::c_str(name), buffer, bufferSize); } IncFsErrorCode IncFs_GetMetadataByPath(const IncFsControl* control, const char* path, char buffer[], size_t* bufferSize) { if (!control) { return -EINVAL; } const auto pathRoot = registry().rootFor(path); const auto root = rootForCmd(control->cmd); if (root.empty() || root != pathRoot) { return -EINVAL; } return getMetadata(path, buffer, bufferSize); } template static IncFsFileId getId(GetterFunc getter, Param param) { char buffer[kIncFsFileIdStringLength]; const auto res = getter(param, kIdAttrName, buffer, sizeof(buffer)); if (res != sizeof(buffer)) { return kIncFsInvalidFileId; } return toFileIdImpl({buffer, std::size(buffer)}); } IncFsFileId IncFs_GetId(const IncFsControl* control, const char* path) { if (!control) { return kIncFsInvalidFileId; } const auto pathRoot = registry().rootFor(path); const auto root = rootForCmd(control->cmd); if (root.empty() || root != pathRoot) { errno = EINVAL; return kIncFsInvalidFileId; } return getId(::getxattr, path); } static IncFsErrorCode getSignature(int fd, char buffer[], size_t* bufferSize) { incfs_get_file_sig_args args = { .file_signature = (uint64_t)buffer, .file_signature_buf_size = (uint32_t)*bufferSize, }; auto res = ::ioctl(fd, INCFS_IOC_READ_FILE_SIGNATURE, &args); if (res < 0) { if (errno == E2BIG) { *bufferSize = INCFS_MAX_SIGNATURE_SIZE; } return -errno; } *bufferSize = args.file_signature_len_out; return 0; } IncFsErrorCode IncFs_GetSignatureById(const IncFsControl* control, IncFsFileId fileId, char buffer[], size_t* bufferSize) { if (!control) { return -EINVAL; } const auto root = rootForCmd(control->cmd); if (root.empty()) { return -EINVAL; } auto file = indexPath(root, fileId); auto fd = openRaw(file); if (fd < 0) { return fd.get(); } return getSignature(fd, buffer, bufferSize); } IncFsErrorCode IncFs_GetSignatureByPath(const IncFsControl* control, const char* path, char buffer[], size_t* bufferSize) { if (!control) { return -EINVAL; } const auto pathRoot = registry().rootFor(path); const auto root = rootForCmd(control->cmd); if (root.empty() || root != pathRoot) { return -EINVAL; } return IncFs_UnsafeGetSignatureByPath(path, buffer, bufferSize); } IncFsErrorCode IncFs_UnsafeGetSignatureByPath(const char* path, char buffer[], size_t* bufferSize) { if (!isIncFsPath(path)) { return -EINVAL; } auto fd = openRaw(path); if (fd < 0) { return fd.get(); } return getSignature(fd, buffer, bufferSize); } IncFsErrorCode IncFs_Link(const IncFsControl* control, const char* fromPath, const char* wherePath) { if (!control) { return -EINVAL; } auto root = rootForCmd(control->cmd); if (root.empty()) { return -EINVAL; } auto cmdFrom = makeCommandPath(root, fromPath); if (cmdFrom.empty()) { return -EINVAL; } auto cmdWhere = makeCommandPath(root, wherePath); if (cmdWhere.empty()) { return -EINVAL; } if (::link(cmdFrom.c_str(), cmdWhere.c_str())) { return -errno; } return 0; } IncFsErrorCode IncFs_Unlink(const IncFsControl* control, const char* path) { if (!control) { return -EINVAL; } auto root = rootForCmd(control->cmd); if (root.empty()) { return -EINVAL; } auto cmdPath = makeCommandPath(root, path); if (cmdPath.empty()) { return -EINVAL; } if (::unlink(cmdPath.c_str())) { if (errno == EISDIR) { if (!::rmdir(cmdPath.c_str())) { return 0; } } return -errno; } return 0; } template static int waitForReadsImpl(int fd, int32_t timeoutMs, RawPendingRead pendingReadsBuffer[], size_t* pendingReadsBufferSize) { using namespace std::chrono; auto hrTimeout = steady_clock::duration(milliseconds(timeoutMs)); while (hrTimeout > hrTimeout.zero() || (!pendingReadsBuffer && hrTimeout == hrTimeout.zero())) { const auto startTs = steady_clock::now(); pollfd pfd = {fd, POLLIN, 0}; const auto res = ::poll(&pfd, 1, duration_cast(hrTimeout).count()); if (res > 0) { break; } if (res == 0) { if (pendingReadsBufferSize) { *pendingReadsBufferSize = 0; } return -ETIMEDOUT; } const auto error = errno; if (error != EINTR) { PLOG(ERROR) << "poll() failed"; return -error; } hrTimeout -= steady_clock::now() - startTs; } if (!pendingReadsBuffer) { return hrTimeout < hrTimeout.zero() ? -ETIMEDOUT : 0; } auto res = ::read(fd, pendingReadsBuffer, *pendingReadsBufferSize * sizeof(*pendingReadsBuffer)); if (res < 0) { const auto error = errno; PLOG(ERROR) << "read() failed"; return -error; } if (res == 0) { *pendingReadsBufferSize = 0; return -ETIMEDOUT; } if ((res % sizeof(*pendingReadsBuffer)) != 0) { PLOG(ERROR) << "read() returned half of a struct??"; return -EFAULT; } *pendingReadsBufferSize = res / sizeof(*pendingReadsBuffer); return 0; } template PublicPendingRead convertRead(RawPendingRead rawRead) { PublicPendingRead res = { .bootClockTsUs = rawRead.timestamp_us, .block = (IncFsBlockIndex)rawRead.block_index, .serialNo = rawRead.serial_number, }; memcpy(&res.id.data, rawRead.file_id.bytes, sizeof(res.id.data)); if constexpr (std::is_same_v) { if constexpr (std::is_same_v) { res.uid = rawRead.uid; } else { res.uid = kIncFsNoUid; } } return res; } template static int waitForReads(IncFsFd readFd, int32_t timeoutMs, PublicPendingRead buffer[], size_t* bufferSize) { std::vector pendingReads(*bufferSize); if (const auto res = waitForReadsImpl(readFd, timeoutMs, pendingReads.data(), bufferSize)) { return res; } for (size_t i = 0; i != *bufferSize; ++i) { buffer[i] = convertRead(pendingReads[i]); } return 0; } template static int waitForReads(IncFsFd readFd, int32_t timeoutMs, PublicPendingRead buffer[], size_t* bufferSize) { if (features() & Features::v2) { return waitForReads(readFd, timeoutMs, buffer, bufferSize); } return waitForReads(readFd, timeoutMs, buffer, bufferSize); } IncFsErrorCode IncFs_WaitForPendingReads(const IncFsControl* control, int32_t timeoutMs, IncFsReadInfo buffer[], size_t* bufferSize) { if (!control || control->pendingReads < 0) { return -EINVAL; } return waitForReads(control->pendingReads, timeoutMs, buffer, bufferSize); } IncFsErrorCode IncFs_WaitForPendingReadsWithUid(const IncFsControl* control, int32_t timeoutMs, IncFsReadInfoWithUid buffer[], size_t* bufferSize) { if (!control || control->pendingReads < 0) { return -EINVAL; } return waitForReads(control->pendingReads, timeoutMs, buffer, bufferSize); } IncFsErrorCode IncFs_WaitForPageReads(const IncFsControl* control, int32_t timeoutMs, IncFsReadInfo buffer[], size_t* bufferSize) { if (!control || control->logs < 0) { return -EINVAL; } return waitForReads(control->logs, timeoutMs, buffer, bufferSize); } IncFsErrorCode IncFs_WaitForPageReadsWithUid(const IncFsControl* control, int32_t timeoutMs, IncFsReadInfoWithUid buffer[], size_t* bufferSize) { if (!control || control->logs < 0) { return -EINVAL; } return waitForReads(control->logs, timeoutMs, buffer, bufferSize); } static IncFsFd openForSpecialOps(int cmd, const char* path) { ab::unique_fd fd(::open(path, O_RDONLY | O_CLOEXEC)); if (fd < 0) { return -errno; } struct incfs_permit_fill args = {.file_descriptor = (uint32_t)fd.get()}; auto err = ::ioctl(cmd, INCFS_IOC_PERMIT_FILL, &args); if (err < 0) { return -errno; } return fd.release(); } IncFsFd IncFs_OpenForSpecialOpsByPath(const IncFsControl* control, const char* path) { if (!control) { return -EINVAL; } const auto pathRoot = registry().rootFor(path); const auto cmd = control->cmd; const auto root = rootForCmd(cmd); if (root.empty() || root != pathRoot) { return -EINVAL; } return openForSpecialOps(cmd, makeCommandPath(root, path).c_str()); } IncFsFd IncFs_OpenForSpecialOpsById(const IncFsControl* control, IncFsFileId id) { if (!control) { return -EINVAL; } const auto cmd = control->cmd; const auto root = rootForCmd(cmd); if (root.empty()) { return -EINVAL; } auto name = indexPath(root, id); return openForSpecialOps(cmd, makeCommandPath(root, name).c_str()); } static int writeBlocks(int fd, const incfs_fill_block blocks[], int blocksCount) { if (fd < 0 || blocksCount == 0) { return 0; } if (blocksCount < 0) { return -EINVAL; } auto ptr = blocks; const auto end = blocks + blocksCount; do { struct incfs_fill_blocks args = {.count = uint64_t(end - ptr), .fill_blocks = (uint64_t)(uintptr_t)ptr}; const auto written = ::ioctl(fd, INCFS_IOC_FILL_BLOCKS, &args); if (written < 0) { if (errno == EINTR) { continue; } const auto error = errno; PLOG(WARNING) << "writing IncFS blocks failed"; if (ptr == blocks) { return -error; } // something has been written, return a success here and let the // next call handle the error. break; } ptr += written; } while (ptr < end); return ptr - blocks; } IncFsErrorCode IncFs_WriteBlocks(const IncFsDataBlock blocks[], size_t blocksCount) { incfs_fill_block incfsBlocks[128]; int writtenCount = 0; int incfsBlocksUsed = 0; int lastBlockFd = -1; for (size_t i = 0; i < blocksCount; ++i) { if (lastBlockFd != blocks[i].fileFd || incfsBlocksUsed == std::size(incfsBlocks)) { auto count = writeBlocks(lastBlockFd, incfsBlocks, incfsBlocksUsed); if (count > 0) { writtenCount += count; } if (count != incfsBlocksUsed) { return writtenCount ? writtenCount : count; } lastBlockFd = blocks[i].fileFd; incfsBlocksUsed = 0; } incfsBlocks[incfsBlocksUsed] = incfs_fill_block{ .block_index = (uint32_t)blocks[i].pageIndex, .data_len = blocks[i].dataSize, .data = (uint64_t)blocks[i].data, .compression = (uint8_t)blocks[i].compression, .flags = uint8_t(blocks[i].kind == INCFS_BLOCK_KIND_HASH ? INCFS_BLOCK_FLAGS_HASH : 0), }; ++incfsBlocksUsed; } auto count = writeBlocks(lastBlockFd, incfsBlocks, incfsBlocksUsed); if (count > 0) { writtenCount += count; } return writtenCount ? writtenCount : count; } IncFsErrorCode IncFs_BindMount(const char* sourceDir, const char* targetDir) { if (!enabled()) { return -ENOTSUP; } if (path::dirName(sourceDir) == path::procfsFdDir) { // can't find such path in the mount registry, but still can verify the filesystem // via the stat() call if (!isIncFsPathImpl(sourceDir)) { return -EINVAL; } } else { auto [sourceRoot, subpath] = registry().rootAndSubpathFor(sourceDir); if (sourceRoot.empty()) { return -EINVAL; } if (subpath.empty()) { LOG(WARNING) << "[incfs] Binding the root mount '" << sourceRoot << "' is not allowed"; return -EINVAL; } } if (auto err = isValidMountTarget(targetDir); err != 0) { return err; } if (::mount(sourceDir, targetDir, nullptr, MS_BIND, nullptr)) { PLOG(ERROR) << "[incfs] Failed to bind mount '" << sourceDir << "' to '" << targetDir << '\''; return -errno; } return 0; } IncFsErrorCode IncFs_Unmount(const char* dir) { if (!enabled()) { return -ENOTSUP; } if (!isIncFsPathImpl(dir)) { LOG(WARNING) << __func__ << ": umount() called on non-incfs directory '" << dir << '\''; return -EINVAL; } errno = 0; if (::umount2(dir, MNT_FORCE) == 0 || errno == EINVAL || errno == ENOENT) { // EINVAL - not a mount point, ENOENT - doesn't exist at all return -errno; } PLOG(WARNING) << __func__ << ": umount(force) failed, detaching '" << dir << '\''; errno = 0; if (!::umount2(dir, MNT_DETACH)) { return 0; } PLOG(WARNING) << __func__ << ": umount(detach) returned non-zero for '" << dir << '\''; return 0; } bool IncFs_IsIncFsFd(int fd) { return isIncFsFdImpl(fd); } bool IncFs_IsIncFsPath(const char* path) { return isIncFsPathImpl(path); } IncFsErrorCode IncFs_GetFilledRanges(int fd, IncFsSpan outBuffer, IncFsFilledRanges* filledRanges) { return IncFs_GetFilledRangesStartingFrom(fd, 0, outBuffer, filledRanges); } IncFsErrorCode IncFs_GetFilledRangesStartingFrom(int fd, int startBlockIndex, IncFsSpan outBuffer, IncFsFilledRanges* filledRanges) { if (fd < 0) { return -EBADF; } if (startBlockIndex < 0) { return -EINVAL; } if (!outBuffer.data && outBuffer.size > 0) { return -EINVAL; } if (!filledRanges) { return -EINVAL; } // Use this to optimize the incfs call and have the same buffer for both the incfs and the // public structs. static_assert(sizeof(IncFsBlockRange) == sizeof(incfs_filled_range)); *filledRanges = {}; auto outStart = (IncFsBlockRange*)outBuffer.data; auto outEnd = outStart + outBuffer.size / sizeof(*outStart); auto outPtr = outStart; int error = 0; int dataBlocks; incfs_get_filled_blocks_args args = {}; for (;;) { auto start = args.index_out ? args.index_out : startBlockIndex; args = incfs_get_filled_blocks_args{ .range_buffer = (uint64_t)(uintptr_t)outPtr, .range_buffer_size = uint32_t((outEnd - outPtr) * sizeof(*outPtr)), .start_index = start, }; errno = 0; auto res = ::ioctl(fd, INCFS_IOC_GET_FILLED_BLOCKS, &args); error = errno; if (res && error != EINTR && error != ERANGE) { return -error; } dataBlocks = args.data_blocks_out; outPtr += args.range_buffer_size_out / sizeof(incfs_filled_range); if (!res || error == ERANGE) { break; } // in case of EINTR we want to continue calling the function } if (outPtr > outEnd) { outPtr = outEnd; error = ERANGE; } filledRanges->endIndex = args.index_out; auto hashStartPtr = outPtr; if (outPtr != outStart) { // figure out the ranges for data block and hash blocks in the output for (; hashStartPtr != outStart; --hashStartPtr) { if ((hashStartPtr - 1)->begin < dataBlocks) { break; } } auto lastDataPtr = hashStartPtr - 1; // here we go, this is the first block that's before or at the hashes if (lastDataPtr->end <= dataBlocks) { ; // we're good, the boundary is between the ranges - |hashStartPtr| is correct } else { // the hard part: split the |lastDataPtr| range into the data and the hash pieces if (outPtr == outEnd) { // the buffer turned out to be too small, even though it actually wasn't error = ERANGE; if (hashStartPtr == outEnd) { // this is even worse: there's no room to put even a single hash block into. filledRanges->endIndex = lastDataPtr->end = dataBlocks; } else { std::copy_backward(lastDataPtr, outPtr - 1, outPtr); lastDataPtr->end = hashStartPtr->begin = dataBlocks; filledRanges->endIndex = (outPtr - 1)->end; } } else { std::copy_backward(lastDataPtr, outPtr, outPtr + 1); lastDataPtr->end = hashStartPtr->begin = dataBlocks; ++outPtr; } } // now fix the indices of all hash blocks - no one should know they're simply past the // regular data blocks in the file! for (auto ptr = hashStartPtr; ptr != outPtr; ++ptr) { ptr->begin -= dataBlocks; ptr->end -= dataBlocks; } } filledRanges->dataRanges = outStart; filledRanges->dataRangesCount = hashStartPtr - outStart; filledRanges->hashRanges = hashStartPtr; filledRanges->hashRangesCount = outPtr - hashStartPtr; return -error; } static IncFsErrorCode isFullyLoadedV2(std::string_view root, IncFsFileId id) { if (::access(path::join(root, INCFS_INCOMPLETE_NAME, toStringImpl(id)).c_str(), F_OK)) { if (errno == ENOENT) { return 0; // no such incomplete file -> it's fully loaded. } return -errno; } return -ENODATA; } static IncFsErrorCode isFullyLoadedSlow(int fd) { char buffer[2 * sizeof(IncFsBlockRange)]; IncFsFilledRanges ranges; auto res = IncFs_GetFilledRanges(fd, IncFsSpan{.data = buffer, .size = std::size(buffer)}, &ranges); if (res == -ERANGE) { // need room for more than two ranges - definitely not fully loaded return -ENODATA; } if (res != 0) { return res; } // empty file if (ranges.endIndex == 0) { return 0; } // file with no hash tree if (ranges.dataRangesCount == 1 && ranges.hashRangesCount == 0) { return (ranges.dataRanges[0].begin == 0 && ranges.dataRanges[0].end == ranges.endIndex) ? 0 : -ENODATA; } // file with a hash tree if (ranges.dataRangesCount == 1 && ranges.hashRangesCount == 1) { // calculate the expected data size from the size of the hash range and |endIndex|, which is // the total number of blocks in the file, both data and hash blocks together. if (ranges.hashRanges[0].begin != 0) { return -ENODATA; } const auto expectedDataBlocks = ranges.endIndex - (ranges.hashRanges[0].end - ranges.hashRanges[0].begin); return (ranges.dataRanges[0].begin == 0 && ranges.dataRanges[0].end == expectedDataBlocks) ? 0 : -ENODATA; } return -ENODATA; } IncFsErrorCode IncFs_IsFullyLoaded(int fd) { if (features() & Features::v2) { const auto fdPath = path::fromFd(fd); if (fdPath.empty()) { return errno ? -errno : -EINVAL; } const auto id = getId(::fgetxattr, fd); if (id == kIncFsInvalidFileId) { return -errno; } return isFullyLoadedV2(registry().rootFor(fdPath), id); } return isFullyLoadedSlow(fd); } IncFsErrorCode IncFs_IsFullyLoadedByPath(const IncFsControl* control, const char* path) { if (!control || !path) { return -EINVAL; } const auto root = rootForCmd(control->cmd); if (root.empty()) { return -EINVAL; } const auto pathRoot = registry().rootFor(path); if (pathRoot != root) { return -EINVAL; } if (features() & Features::v2) { const auto id = getId(::getxattr, path); if (id == kIncFsInvalidFileId) { return -ENOTSUP; } return isFullyLoadedV2(root, id); } return isFullyLoadedSlow(openForSpecialOps(control->cmd, makeCommandPath(root, path).c_str())); } IncFsErrorCode IncFs_IsFullyLoadedById(const IncFsControl* control, IncFsFileId fileId) { if (!control) { return -EINVAL; } const auto root = rootForCmd(control->cmd); if (root.empty()) { return -EINVAL; } if (features() & Features::v2) { return isFullyLoadedV2(root, fileId); } return isFullyLoadedSlow( openForSpecialOps(control->cmd, makeCommandPath(root, indexPath(root, fileId)).c_str())); } static IncFsErrorCode isEverythingLoadedV2(const IncFsControl* control) { const auto root = rootForCmd(control->cmd); if (root.empty()) { return -EINVAL; } auto res = forEachFileIn(path::join(root, INCFS_INCOMPLETE_NAME), [](auto) { return false; }); return res < 0 ? res : res > 0 ? -ENODATA : 0; } static IncFsErrorCode isEverythingLoadedSlow(const IncFsControl* control) { const auto root = rootForCmd(control->cmd); if (root.empty()) { return -EINVAL; } // No special API for this version of the driver, need to recurse and check each file // separately. Can at least speed it up by iterating over the .index/ dir and not dealing with // the directory tree. const auto indexPath = path::join(root, INCFS_INDEX_NAME); const auto dir = path::openDir(indexPath.c_str()); if (!dir) { return -EINVAL; } while (const auto entry = ::readdir(dir.get())) { if (entry->d_type != DT_REG) { continue; } const auto name = path::join(indexPath, entry->d_name); auto fd = ab::unique_fd(openForSpecialOps(control->cmd, makeCommandPath(root, name).c_str())); if (fd.get() < 0) { PLOG(WARNING) << __func__ << "(): can't open " << entry->d_name << " for special ops"; return fd.release(); } const auto checkFullyLoaded = IncFs_IsFullyLoaded(fd.get()); if (checkFullyLoaded == 0 || checkFullyLoaded == -EOPNOTSUPP || checkFullyLoaded == -ENOTSUP || checkFullyLoaded == -ENOENT) { // special kinds of files may return an error here, but it still means // _this_ file is OK - you simply need to check the rest. E.g. can't query // a mapped file, instead need to check its parent. continue; } return checkFullyLoaded; } return 0; } IncFsErrorCode IncFs_IsEverythingFullyLoaded(const IncFsControl* control) { if (!control) { return -EINVAL; } if (features() & Features::v2) { return isEverythingLoadedV2(control); } return isEverythingLoadedSlow(control); } IncFsErrorCode IncFs_SetUidReadTimeouts(const IncFsControl* control, const IncFsUidReadTimeouts timeouts[], size_t count) { if (!control) { return -EINVAL; } if (!(features() & Features::v2)) { return -ENOTSUP; } std::vector argTimeouts(count); for (size_t i = 0; i != count; ++i) { argTimeouts[i] = incfs_per_uid_read_timeouts{ .uid = (uint32_t)timeouts[i].uid, .min_time_us = timeouts[i].minTimeUs, .min_pending_time_us = timeouts[i].minPendingTimeUs, .max_pending_time_us = timeouts[i].maxPendingTimeUs, }; } incfs_set_read_timeouts_args args = {.timeouts_array = (uint64_t)(uintptr_t)argTimeouts.data(), .timeouts_array_size = uint32_t( argTimeouts.size() * sizeof(*argTimeouts.data()))}; if (::ioctl(control->cmd, INCFS_IOC_SET_READ_TIMEOUTS, &args)) { PLOG(WARNING) << "[incfs] setUidReadTimeouts failed"; return -errno; } return 0; } IncFsErrorCode IncFs_GetUidReadTimeouts(const IncFsControl* control, IncFsUidReadTimeouts timeouts[], size_t* bufferSize) { if (!control || !bufferSize) { return -EINVAL; } if (!(features() & Features::v2)) { return -ENOTSUP; } std::vector argTimeouts(*bufferSize); incfs_get_read_timeouts_args args = {.timeouts_array = (uint64_t)(uintptr_t)argTimeouts.data(), .timeouts_array_size = uint32_t( argTimeouts.size() * sizeof(*argTimeouts.data())), .timeouts_array_size_out = args.timeouts_array_size}; if (::ioctl(control->cmd, INCFS_IOC_GET_READ_TIMEOUTS, &args)) { if (errno == E2BIG) { *bufferSize = args.timeouts_array_size_out / sizeof(*argTimeouts.data()); } return -errno; } *bufferSize = args.timeouts_array_size_out / sizeof(*argTimeouts.data()); for (size_t i = 0; i != *bufferSize; ++i) { timeouts[i].uid = argTimeouts[i].uid; timeouts[i].minTimeUs = argTimeouts[i].min_time_us; timeouts[i].minPendingTimeUs = argTimeouts[i].min_pending_time_us; timeouts[i].maxPendingTimeUs = argTimeouts[i].max_pending_time_us; } return 0; } // Trying to detect if this is a mapped file. // Not the best way as it might return true for other system files. // TODO: remove after IncFS returns ENOTSUP for such files. static bool isMapped(int fd) { char buffer[kIncFsFileIdStringLength]; const auto res = ::fgetxattr(fd, kIdAttrName, buffer, sizeof(buffer)); return res != sizeof(buffer); } static IncFsErrorCode getFileBlockCount(int fd, IncFsBlockCounts* blockCount) { if (isMapped(fd)) { return -ENOTSUP; } incfs_get_block_count_args args = {}; auto res = ::ioctl(fd, INCFS_IOC_GET_BLOCK_COUNT, &args); if (res < 0) { return -errno; } *blockCount = IncFsBlockCounts{ .totalDataBlocks = args.total_data_blocks_out, .filledDataBlocks = args.filled_data_blocks_out, .totalHashBlocks = args.total_hash_blocks_out, .filledHashBlocks = args.filled_hash_blocks_out, }; return 0; } IncFsErrorCode IncFs_GetFileBlockCountById(const IncFsControl* control, IncFsFileId id, IncFsBlockCounts* blockCount) { if (!control) { return -EINVAL; } if (!(features() & Features::v2)) { return -ENOTSUP; } const auto root = rootForCmd(control->cmd); if (root.empty()) { return -EINVAL; } auto name = indexPath(root, id); auto fd = openRaw(name); if (fd < 0) { return fd.get(); } return getFileBlockCount(fd, blockCount); } IncFsErrorCode IncFs_GetFileBlockCountByPath(const IncFsControl* control, const char* path, IncFsBlockCounts* blockCount) { if (!control) { return -EINVAL; } if (!(features() & Features::v2)) { return -ENOTSUP; } const auto pathRoot = registry().rootFor(path); const auto root = rootForCmd(control->cmd); if (root.empty() || root != pathRoot) { return -EINVAL; } auto fd = openRaw(path); if (fd < 0) { return fd.get(); } return getFileBlockCount(fd, blockCount); } IncFsErrorCode IncFs_ListIncompleteFiles(const IncFsControl* control, IncFsFileId ids[], size_t* bufferSize) { if (!control || !bufferSize) { return -EINVAL; } if (!(features() & Features::v2)) { return -ENOTSUP; } const auto root = rootForCmd(control->cmd); if (root.empty()) { return -EINVAL; } size_t index = 0; int error = 0; const auto res = forEachFileIn(path::join(root, INCFS_INCOMPLETE_NAME), [&](const char* name) { if (index >= *bufferSize) { error = -E2BIG; } else { ids[index] = IncFs_FileIdFromString(name); } ++index; return true; }); if (res < 0) { return res; } *bufferSize = index; return error ? error : 0; } IncFsErrorCode IncFs_ForEachFile(const IncFsControl* control, void* context, FileCallback cb) { if (!control || !cb) { return -EINVAL; } const auto root = rootForCmd(control->cmd); if (root.empty()) { return -EINVAL; } return forEachFileIn(path::join(root, INCFS_INDEX_NAME), [&](const char* name) { return cb(context, control, IncFs_FileIdFromString(name)); }); } IncFsErrorCode IncFs_ForEachIncompleteFile(const IncFsControl* control, void* context, FileCallback cb) { if (!control || !cb) { return -EINVAL; } if (!(features() & Features::v2)) { return -ENOTSUP; } const auto root = rootForCmd(control->cmd); if (root.empty()) { return -EINVAL; } return forEachFileIn(path::join(root, INCFS_INCOMPLETE_NAME), [&](const char* name) { return cb(context, control, IncFs_FileIdFromString(name)); }); } IncFsErrorCode IncFs_WaitForLoadingComplete(const IncFsControl* control, int32_t timeoutMs) { if (!control) { return -EINVAL; } if (!(features() & Features::v2)) { return -ENOTSUP; } using namespace std::chrono; auto hrTimeout = steady_clock::duration(milliseconds(timeoutMs)); const auto root = rootForCmd(control->cmd); if (root.empty()) { return -EINVAL; } ab::unique_fd fd(inotify_init1(IN_NONBLOCK | IN_CLOEXEC)); if (!fd.ok()) { return -EFAULT; } // first create all the watches, and only then list existing files to prevent races auto dirPath = path::join(root, INCFS_INCOMPLETE_NAME); int watchFd = inotify_add_watch(fd.get(), dirPath.c_str(), IN_DELETE); if (watchFd < 0) { return -errno; } size_t count = 0; auto res = IncFs_ListIncompleteFiles(control, nullptr, &count); if (!res) { return 0; } if (res != -E2BIG) { return res; } while (hrTimeout > hrTimeout.zero()) { const auto startTs = steady_clock::now(); pollfd pfd = {fd.get(), POLLIN, 0}; const auto res = ::poll(&pfd, 1, duration_cast(hrTimeout).count()); if (res == 0) { return -ETIMEDOUT; } if (res < 0) { const auto error = errno; if (error != EINTR) { PLOG(ERROR) << "poll() failed"; return -error; } } else { // empty the inotify fd first to not miss any new deletions, // then check if the directory is empty. char buffer[sizeof(inotify_event) + NAME_MAX + 1]; for (;;) { auto err = TEMP_FAILURE_RETRY(::read(fd.get(), buffer, sizeof(buffer))); if (err < 0) { if (errno == EAGAIN) { // no new events break; } return -errno; } } size_t count = 0; auto res = IncFs_ListIncompleteFiles(control, nullptr, &count); if (!res) { return 0; } if (res != -E2BIG) { return res; } } hrTimeout -= steady_clock::now() - startTs; } return -ETIMEDOUT; } IncFsErrorCode IncFs_WaitForFsWrittenBlocksChange(const IncFsControl* control, int32_t timeoutMs, IncFsSize* count) { if (!control || !count) { return -EINVAL; } if (!(features() & Features::v2)) { return -ENOTSUP; } using namespace std::chrono; auto hrTimeout = steady_clock::duration(milliseconds(timeoutMs)); while (hrTimeout > hrTimeout.zero()) { const auto startTs = steady_clock::now(); pollfd pfd = {control->blocksWritten, POLLIN, 0}; const auto res = ::poll(&pfd, 1, duration_cast(hrTimeout).count()); if (res > 0) { break; } if (res == 0) { return -ETIMEDOUT; } const auto error = errno; if (error != EINTR) { PLOG(ERROR) << "poll() failed"; return -error; } hrTimeout -= steady_clock::now() - startTs; } char str[32]; auto size = ::read(control->blocksWritten, str, sizeof(str)); if (size < 0) { const auto error = errno; PLOG(ERROR) << "read() failed"; return -error; } const auto res = std::from_chars(str, str + size, *count); if (res.ec != std::errc{}) { return res.ec == std::errc::invalid_argument ? -EINVAL : -ERANGE; } return 0; } static IncFsErrorCode reserveSpace(const char* backingPath, IncFsSize size) { auto fd = ab::unique_fd(::open(backingPath, O_WRONLY | O_CLOEXEC)); if (fd < 0) { return -errno; } struct stat st = {}; if (::fstat(fd.get(), &st)) { return -errno; } if (size == kIncFsTrimReservedSpace) { if (::ftruncate(fd.get(), st.st_size)) { return -errno; } } else { // Add 1.5% of the size for the hash tree and the blockmap, and some more blocks // for fixed overhead. // hash tree is ~33 bytes / page, and blockmap is 10 bytes / page // no need to round to a page size as filesystems already do that. const auto backingSize = IncFsSize(size * 1.015) + INCFS_DATA_FILE_BLOCK_SIZE * 4; if (backingSize < st.st_size) { return -EPERM; } if (::fallocate(fd.get(), FALLOC_FL_KEEP_SIZE, 0, backingSize)) { return -errno; } } return 0; } IncFsErrorCode IncFs_ReserveSpaceByPath(const IncFsControl* control, const char* path, IncFsSize size) { if (!control || (size != kIncFsTrimReservedSpace && size < 0)) { return -EINVAL; } const auto [pathRoot, backingRoot, subpath] = registry().detailsFor(path); const auto root = rootForCmd(control->cmd); if (root.empty() || root != pathRoot) { return -EINVAL; } return reserveSpace(path::join(backingRoot, subpath).c_str(), size); } IncFsErrorCode IncFs_ReserveSpaceById(const IncFsControl* control, IncFsFileId id, IncFsSize size) { if (!control || (size != kIncFsTrimReservedSpace && size < 0)) { return -EINVAL; } const auto root = rootForCmd(control->cmd); if (root.empty()) { return -EINVAL; } auto path = indexPath(root, id); const auto [pathRoot, backingRoot, subpath] = registry().detailsFor(path); if (root != pathRoot) { return -EINVAL; } return reserveSpace(path::join(backingRoot, subpath).c_str(), size); } template static int readIntFromFile(std::string_view rootDir, std::string_view subPath, IntType& result) { std::string content; if (!ab::ReadFileToString(path::join(rootDir, subPath), &content)) { PLOG(ERROR) << "IncFs_GetMetrics: failed to read file: " << rootDir << "/" << subPath; return -errno; } const auto res = std::from_chars(content.data(), content.data() + content.size(), result); if (res.ec != std::errc()) { return -static_cast(res.ec); } return 0; } IncFsErrorCode IncFs_GetMetrics(const char* sysfsName, IncFsMetrics* metrics) { if (!sysfsName || !*sysfsName) { return -EINVAL; } const auto kSysfsMetricsDir = ab::StringPrintf("/sys/fs/%s/instances/%s", INCFS_NAME, sysfsName); int err; if (err = readIntFromFile(kSysfsMetricsDir, "reads_delayed_min", metrics->readsDelayedMin); err != 0) { return err; } if (err = readIntFromFile(kSysfsMetricsDir, "reads_delayed_min_us", metrics->readsDelayedMinUs); err != 0) { return err; } if (err = readIntFromFile(kSysfsMetricsDir, "reads_delayed_pending", metrics->readsDelayedPending); err != 0) { return err; } if (err = readIntFromFile(kSysfsMetricsDir, "reads_delayed_pending_us", metrics->readsDelayedPendingUs); err != 0) { return err; } if (err = readIntFromFile(kSysfsMetricsDir, "reads_failed_hash_verification", metrics->readsFailedHashVerification); err != 0) { return err; } if (err = readIntFromFile(kSysfsMetricsDir, "reads_failed_other", metrics->readsFailedOther); err != 0) { return err; } if (err = readIntFromFile(kSysfsMetricsDir, "reads_failed_timed_out", metrics->readsFailedTimedOut); err != 0) { return err; } return 0; } IncFsErrorCode IncFs_GetLastReadError(const IncFsControl* control, IncFsLastReadError* lastReadError) { if (!control) { return -EINVAL; } if (!(features() & Features::v2)) { return -ENOTSUP; } incfs_get_last_read_error_args args = {}; auto res = ::ioctl(control->cmd, INCFS_IOC_GET_LAST_READ_ERROR, &args); if (res < 0) { PLOG(ERROR) << "[incfs] IncFs_GetLastReadError failed."; return -errno; } *lastReadError = IncFsLastReadError{ .timestampUs = args.time_us_out, .block = static_cast(args.page_out), .errorNo = args.errno_out, .uid = static_cast(args.uid_out), }; static_assert(sizeof(args.file_id_out.bytes) == sizeof(lastReadError->id.data)); memcpy(lastReadError->id.data, args.file_id_out.bytes, sizeof(args.file_id_out.bytes)); return 0; } MountRegistry& android::incfs::defaultMountRegistry() { return registry(); }