Merge Android 12

Bug: 202323961
Merged-In: Ibd657c8fc8a3169a4e83e192a157f2e9e5e1240a
Change-Id: I261359334b74ed35eed116716a33ea55d5083fa3
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..722d5e7
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1 @@
+.vscode
diff --git a/incfs/Android.bp b/incfs/Android.bp
index cb4cb1e..88b3abb 100644
--- a/incfs/Android.bp
+++ b/incfs/Android.bp
@@ -163,6 +163,21 @@
     require_root: true,
 }
 
+cc_benchmark {
+    name: "hardening-benchmark",
+    defaults: ["libincfs_defaults"],
+
+    srcs: [
+        "tests/hardening_benchmark.cpp",
+    ],
+    static_libs: [
+        "libziparchive_for_incfs",
+        "libutils",
+        "libincfs",
+        "libincfs-utils",
+    ],
+}
+
 cc_binary {
     name: "incfsdump",
     defaults: ["libincfs_defaults"],
diff --git a/incfs/MountRegistry.cpp b/incfs/MountRegistry.cpp
index c4752f3..b7b53c8 100644
--- a/incfs/MountRegistry.cpp
+++ b/incfs/MountRegistry.cpp
@@ -18,18 +18,18 @@
 
 #include "MountRegistry.h"
 
+#include <android-base/logging.h>
+#include <poll.h>
+#include <stdlib.h>
+
+#include <charconv>
+#include <set>
+#include <unordered_map>
+
 #include "incfs.h"
 #include "path.h"
 #include "split.h"
 
-#include <android-base/logging.h>
-
-#include <charconv>
-#include <unordered_map>
-
-#include <poll.h>
-#include <stdlib.h>
-
 using namespace std::literals;
 
 namespace android::incfs {
@@ -69,7 +69,7 @@
     std::vector<std::pair<std::string_view, std::string_view>> result;
     result.reserve(mBase->binds.size());
     for (auto it : mBase->binds) {
-        result.emplace_back(it->second.first, it->first);
+        result.emplace_back(it->second.subdir, it->first);
     }
     return result;
 }
@@ -88,12 +88,12 @@
         std::string_view path) const {
     auto it = rootByBindPoint.lower_bound(path);
     if (it != rootByBindPoint.end() && it->first == path) {
-        return {it->second.second, it};
+        return {it->second.rootIndex, it};
     }
     if (it != rootByBindPoint.begin()) {
         --it;
         if (path::startsWith(path, it->first) && path.size() > it->first.size()) {
-            const auto index = it->second.second;
+            const auto index = it->second.rootIndex;
             if (index >= int(roots.size()) || roots[index].empty()) {
                 LOG(ERROR) << "[incfs] Root for path '" << path << "' #" << index
                            << " is not valid";
@@ -113,24 +113,24 @@
     return roots[index].path;
 }
 
-std::pair<std::string_view, std::string> MountRegistry::Mounts::rootAndSubpathFor(
-        std::string_view path) const {
+auto MountRegistry::Mounts::rootAndSubpathFor(std::string_view path) const
+        -> std::pair<const Root*, std::string> {
     auto normalPath = path::normalize(path);
     auto [index, bindIt] = rootIndex(normalPath);
     if (index < 0) {
         return {};
     }
 
-    const auto& bindSubdir = bindIt->second.first;
+    const auto& bindSubdir = bindIt->second.subdir;
     const auto pastBindSubdir = path::relativize(bindIt->first, normalPath);
     const auto& root = roots[index];
-    return {root.path, path::join(bindSubdir, pastBindSubdir)};
+    return {&root, path::join(bindSubdir, pastBindSubdir)};
 }
 
 void MountRegistry::Mounts::addRoot(std::string_view root, std::string_view backingDir) {
     const auto index = roots.size();
     auto absolute = path::normalize(root);
-    auto it = rootByBindPoint.insert_or_assign(absolute, std::pair{std::string(), index}).first;
+    auto it = rootByBindPoint.insert_or_assign(absolute, Bind{std::string(), int(index)}).first;
     roots.push_back({std::move(absolute), path::normalize(backingDir), {it}});
 }
 
@@ -141,13 +141,17 @@
         LOG(WARNING) << "[incfs] Trying to remove non-existent root '" << root << '\'';
         return;
     }
-    const auto index = it->second.second;
+    const auto index = it->second.rootIndex;
     if (index >= int(roots.size())) {
         LOG(ERROR) << "[incfs] Root '" << root << "' has index " << index
                    << " out of bounds (total roots count is " << roots.size();
         return;
     }
 
+    for (auto bindIt : roots[index].binds) {
+        rootByBindPoint.erase(bindIt);
+    }
+
     if (index + 1 == int(roots.size())) {
         roots.pop_back();
         // Run a small GC job here as we may be able to remove some obsolete
@@ -158,37 +162,6 @@
     } else {
         roots[index].clear();
     }
-    rootByBindPoint.erase(it);
-}
-
-void MountRegistry::Mounts::moveBind(std::string_view src, std::string_view dest) {
-    auto srcAbsolute = path::normalize(src);
-    auto destAbsolute = path::normalize(dest);
-    if (srcAbsolute == destAbsolute) {
-        return;
-    }
-
-    auto [root, rootIt] = rootIndex(srcAbsolute);
-    if (root < 0) {
-        LOG(ERROR) << "[incfs] No root found for bind move from " << src << " to " << dest;
-        return;
-    }
-
-    if (roots[root].path == srcAbsolute) {
-        // moving the whole root
-        roots[root].path = destAbsolute;
-    }
-
-    // const_cast<> here is safe as we're erasing that element on the next line.
-    const auto newRootIt = rootByBindPoint
-                                   .insert_or_assign(std::move(destAbsolute),
-                                                     std::pair{std::move(const_cast<std::string&>(
-                                                                       rootIt->second.first)),
-                                                               root})
-                                   .first;
-    rootByBindPoint.erase(rootIt);
-    const auto bindIt = std::find(roots[root].binds.begin(), roots[root].binds.end(), rootIt);
-    *bindIt = newRootIt;
 }
 
 void MountRegistry::Mounts::addBind(std::string_view what, std::string_view where) {
@@ -201,11 +174,10 @@
 
     const auto& currentBind = rootIt->first;
     auto whatSubpath = path::relativize(currentBind, whatAbsolute);
-    const auto& subdir = rootIt->second.first;
+    const auto& subdir = rootIt->second.subdir;
     auto realSubdir = path::join(subdir, whatSubpath);
     auto it = rootByBindPoint
-                      .insert_or_assign(path::normalize(where),
-                                        std::pair{std::move(realSubdir), root})
+                      .insert_or_assign(path::normalize(where), Bind{std::move(realSubdir), root})
                       .first;
     roots[root].binds.push_back(it);
 }
@@ -244,10 +216,23 @@
     auto lock = ensureUpToDate();
     return std::string(mMounts.rootFor(path));
 }
+
+auto MountRegistry::detailsFor(std::string_view path) -> Details {
+    auto lock = ensureUpToDate();
+    auto [root, subpath] = mMounts.rootAndSubpathFor(path);
+    if (!root) {
+        return {};
+    }
+    return {root->path, root->backing, subpath};
+}
+
 std::pair<std::string, std::string> MountRegistry::rootAndSubpathFor(std::string_view path) {
     auto lock = ensureUpToDate();
     auto [root, subpath] = mMounts.rootAndSubpathFor(path);
-    return {std::string(root), std::move(subpath)};
+    if (!root) {
+        return {};
+    }
+    return {std::string(root->path), std::move(subpath)};
 }
 
 MountRegistry::Mounts MountRegistry::copyMounts() {
@@ -319,8 +304,8 @@
 
 bool MountRegistry::Mounts::loadFrom(base::borrowed_fd fd, std::string_view filesystem) {
     struct MountInfo {
-        std::string root;
         std::string backing;
+        std::set<std::string, std::less<>> roots;
         std::vector<std::pair<std::string, std::string>> bindPoints;
     };
     std::unordered_map<std::string, MountInfo> mountsByGroup(16);
@@ -342,20 +327,22 @@
         }
         const auto groupId = items[2];
         auto subdir = items[3];
+        auto backingDir = items.rbegin()[1];
         auto mountPoint = std::string(items[4]);
         fixProcPath(mountPoint);
         mountPoint = path::normalize(mountPoint);
         auto& mount = mountsByGroup[std::string(groupId)];
+        if (mount.backing.empty()) {
+            mount.backing.assign(backingDir);
+        } else if (mount.backing != backingDir) {
+            LOG(WARNING) << "[incfs] root '" << *mount.roots.begin()
+                         << "' mounted in multiple places with different backing dirs, '"
+                         << mount.backing << "' vs new '" << backingDir
+                         << "'; updating to the new one";
+            mount.backing.assign(backingDir);
+        }
         if (subdir == "/"sv) {
-            if (mount.root.empty()) {
-                mount.root.assign(mountPoint);
-                mount.backing.assign(items.rbegin()[1]);
-                fixProcPath(mount.backing);
-            } else {
-                LOG(WARNING) << "[incfs] incfs root '" << mount.root
-                             << "' mounted in multiple places, ignoring later mount '" << mountPoint
-                             << '\'';
-            }
+            mount.roots.emplace(mountPoint);
             subdir = ""sv;
         }
         mount.bindPoints.emplace_back(std::string(subdir), std::move(mountPoint));
@@ -366,7 +353,7 @@
     }
 
     rootByBindPoint.clear();
-    // preserve the allocated capacity, but clean existing data
+    // preserve the allocated capacity, but clear existing data
     roots.resize(mountsByGroup.size());
     for (auto& root : roots) {
         root.binds.clear();
@@ -374,20 +361,32 @@
 
     int index = 0;
     for (auto& [_, mount] : mountsByGroup) {
+        if (mount.roots.empty()) {
+            // the mount has no root, and without root we have no good way of accessing the
+            // control files - so the only valid reaction here is to ignore it
+            LOG(WARNING) << "[incfs] mount '" << mount.backing << "' has no root, but "
+                         << mount.bindPoints.size() << " bind(s), ignoring";
+            continue;
+        }
+
         Root& root = roots[index];
         auto& binds = root.binds;
         binds.reserve(mount.bindPoints.size());
         for (auto& [subdir, bind] : mount.bindPoints) {
-            auto it =
-                    rootByBindPoint
-                            .insert_or_assign(std::move(bind), std::pair(std::move(subdir), index))
-                            .first;
+            auto it = rootByBindPoint
+                              .insert_or_assign(std::move(bind), Bind{std::move(subdir), index})
+                              .first;
             binds.push_back(it);
         }
-        root.path = std::move(mount.root);
         root.backing = std::move(mount.backing);
+        fixProcPath(root.backing);
+
+        // a trick here: given that as of now we either have exactly one root, or the preferred one
+        // is always at the front, let's pick that one here.
+        root.path = std::move(mount.roots.extract(mount.roots.begin()).value());
         ++index;
     }
+    roots.resize(index);
 
     LOG(INFO) << "[incfs] Loaded " << filesystem << " mount info: " << roots.size()
               << " instances, " << rootByBindPoint.size() << " mount points";
@@ -396,7 +395,7 @@
             LOG(INFO) << "[incfs]  '" << root << '\'';
             LOG(INFO) << "[incfs]    backing: '" << backing << '\'';
             for (auto&& bind : binds) {
-                LOG(INFO) << "[incfs]      bind : '" << bind->second.first << "'->'" << bind->first
+                LOG(INFO) << "[incfs]      bind : '" << bind->second.subdir << "'->'" << bind->first
                           << '\'';
             }
         }
diff --git a/incfs/incfs.cpp b/incfs/incfs.cpp
index 5fe675b..490b907 100644
--- a/incfs/incfs.cpp
+++ b/incfs/incfs.cpp
@@ -23,6 +23,7 @@
 #include <android-base/logging.h>
 #include <android-base/no_destructor.h>
 #include <android-base/parsebool.h>
+#include <android-base/properties.h>
 #include <android-base/stringprintf.h>
 #include <android-base/strings.h>
 #include <android-base/unique_fd.h>
@@ -33,6 +34,7 @@
 #include <openssl/sha.h>
 #include <selinux/android.h>
 #include <selinux/selinux.h>
+#include <sys/inotify.h>
 #include <sys/mount.h>
 #include <sys/poll.h>
 #include <sys/stat.h>
@@ -42,8 +44,8 @@
 #include <sys/xattr.h>
 #include <unistd.h>
 
+#include <charconv>
 #include <chrono>
-#include <fstream>
 #include <iterator>
 #include <mutex>
 #include <optional>
@@ -53,47 +55,52 @@
 #include "path.h"
 
 using namespace std::literals;
-
-using android::base::StringPrintf;
-using android::base::unique_fd;
+using namespace android::incfs;
+using namespace android::sysprop;
+namespace ab = android::base;
 
 struct IncFsControl final {
     IncFsFd cmd;
     IncFsFd pendingReads;
     IncFsFd logs;
-    constexpr IncFsControl(IncFsFd cmd, IncFsFd pendingReads, IncFsFd logs)
-          : cmd(cmd), pendingReads(pendingReads), logs(logs) {}
+    IncFsFd blocksWritten;
+    constexpr IncFsControl(IncFsFd cmd, IncFsFd pendingReads, IncFsFd logs, IncFsFd blocksWritten)
+          : cmd(cmd), pendingReads(pendingReads), logs(logs), blocksWritten(blocksWritten) {}
 };
 
-static android::incfs::MountRegistry& registry() {
-    static android::base::NoDestructor<android::incfs::MountRegistry> instance{};
+static MountRegistry& registry() {
+    static ab::NoDestructor<MountRegistry> instance{};
     return *instance;
 }
 
-static unique_fd openRaw(std::string_view file) {
-    auto fd = unique_fd(::open(android::incfs::details::c_str(file), O_RDONLY | O_CLOEXEC));
+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 unique_fd{-errno};
+        return ab::unique_fd{-errno};
     }
     return fd;
 }
 
-static unique_fd openRaw(std::string_view dir, std::string_view name) {
-    return openRaw(android::incfs::path::join(dir, name));
+static ab::unique_fd openRaw(std::string_view dir, std::string_view name) {
+    return openRaw(path::join(dir, name));
+}
+
+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 = android::incfs::path::fromFd(fd);
+    auto cmdFile = path::fromFd(fd);
     if (cmdFile.empty()) {
         LOG(INFO) << __func__ << "(): name empty for " << fd;
         return {};
     }
-    auto res = android::incfs::path::dirName(cmdFile);
+    auto res = path::dirName(cmdFile);
     if (res.empty()) {
         LOG(INFO) << __func__ << "(): dirname empty for " << cmdFile;
         return {};
     }
-    if (!android::incfs::path::endsWith(cmdFile, INCFS_PENDING_READS_FILENAME)) {
+    if (!path::endsWith(cmdFile, INCFS_PENDING_READS_FILENAME)) {
         LOG(INFO) << __func__ << "(): invalid file name " << cmdFile;
         return {};
     }
@@ -104,51 +111,38 @@
     return std::string(res);
 }
 
-static android::incfs::Features readIncFsFeatures() {
-    static const char kSysfsFeaturesDir[] = "/sys/fs/" INCFS_NAME "/features";
-    const auto dir = android::incfs::path::openDir(kSysfsFeaturesDir);
-    if (!dir) {
-        return android::incfs::Features::none;
-    }
-
-    int res = android::incfs::Features::none;
-    while (auto entry = ::readdir(dir.get())) {
-        if (entry->d_type != DT_REG) {
-            continue;
-        }
-        if (entry->d_name == "corefs"sv) {
-            res |= android::incfs::Features::core;
-        }
-    }
-
-    return android::incfs::Features(res);
-}
-
-IncFsFeatures IncFs_Features() {
-    return IncFsFeatures(readIncFsFeatures());
-}
-
 static bool isFsAvailable() {
     static const char kProcFilesystems[] = "/proc/filesystems";
     std::string filesystems;
-    if (!android::base::ReadFileToString(kProcFilesystems, &filesystems)) {
+    if (!ab::ReadFileToString(kProcFilesystems, &filesystems)) {
         return false;
     }
-    return filesystems.find("\t" INCFS_NAME "\n") != std::string::npos;
+    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<uint64_t>("ro.product.first_api_level", 0);
+    LOG(INFO) << "Initial API level of the device: " << api_level;
+    return api_level;
 }
 
 static std::string_view incFsPropertyValue() {
-    static const android::base::NoDestructor<std::string> kValue{
-            android::sysprop::IncrementalProperties::enable().value_or("")};
+    constexpr const int R_API = 30;
+    static const auto kDefaultValue{getFirstApiLevel() > R_API ? "on" : ""};
+    static const ab::NoDestructor<std::string> kValue{
+            IncrementalProperties::enable().value_or(kDefaultValue)};
+    LOG(INFO) << "ro.incremental.enable: " << *kValue;
     return *kValue;
 }
 
 static std::pair<bool, std::string_view> parseProperty(std::string_view property) {
-    auto boolVal = android::base::ParseBool(property);
-    if (boolVal == android::base::ParseBoolResult::kTrue) {
+    auto boolVal = ab::ParseBool(property);
+    if (boolVal == ab::ParseBoolResult::kTrue) {
         return {isFsAvailable(), {}};
     }
-    if (boolVal == android::base::ParseBoolResult::kFalse) {
+    if (boolVal == ab::ParseBoolResult::kFalse) {
         return {false, {}};
     }
 
@@ -156,11 +150,34 @@
     static const auto kModulePrefix = "module:"sv;
     if (property.starts_with(kModulePrefix)) {
         const auto modulePath = property.substr(kModulePrefix.size());
-        return {::access(android::incfs::details::c_str(modulePath), R_OK | X_OK), modulePath};
+        return {::access(details::c_str(modulePath), R_OK | X_OK), modulePath};
     }
     return {false, {}};
 }
 
+template <class Callback>
+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 {
@@ -186,9 +203,14 @@
             return true;
         }
         std::call_once(loadedFlag_, [this] {
-            const unique_fd fd(
-                    TEMP_FAILURE_RETRY(::open(android::incfs::details::c_str(moduleName_),
-                                              O_RDONLY | O_NOFOLLOW | O_CLOEXEC)));
+            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;
@@ -227,7 +249,39 @@
     return init().enabled();
 }
 
-bool isIncFsFd(int fd) {
+static Features readIncFsFeatures() {
+    init().enabledAndReady();
+
+    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::none;
+    }
+
+    int res = Features::none;
+    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;
@@ -237,7 +291,7 @@
     return fs.f_type == (decltype(fs.f_type))INCFS_MAGIC_NUMBER;
 }
 
-bool isIncFsPath(const char* path) {
+bool isIncFsPathImpl(const char* path) {
     struct statfs fs = {};
     if (::statfs(path, &fs) != 0) {
         PLOG(WARNING) << __func__ << "(): could not statfs " << path;
@@ -273,14 +327,14 @@
     if (const auto err = isDir(path); err != 0) {
         return err;
     }
-    if (const auto err = android::incfs::path::isEmptyDir(path); err != 0) {
+    if (const auto err = path::isEmptyDir(path); err != 0) {
         return err;
     }
     return 0;
 }
 
 static int rmDirContent(const char* path) {
-    auto dir = android::incfs::path::openDir(path);
+    auto dir = path::openDir(path);
     if (!dir) {
         return -EINVAL;
     }
@@ -288,7 +342,7 @@
         if (entry->d_name == "."sv || entry->d_name == ".."sv) {
             continue;
         }
-        auto fullPath = StringPrintf("%s/%s", path, entry->d_name);
+        auto fullPath = ab::StringPrintf("%s/%s", path, entry->d_name);
         if (entry->d_type == DT_DIR) {
             if (const auto err = rmDirContent(fullPath.c_str()); err != 0) {
                 return err;
@@ -306,11 +360,18 @@
 }
 
 static std::string makeMountOptionsString(IncFsMountOptions options) {
-    return 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));
+    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(const char* root) {
@@ -318,17 +379,30 @@
     if (!cmd.ok()) {
         return nullptr;
     }
-    unique_fd pendingReads(fcntl(cmd.get(), F_DUPFD_CLOEXEC, cmd.get()));
+    ab::unique_fd pendingReads(fcntl(cmd.get(), F_DUPFD_CLOEXEC, cmd.get()));
     if (!pendingReads.ok()) {
         return nullptr;
     }
     auto logs = openRaw(root, INCFS_LOG_FILENAME);
-    // logs may be absent, that's fine
-    auto control = IncFs_CreateControl(cmd.get(), pendingReads.get(), logs.get());
+    if (!logs.ok()) {
+        return nullptr;
+    }
+    ab::unique_fd blocksWritten;
+    if (features() & Features::v2) {
+        blocksWritten = openRaw(root, 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;
 }
@@ -339,7 +413,7 @@
         return {};
     }
     // TODO: add "/.cmd/" if we decide to use a separate control tree.
-    return android::incfs::path::join(itemRoot, subpath);
+    return path::join(itemRoot, subpath);
 }
 
 static void toString(IncFsFileId id, char* out) {
@@ -416,18 +490,24 @@
 }
 
 static bool restoreconControlFiles(std::string_view targetDir) {
-    const std::string controlFilePaths[] =
-            {android::incfs::path::join(targetDir, INCFS_PENDING_READS_FILENAME),
-             android::incfs::path::join(targetDir, INCFS_LOG_FILENAME)};
-    for (size_t i = 0; i < std::size(controlFilePaths); i++) {
-        if (const auto err = selinux_android_restorecon(controlFilePaths[i].c_str(),
-                                                        SELINUX_ANDROID_RESTORECON_FORCE);
+    static constexpr auto restorecon = [](const char* name) {
+        if (const auto err = selinux_android_restorecon(name, SELINUX_ANDROID_RESTORECON_FORCE);
             err != 0) {
-            PLOG(ERROR) << "[incfs] Failed to restorecon: " << controlFilePaths[i]
-                        << " error code: " << err;
             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;
 }
@@ -449,8 +529,8 @@
         return nullptr;
     }
 
-    if (options.flags & android::incfs::createOnly) {
-        if (const auto err = android::incfs::path::isEmptyDir(backingPath); err != 0) {
+    if (options.flags & createOnly) {
+        if (const auto err = path::isEmptyDir(backingPath); err != 0) {
             errno = -err;
             return nullptr;
         }
@@ -464,17 +544,18 @@
     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
-                    << " errno: " << errno;
+        PLOG(ERROR) << "[incfs] Failed to mount IncFS filesystem: " << targetDir;
         return nullptr;
     }
 
     if (!restoreconControlFiles(targetDir)) {
+        (void)IncFs_Unmount(targetDir);
         return nullptr;
     }
 
     auto control = makeControl(targetDir);
     if (control == nullptr) {
+        (void)IncFs_Unmount(targetDir);
         return nullptr;
     }
     return control;
@@ -486,7 +567,7 @@
         errno = EINVAL;
         return nullptr;
     }
-    return makeControl(android::incfs::details::c_str(root));
+    return makeControl(details::c_str(root));
 }
 
 IncFsFd IncFs_GetControlFd(const IncFsControl* control, IncFsFdType type) {
@@ -500,6 +581,8 @@
             return control->pendingReads;
         case LOGS:
             return control->logs;
+        case BLOCKS_WRITTEN:
+            return control->blocksWritten;
         default:
             return -EINVAL;
     }
@@ -515,11 +598,13 @@
     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) {
-    return new IncFsControl(cmd, pendingReads, logs);
+IncFsControl* IncFs_CreateControl(IncFsFd cmd, IncFsFd pendingReads, IncFsFd logs,
+                                  IncFsFd blocksWritten) {
+    return new IncFsControl(cmd, pendingReads, logs, blocksWritten);
 }
 
 void IncFs_DeleteControl(IncFsControl* control) {
@@ -533,6 +618,9 @@
         if (control->logs >= 0) {
             close(control->logs);
         }
+        if (control->blocksWritten >= 0) {
+            close(control->blocksWritten);
+        }
         delete control;
     }
 }
@@ -677,7 +765,7 @@
         return -ERANGE;
     }
 
-    const auto [subdir, name] = android::incfs::path::splitDirBase(subpath);
+    const auto [subdir, name] = path::splitDirBase(subpath);
     incfs_new_file_args args = {
             .size = (uint64_t)params.size,
             .mode = (uint16_t)mode,
@@ -700,13 +788,55 @@
                       << " of " << params.size << " bytes";
         return -errno;
     }
-    if (::chmod(android::incfs::path::join(root, subpath).c_str(), mode)) {
+    if (::chmod(path::join(root, subpath).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)) {
@@ -720,7 +850,7 @@
 
 static IncFsErrorCode makeDirs(std::string_view commandPath, std::string_view path,
                                std::string_view root, int32_t mode) {
-    auto commandCPath = android::incfs::details::c_str(commandPath);
+    auto commandCPath = details::c_str(commandPath);
     const auto mkdirRes = makeDir(commandCPath, mode, true);
     if (!mkdirRes) {
         return 0;
@@ -730,13 +860,13 @@
         return mkdirRes;
     }
 
-    const auto parent = android::incfs::path::dirName(commandPath);
-    if (!android::incfs::path::startsWith(parent, root)) {
+    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, android::incfs::path::dirName(path), root, mode)) {
+    if (auto parentMkdirRes = makeDirs(parent, path::dirName(path), root, mode)) {
         return parentMkdirRes;
     }
     return makeDir(commandCPath, mode, true);
@@ -781,10 +911,10 @@
 }
 
 static IncFsErrorCode getMetadata(const char* path, char buffer[], size_t* bufferSize) {
-    const auto res = ::getxattr(path, android::incfs::kMetadataAttrName, buffer, *bufferSize);
+    const auto res = ::getxattr(path, kMetadataAttrName, buffer, *bufferSize);
     if (res < 0) {
         if (errno == ERANGE) {
-            auto neededSize = ::getxattr(path, android::incfs::kMetadataAttrName, buffer, 0);
+            auto neededSize = ::getxattr(path, kMetadataAttrName, buffer, 0);
             if (neededSize >= 0) {
                 *bufferSize = neededSize;
                 return 0;
@@ -806,8 +936,8 @@
     if (root.empty()) {
         return -EINVAL;
     }
-    auto name = android::incfs::path::join(root, android::incfs::kIndexDir, toStringImpl(fileId));
-    return getMetadata(android::incfs::details::c_str(name), buffer, bufferSize);
+    auto name = indexPath(root, fileId);
+    return getMetadata(details::c_str(name), buffer, bufferSize);
 }
 
 IncFsErrorCode IncFs_GetMetadataByPath(const IncFsControl* control, const char* path, char buffer[],
@@ -824,6 +954,16 @@
     return getMetadata(path, buffer, bufferSize);
 }
 
+template <class GetterFunc, class Param>
+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;
@@ -834,12 +974,7 @@
         errno = EINVAL;
         return kIncFsInvalidFileId;
     }
-    char buffer[kIncFsFileIdStringLength];
-    const auto res = ::getxattr(path, android::incfs::kIdAttrName, buffer, sizeof(buffer));
-    if (res != sizeof(buffer)) {
-        return kIncFsInvalidFileId;
-    }
-    return toFileIdImpl({buffer, std::size(buffer)});
+    return getId(::getxattr, path);
 }
 
 static IncFsErrorCode getSignature(int fd, char buffer[], size_t* bufferSize) {
@@ -869,7 +1004,7 @@
     if (root.empty()) {
         return -EINVAL;
     }
-    auto file = android::incfs::path::join(root, android::incfs::kIndexDir, toStringImpl(fileId));
+    auto file = indexPath(root, fileId);
     auto fd = openRaw(file);
     if (fd < 0) {
         return fd.get();
@@ -950,16 +1085,17 @@
     return 0;
 }
 
-static int waitForReads(int fd, int32_t timeoutMs, incfs_pending_read_info pendingReadsBuffer[],
-                        size_t* pendingReadsBufferSize) {
-    auto hrTimeout = std::chrono::steady_clock::duration(std::chrono::milliseconds(timeoutMs));
+template <class RawPendingRead>
+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 = std::chrono::steady_clock::now();
+        const auto startTs = steady_clock::now();
 
         pollfd pfd = {fd, POLLIN, 0};
-        const auto res =
-                ::poll(&pfd, 1, duration_cast<std::chrono::milliseconds>(hrTimeout).count());
+        const auto res = ::poll(&pfd, 1, duration_cast<milliseconds>(hrTimeout).count());
         if (res > 0) {
             break;
         }
@@ -974,7 +1110,7 @@
             PLOG(ERROR) << "poll() failed";
             return -error;
         }
-        hrTimeout -= std::chrono::steady_clock::now() - startTs;
+        hrTimeout -= steady_clock::now() - startTs;
     }
     if (!pendingReadsBuffer) {
         return hrTimeout < hrTimeout.zero() ? -ETIMEDOUT : 0;
@@ -999,57 +1135,85 @@
     return 0;
 }
 
+template <class PublicPendingRead, class RawPendingRead>
+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<PublicPendingRead, IncFsReadInfoWithUid>) {
+        if constexpr (std::is_same_v<RawPendingRead, incfs_pending_read_info2>) {
+            res.uid = rawRead.uid;
+        } else {
+            res.uid = kIncFsNoUid;
+        }
+    }
+    return res;
+}
+
+template <class RawPendingRead, class PublicPendingRead>
+static int waitForReads(IncFsFd readFd, int32_t timeoutMs, PublicPendingRead buffer[],
+                        size_t* bufferSize) {
+    std::vector<RawPendingRead> 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<PublicPendingRead>(pendingReads[i]);
+    }
+    return 0;
+}
+
+template <class PublicPendingRead>
+static int waitForReads(IncFsFd readFd, int32_t timeoutMs, PublicPendingRead buffer[],
+                        size_t* bufferSize) {
+    if (features() & Features::v2) {
+        return waitForReads<incfs_pending_read_info2>(readFd, timeoutMs, buffer, bufferSize);
+    }
+    return waitForReads<incfs_pending_read_info>(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;
     }
 
-    std::vector<incfs_pending_read_info> pendingReads;
-    pendingReads.resize(*bufferSize);
-    if (const auto res =
-                waitForReads(control->pendingReads, timeoutMs, pendingReads.data(), bufferSize)) {
-        return res;
+    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;
     }
-    for (size_t i = 0; i != *bufferSize; ++i) {
-        buffer[i] = IncFsReadInfo{
-                .bootClockTsUs = pendingReads[i].timestamp_us,
-                .block = (IncFsBlockIndex)pendingReads[i].block_index,
-                .serialNo = pendingReads[i].serial_number,
-        };
-        memcpy(&buffer[i].id.data, pendingReads[i].file_id.bytes, sizeof(buffer[i].id.data));
-    }
-    return 0;
+
+    return waitForReads(control->pendingReads, timeoutMs, buffer, bufferSize);
 }
 
 IncFsErrorCode IncFs_WaitForPageReads(const IncFsControl* control, int32_t timeoutMs,
                                       IncFsReadInfo buffer[], size_t* bufferSize) {
-    if (!control) {
+    if (!control || control->logs < 0) {
         return -EINVAL;
     }
 
-    auto logsFd = control->logs;
-    if (logsFd < 0) {
+    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;
     }
-    std::vector<incfs_pending_read_info> pendingReads;
-    pendingReads.resize(*bufferSize);
-    if (const auto res = waitForReads(logsFd, timeoutMs, pendingReads.data(), bufferSize)) {
-        return res;
-    }
-    for (size_t i = 0; i != *bufferSize; ++i) {
-        buffer[i] = IncFsReadInfo{
-                .bootClockTsUs = pendingReads[i].timestamp_us,
-                .block = (IncFsBlockIndex)pendingReads[i].block_index,
-                .serialNo = pendingReads[i].serial_number,
-        };
-        memcpy(&buffer[i].id.data, pendingReads[i].file_id.bytes, sizeof(buffer[i].id.data));
-    }
-    return 0;
+
+    return waitForReads(control->logs, timeoutMs, buffer, bufferSize);
 }
 
 static IncFsFd openForSpecialOps(int cmd, const char* path) {
-    unique_fd fd(::open(path, O_RDONLY | O_CLOEXEC));
+    ab::unique_fd fd(::open(path, O_RDONLY | O_CLOEXEC));
     if (fd < 0) {
         return -errno;
     }
@@ -1085,7 +1249,7 @@
     if (root.empty()) {
         return -EINVAL;
     }
-    auto name = android::incfs::path::join(root, android::incfs::kIndexDir, toStringImpl(id));
+    auto name = indexPath(root, id);
     return openForSpecialOps(cmd, makeCommandPath(root, name).c_str());
 }
 
@@ -1156,7 +1320,7 @@
 }
 
 IncFsErrorCode IncFs_BindMount(const char* sourceDir, const char* targetDir) {
-    if (!android::incfs::enabled()) {
+    if (!enabled()) {
         return -ENOTSUP;
     }
 
@@ -1182,7 +1346,7 @@
 }
 
 IncFsErrorCode IncFs_Unmount(const char* dir) {
-    if (!android::incfs::enabled()) {
+    if (!enabled()) {
         return -ENOTSUP;
     }
 
@@ -1201,11 +1365,11 @@
 }
 
 bool IncFs_IsIncFsFd(int fd) {
-    return isIncFsFd(fd);
+    return isIncFsFdImpl(fd);
 }
 
 bool IncFs_IsIncFsPath(const char* path) {
-    return isIncFsPath(path);
+    return isIncFsPathImpl(path);
 }
 
 IncFsErrorCode IncFs_GetFilledRanges(int fd, IncFsSpan outBuffer, IncFsFilledRanges* filledRanges) {
@@ -1314,7 +1478,17 @@
     return -error;
 }
 
-IncFsErrorCode IncFs_IsFullyLoaded(int fd) {
+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)},
@@ -1352,6 +1526,554 @@
     return -ENODATA;
 }
 
-android::incfs::MountRegistry& android::incfs::defaultMountRegistry() {
+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 -errno;
+        }
+        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<incfs_per_uid_read_timeouts> 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<incfs_per_uid_read_timeouts> 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;
+}
+
+static IncFsErrorCode getFileBlockCount(int fd, IncFsBlockCounts* blockCount) {
+    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<milliseconds>(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<milliseconds>(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 <class IntType>
+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<int>(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<IncFsBlockIndex>(args.page_out),
+            .errorNo = args.errno_out,
+            .uid = static_cast<IncFsUid>(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();
 }
diff --git a/incfs/include/MountRegistry.h b/incfs/include/MountRegistry.h
index f8cfe4d..1ff320f 100644
--- a/incfs/include/MountRegistry.h
+++ b/incfs/include/MountRegistry.h
@@ -35,8 +35,12 @@
 
 class MountRegistry final {
 public:
+    struct Bind {
+        std::string subdir;
+        int rootIndex;
+    };
     // std::less<> enables heterogeneous lookups, e.g. by a string_view
-    using BindMap = std::map<std::string, std::pair<std::string, int>, std::less<>>;
+    using BindMap = std::map<std::string, Bind, std::less<>>;
 
     class Mounts final {
         struct Root {
@@ -80,7 +84,7 @@
         bool empty() const { return roots.empty(); }
 
         std::string_view rootFor(std::string_view path) const;
-        std::pair<std::string_view, std::string> rootAndSubpathFor(std::string_view path) const;
+        std::pair<const Root*, std::string> rootAndSubpathFor(std::string_view path) const;
 
         void swap(Mounts& other);
         void clear();
@@ -88,7 +92,6 @@
         void addRoot(std::string_view root, std::string_view backingDir);
         void removeRoot(std::string_view root);
         void addBind(std::string_view what, std::string_view where);
-        void moveBind(std::string_view src, std::string_view dest);
         void removeBind(std::string_view what);
 
     private:
@@ -103,6 +106,14 @@
 
     std::string rootFor(std::string_view path);
     std::pair<std::string, std::string> rootAndSubpathFor(std::string_view path);
+
+    struct Details {
+        std::string root;
+        std::string backing;
+        std::string subpath;
+    };
+    Details detailsFor(std::string_view path);
+
     Mounts copyMounts();
 
     void reload();
diff --git a/incfs/include/incfs.h b/incfs/include/incfs.h
index 1ecefaa..76619c2 100644
--- a/incfs/include/incfs.h
+++ b/incfs/include/incfs.h
@@ -20,6 +20,7 @@
 #include <array>
 #include <chrono>
 #include <functional>
+#include <optional>
 #include <string>
 #include <string_view>
 #include <utility>
@@ -39,6 +40,7 @@
 enum Features {
     none = INCFS_FEATURE_NONE,
     core = INCFS_FEATURE_CORE,
+    v2 = INCFS_FEATURE_V2,
 };
 
 enum class HashAlgorithm {
@@ -49,6 +51,7 @@
 enum class CompressionKind {
     none = INCFS_COMPRESSION_KIND_NONE,
     lz4 = INCFS_COMPRESSION_KIND_LZ4,
+    zstd = INCFS_COMPRESSION_KIND_ZSTD,
 };
 
 enum class BlockKind {
@@ -97,9 +100,12 @@
     IncFsFd cmd() const;
     IncFsFd pendingReads() const;
     IncFsFd logs() const;
-    operator IncFsControl*() const { return mControl; };
+    IncFsFd blocksWritten() const;
+
     void close();
 
+    operator IncFsControl*() const { return mControl; }
+
     using Fds = std::array<UniqueFd, IncFsFdType::FDS_COUNT>;
     [[nodiscard]] Fds releaseFds();
 
@@ -175,16 +181,24 @@
 using BlockIndex = IncFsBlockIndex;
 using ErrorCode = IncFsErrorCode;
 using Fd = IncFsFd;
+using Uid = IncFsUid;
 using ReadInfo = IncFsReadInfo;
+using ReadInfoWithUid = IncFsReadInfoWithUid;
 using RawMetadata = ByteBuffer;
 using RawSignature = ByteBuffer;
 using MountOptions = IncFsMountOptions;
 using DataBlock = IncFsDataBlock;
 using NewFileParams = IncFsNewFileParams;
+using NewMappedFileParams = IncFsNewMappedFileParams;
+using BlockCounts = IncFsBlockCounts;
+using UidReadTimeouts = IncFsUidReadTimeouts;
+using Metrics = IncFsMetrics;
+using LastReadError = IncFsLastReadError;
 
 constexpr auto kDefaultReadTimeout = std::chrono::milliseconds(INCFS_DEFAULT_READ_TIMEOUT_MS);
 constexpr int kBlockSize = INCFS_DATA_FILE_BLOCK_SIZE;
 const auto kInvalidFileId = kIncFsInvalidFileId;
+const auto kNoUid = kIncFsNoUid;
 
 bool enabled();
 Features features();
@@ -197,7 +211,7 @@
 UniqueControl mount(std::string_view backingPath, std::string_view targetDir,
                     IncFsMountOptions options);
 UniqueControl open(std::string_view dir);
-UniqueControl createControl(IncFsFd cmd, IncFsFd pendingReads, IncFsFd logs);
+UniqueControl createControl(IncFsFd cmd, IncFsFd pendingReads, IncFsFd logs, IncFsFd blocksWritten);
 
 ErrorCode setOptions(const Control& control, MountOptions newOptions);
 
@@ -208,6 +222,8 @@
 
 ErrorCode makeFile(const Control& control, std::string_view path, int mode, FileId fileId,
                    NewFileParams params);
+ErrorCode makeMappedFile(const Control& control, std::string_view path, int mode,
+                         NewMappedFileParams params);
 ErrorCode makeDir(const Control& control, std::string_view path, int mode = 0555);
 ErrorCode makeDirs(const Control& control, std::string_view path, int mode = 0555);
 
@@ -227,6 +243,10 @@
                                std::vector<ReadInfo>* pendingReadsBuffer);
 WaitResult waitForPageReads(const Control& control, std::chrono::milliseconds timeout,
                             std::vector<ReadInfo>* pageReadsBuffer);
+WaitResult waitForPendingReads(const Control& control, std::chrono::milliseconds timeout,
+                               std::vector<ReadInfoWithUid>* pendingReadsBuffer);
+WaitResult waitForPageReads(const Control& control, std::chrono::milliseconds timeout,
+                            std::vector<ReadInfoWithUid>* pageReadsBuffer);
 
 UniqueFd openForSpecialOps(const Control& control, FileId fileId);
 UniqueFd openForSpecialOps(const Control& control, std::string_view path);
@@ -236,8 +256,33 @@
 std::pair<ErrorCode, FilledRanges> getFilledRanges(int fd, FilledRanges::RangeBuffer&& buffer);
 std::pair<ErrorCode, FilledRanges> getFilledRanges(int fd, FilledRanges&& resumeFrom);
 
+ErrorCode setUidReadTimeouts(const Control& control, Span<const UidReadTimeouts> timeouts);
+std::optional<std::vector<UidReadTimeouts>> getUidReadTimeouts(const Control& control);
+
+std::optional<BlockCounts> getBlockCount(const Control& control, FileId fileId);
+std::optional<BlockCounts> getBlockCount(const Control& control, std::string_view path);
+
+std::optional<std::vector<FileId>> listIncompleteFiles(const Control& control);
+
+template <class Callback>
+ErrorCode forEachFile(const Control& control, Callback&& cb);
+template <class Callback>
+ErrorCode forEachIncompleteFile(const Control& control, Callback&& cb);
+
+WaitResult waitForLoadingComplete(const Control& control, std::chrono::milliseconds timeout);
+
 enum class LoadingState { Full, MissingBlocks };
 LoadingState isFullyLoaded(int fd);
+LoadingState isFullyLoaded(const Control& control, std::string_view path);
+LoadingState isFullyLoaded(const Control& control, FileId fileId);
+LoadingState isEverythingFullyLoaded(const Control& control);
+
+static const auto kTrimReservedSpace = kIncFsTrimReservedSpace;
+ErrorCode reserveSpace(const Control& control, std::string_view path, Size size);
+ErrorCode reserveSpace(const Control& control, FileId id, Size size);
+
+std::optional<Metrics> getMetrics(std::string_view sysfsName);
+std::optional<LastReadError> getLastReadError(const Control& control);
 
 // Some internal secret API as well that's not backed by C API yet.
 class MountRegistry;
diff --git a/incfs/include/incfs_inline.h b/incfs/include/incfs_inline.h
index 946255c..f3d1e89 100644
--- a/incfs/include/incfs_inline.h
+++ b/incfs/include/incfs_inline.h
@@ -28,14 +28,14 @@
 constexpr char kSizeAttrName[] = INCFS_XATTR_SIZE_NAME;
 constexpr char kMetadataAttrName[] = INCFS_XATTR_METADATA_NAME;
 
-constexpr char kIndexDir[] = ".index";
-
 namespace details {
 
 class CStrWrapper {
 public:
     CStrWrapper(std::string_view sv) {
-        if (sv[sv.size()] == '\0') {
+        if (!sv.data()) {
+            mCstr = "";
+        } else if (sv[sv.size()] == '\0') {
             mCstr = sv.data();
         } else {
             mCopy.emplace(sv);
@@ -116,6 +116,10 @@
     return IncFs_GetControlFd(mControl, LOGS);
 }
 
+inline IncFsFd UniqueControl::blocksWritten() const {
+    return IncFs_GetControlFd(mControl, BLOCKS_WRITTEN);
+}
+
 inline UniqueControl::Fds UniqueControl::releaseFds() {
     Fds result;
     IncFsFd fds[result.size()];
@@ -137,8 +141,9 @@
     return UniqueControl(control);
 }
 
-inline UniqueControl createControl(IncFsFd cmd, IncFsFd pendingReads, IncFsFd logs) {
-    return UniqueControl(IncFs_CreateControl(cmd, pendingReads, logs));
+inline UniqueControl createControl(IncFsFd cmd, IncFsFd pendingReads, IncFsFd logs,
+                                   IncFsFd blocksWritten) {
+    return UniqueControl(IncFs_CreateControl(cmd, pendingReads, logs, blocksWritten));
 }
 
 inline ErrorCode setOptions(const Control& control, MountOptions newOptions) {
@@ -169,6 +174,10 @@
                           NewFileParams params) {
     return IncFs_MakeFile(control, details::c_str(path), mode, fileId, params);
 }
+inline ErrorCode makeMappedFile(const Control& control, std::string_view path, int mode,
+                                NewMappedFileParams params) {
+    return IncFs_MakeMappedFile(control, details::c_str(path), mode, params);
+}
 inline ErrorCode makeDir(const Control& control, std::string_view path, int mode) {
     return IncFs_MakeDir(control, details::c_str(path), mode);
 }
@@ -229,15 +238,15 @@
     return IncFs_Unlink(control, details::c_str(path));
 }
 
-inline WaitResult waitForPendingReads(const Control& control, std::chrono::milliseconds timeout,
-                                      std::vector<ReadInfo>* pendingReadsBuffer) {
-    static constexpr auto kDefaultBufferSize = INCFS_DEFAULT_PENDING_READ_BUFFER_SIZE;
+template <class ReadInfoStruct, class Impl>
+WaitResult waitForReads(const Control& control, std::chrono::milliseconds timeout,
+                        std::vector<ReadInfoStruct>* pendingReadsBuffer, size_t defaultBufferSize,
+                        Impl impl) {
     if (pendingReadsBuffer->empty()) {
-        pendingReadsBuffer->resize(kDefaultBufferSize);
+        pendingReadsBuffer->resize(defaultBufferSize);
     }
     size_t size = pendingReadsBuffer->size();
-    IncFsErrorCode err =
-            IncFs_WaitForPendingReads(control, timeout.count(), pendingReadsBuffer->data(), &size);
+    IncFsErrorCode err = impl(control, timeout.count(), pendingReadsBuffer->data(), &size);
     pendingReadsBuffer->resize(size);
     switch (err) {
         case 0:
@@ -248,24 +257,32 @@
     return WaitResult(err);
 }
 
+inline WaitResult waitForPendingReads(const Control& control, std::chrono::milliseconds timeout,
+                                      std::vector<ReadInfo>* pendingReadsBuffer) {
+    return waitForReads(control, timeout, pendingReadsBuffer,
+                        INCFS_DEFAULT_PENDING_READ_BUFFER_SIZE, IncFs_WaitForPendingReads);
+}
+
+inline WaitResult waitForPendingReads(const Control& control, std::chrono::milliseconds timeout,
+                                      std::vector<ReadInfoWithUid>* pendingReadsBuffer) {
+    return waitForReads(control, timeout, pendingReadsBuffer,
+                        INCFS_DEFAULT_PENDING_READ_BUFFER_SIZE, IncFs_WaitForPendingReadsWithUid);
+}
+
 inline WaitResult waitForPageReads(const Control& control, std::chrono::milliseconds timeout,
                                    std::vector<ReadInfo>* pageReadsBuffer) {
     static constexpr auto kDefaultBufferSize =
             INCFS_DEFAULT_PAGE_READ_BUFFER_PAGES * PAGE_SIZE / sizeof(ReadInfo);
-    if (pageReadsBuffer->empty()) {
-        pageReadsBuffer->resize(kDefaultBufferSize);
-    }
-    size_t size = pageReadsBuffer->size();
-    IncFsErrorCode err =
-            IncFs_WaitForPageReads(control, timeout.count(), pageReadsBuffer->data(), &size);
-    pageReadsBuffer->resize(size);
-    switch (err) {
-        case 0:
-            return WaitResult::HaveData;
-        case -ETIMEDOUT:
-            return WaitResult::Timeout;
-    }
-    return WaitResult(err);
+    return waitForReads(control, timeout, pageReadsBuffer, kDefaultBufferSize,
+                        IncFs_WaitForPageReads);
+}
+
+inline WaitResult waitForPageReads(const Control& control, std::chrono::milliseconds timeout,
+                                   std::vector<ReadInfoWithUid>* pageReadsBuffer) {
+    static constexpr auto kDefaultBufferSize =
+            INCFS_DEFAULT_PAGE_READ_BUFFER_PAGES * PAGE_SIZE / sizeof(ReadInfoWithUid);
+    return waitForReads(control, timeout, pageReadsBuffer, kDefaultBufferSize,
+                        IncFs_WaitForPageReadsWithUid);
 }
 
 inline UniqueFd openForSpecialOps(const Control& control, FileId fileId) {
@@ -320,8 +337,7 @@
     return {res, FilledRanges(std::move(buffer), rawRanges)};
 }
 
-inline LoadingState isFullyLoaded(int fd) {
-    auto res = IncFs_IsFullyLoaded(fd);
+inline LoadingState toLoadingState(IncFsErrorCode res) {
     switch (res) {
         case 0:
             return LoadingState::Full;
@@ -332,6 +348,138 @@
     }
 }
 
+inline LoadingState isFullyLoaded(int fd) {
+    return toLoadingState(IncFs_IsFullyLoaded(fd));
+}
+inline LoadingState isFullyLoaded(const Control& control, std::string_view path) {
+    return toLoadingState(IncFs_IsFullyLoadedByPath(control, details::c_str(path)));
+}
+inline LoadingState isFullyLoaded(const Control& control, FileId fileId) {
+    return toLoadingState(IncFs_IsFullyLoadedById(control, fileId));
+}
+
+inline LoadingState isEverythingFullyLoaded(const Control& control) {
+    return toLoadingState(IncFs_IsEverythingFullyLoaded(control));
+}
+
+inline std::optional<std::vector<FileId>> listIncompleteFiles(const Control& control) {
+    std::vector<FileId> ids(32);
+    size_t count = ids.size();
+    auto err = IncFs_ListIncompleteFiles(control, ids.data(), &count);
+    if (err == -E2BIG) {
+        ids.resize(count);
+        err = IncFs_ListIncompleteFiles(control, ids.data(), &count);
+    }
+    if (err) {
+        errno = -err;
+        return {};
+    }
+    ids.resize(count);
+    return std::move(ids);
+}
+
+template <class Callback>
+inline ErrorCode forEachFile(const Control& control, Callback&& cb) {
+    struct Context {
+        const Control& c;
+        const Callback& cb;
+    } context = {control, cb};
+    return IncFs_ForEachFile(control, &context, [](void* pcontext, const IncFsControl*, FileId id) {
+        const auto context = (Context*)pcontext;
+        return context->cb(context->c, id);
+    });
+}
+template <class Callback>
+inline ErrorCode forEachIncompleteFile(const Control& control, Callback&& cb) {
+    struct Context {
+        const Control& c;
+        const Callback& cb;
+    } context = {control, cb};
+    return IncFs_ForEachIncompleteFile(control, &context,
+                                       [](void* pcontext, const IncFsControl*, FileId id) {
+                                           const auto context = (Context*)pcontext;
+                                           return context->cb(context->c, id);
+                                       });
+}
+
+inline WaitResult waitForLoadingComplete(const Control& control,
+                                         std::chrono::milliseconds timeout) {
+    const auto res = IncFs_WaitForLoadingComplete(control, timeout.count());
+    switch (res) {
+        case 0:
+            return WaitResult::HaveData;
+        case -ETIMEDOUT:
+            return WaitResult::Timeout;
+        default:
+            return WaitResult(res);
+    }
+}
+
+inline std::optional<BlockCounts> getBlockCount(const Control& control, FileId fileId) {
+    BlockCounts counts;
+    auto res = IncFs_GetFileBlockCountById(control, fileId, &counts);
+    if (res) {
+        errno = -res;
+        return {};
+    }
+    return counts;
+}
+
+inline std::optional<BlockCounts> getBlockCount(const Control& control, std::string_view path) {
+    BlockCounts counts;
+    auto res = IncFs_GetFileBlockCountByPath(control, details::c_str(path), &counts);
+    if (res) {
+        errno = -res;
+        return {};
+    }
+    return counts;
+}
+
+inline ErrorCode setUidReadTimeouts(const Control& control, Span<const UidReadTimeouts> timeouts) {
+    return IncFs_SetUidReadTimeouts(control, timeouts.data(), timeouts.size());
+}
+
+inline std::optional<std::vector<UidReadTimeouts>> getUidReadTimeouts(const Control& control) {
+    std::vector<UidReadTimeouts> timeouts(32);
+    size_t count = timeouts.size();
+    auto res = IncFs_GetUidReadTimeouts(control, timeouts.data(), &count);
+    if (res == -E2BIG) {
+        timeouts.resize(count);
+        res = IncFs_GetUidReadTimeouts(control, timeouts.data(), &count);
+    }
+    if (res) {
+        errno = -res;
+        return {};
+    }
+    timeouts.resize(count);
+    return std::move(timeouts);
+}
+
+inline ErrorCode reserveSpace(const Control& control, std::string_view path, Size size) {
+    return IncFs_ReserveSpaceByPath(control, details::c_str(path), size);
+}
+inline ErrorCode reserveSpace(const Control& control, FileId id, Size size) {
+    return IncFs_ReserveSpaceById(control, id, size);
+}
+
+inline std::optional<Metrics> getMetrics(std::string_view sysfsName) {
+    Metrics metrics;
+    if (const auto res = IncFs_GetMetrics(details::c_str(sysfsName), &metrics); res < 0) {
+        errno = -res;
+        return {};
+    }
+    return metrics;
+}
+
+inline std::optional<LastReadError> getLastReadError(const Control& control) {
+    LastReadError lastReadError;
+    if (const auto res = IncFs_GetLastReadError(control, &lastReadError); res < 0) {
+        errno = -res;
+        return {};
+    }
+    return lastReadError;
+}
+
 } // namespace android::incfs
 
 inline bool operator==(const IncFsFileId& l, const IncFsFileId& r) {
diff --git a/incfs/include/incfs_ndk.h b/incfs/include/incfs_ndk.h
index cfc8f5d..2a538bc 100644
--- a/incfs/include/incfs_ndk.h
+++ b/incfs/include/incfs_ndk.h
@@ -44,6 +44,7 @@
 typedef enum {
     INCFS_FEATURE_NONE = 0,
     INCFS_FEATURE_CORE = 1,
+    INCFS_FEATURE_V2 = 2,
 } IncFsFeatures;
 
 typedef int IncFsErrorCode;
@@ -51,6 +52,9 @@
 typedef int32_t IncFsBlockIndex;
 typedef int IncFsFd;
 typedef struct IncFsControl IncFsControl;
+typedef int IncFsUid;
+
+static const IncFsUid kIncFsNoUid = -1;
 
 typedef struct {
     const char* data;
@@ -61,6 +65,7 @@
     CMD,
     PENDING_READS,
     LOGS,
+    BLOCKS_WRITTEN,
     FDS_COUNT,
 } IncFsFdType;
 
@@ -85,11 +90,13 @@
     int32_t defaultReadTimeoutMs;
     int32_t readLogBufferPages;
     int32_t readLogDisableAfterTimeoutMs;
+    const char* sysfsName;
 } IncFsMountOptions;
 
 typedef enum {
     INCFS_COMPRESSION_KIND_NONE,
     INCFS_COMPRESSION_KIND_LZ4,
+    INCFS_COMPRESSION_KIND_ZSTD,
 } IncFsCompressionKind;
 
 typedef enum {
@@ -113,6 +120,12 @@
 } IncFsNewFileParams;
 
 typedef struct {
+    IncFsFileId sourceId;
+    IncFsSize sourceOffset;
+    IncFsSize size;
+} IncFsNewMappedFileParams;
+
+typedef struct {
     IncFsFileId id;
     uint64_t bootClockTsUs;
     IncFsBlockIndex block;
@@ -120,6 +133,14 @@
 } IncFsReadInfo;
 
 typedef struct {
+    IncFsFileId id;
+    uint64_t bootClockTsUs;
+    IncFsBlockIndex block;
+    uint32_t serialNo;
+    IncFsUid uid;
+} IncFsReadInfoWithUid;
+
+typedef struct {
     IncFsBlockIndex begin;
     IncFsBlockIndex end;
 } IncFsBlockRange;
@@ -132,6 +153,40 @@
     IncFsBlockIndex endIndex;
 } IncFsFilledRanges;
 
+typedef struct {
+    IncFsSize totalDataBlocks;
+    IncFsSize filledDataBlocks;
+    IncFsSize totalHashBlocks;
+    IncFsSize filledHashBlocks;
+} IncFsBlockCounts;
+
+typedef struct {
+    IncFsUid uid;
+    uint32_t minTimeUs;
+    uint32_t minPendingTimeUs;
+    uint32_t maxPendingTimeUs;
+} IncFsUidReadTimeouts;
+
+typedef struct {
+    uint32_t readsDelayedMin;
+    uint64_t readsDelayedMinUs;
+    uint32_t readsDelayedPending;
+    uint64_t readsDelayedPendingUs;
+    uint32_t readsFailedHashVerification;
+    uint32_t readsFailedOther;
+    uint32_t readsFailedTimedOut;
+    uint64_t reserved;
+    uint64_t reserved1;
+} IncFsMetrics;
+
+typedef struct {
+    IncFsFileId id;
+    uint64_t timestampUs;
+    IncFsBlockIndex block;
+    uint32_t errorNo;
+    IncFsUid uid;
+} IncFsLastReadError;
+
 // All functions return -errno in case of failure.
 // All IncFsFd functions return >=0 in case of success.
 // All IncFsFileId functions return invalid IncFsFileId on error.
@@ -155,7 +210,8 @@
 IncFsControl* IncFs_Mount(const char* backingPath, const char* targetDir,
                           IncFsMountOptions options);
 IncFsControl* IncFs_Open(const char* dir);
-IncFsControl* IncFs_CreateControl(IncFsFd cmd, IncFsFd pendingReads, IncFsFd logs);
+IncFsControl* IncFs_CreateControl(IncFsFd cmd, IncFsFd pendingReads, IncFsFd logs,
+                                  IncFsFd blocksWritten);
 void IncFs_DeleteControl(IncFsControl* control);
 IncFsFd IncFs_GetControlFd(const IncFsControl* control, IncFsFdType type);
 IncFsSize IncFs_ReleaseControlFds(IncFsControl* control, IncFsFd out[], IncFsSize outSize);
@@ -169,6 +225,9 @@
 
 IncFsErrorCode IncFs_MakeFile(const IncFsControl* control, const char* path, int32_t mode,
                               IncFsFileId id, IncFsNewFileParams params);
+IncFsErrorCode IncFs_MakeMappedFile(const IncFsControl* control, const char* path, int32_t mode,
+                                    IncFsNewMappedFileParams params);
+
 IncFsErrorCode IncFs_MakeDir(const IncFsControl* control, const char* path, int32_t mode);
 IncFsErrorCode IncFs_MakeDirs(const IncFsControl* control, const char* path, int32_t mode);
 
@@ -194,11 +253,45 @@
 IncFsErrorCode IncFs_WaitForPageReads(const IncFsControl* control, int32_t timeoutMs,
                                       IncFsReadInfo buffer[], size_t* bufferSize);
 
+IncFsErrorCode IncFs_WaitForPendingReadsWithUid(const IncFsControl* control, int32_t timeoutMs,
+                                                IncFsReadInfoWithUid buffer[], size_t* bufferSize);
+IncFsErrorCode IncFs_WaitForPageReadsWithUid(const IncFsControl* control, int32_t timeoutMs,
+                                             IncFsReadInfoWithUid buffer[], size_t* bufferSize);
+
+IncFsErrorCode IncFs_WaitForFsWrittenBlocksChange(const IncFsControl* control, int32_t timeoutMs,
+                                                  IncFsSize* count);
+
 IncFsFd IncFs_OpenForSpecialOpsByPath(const IncFsControl* control, const char* path);
 IncFsFd IncFs_OpenForSpecialOpsById(const IncFsControl* control, IncFsFileId id);
 
 IncFsErrorCode IncFs_WriteBlocks(const IncFsDataBlock blocks[], size_t blocksCount);
 
+IncFsErrorCode IncFs_SetUidReadTimeouts(const IncFsControl* control,
+                                        const IncFsUidReadTimeouts timeouts[], size_t count);
+IncFsErrorCode IncFs_GetUidReadTimeouts(const IncFsControl* control,
+                                        IncFsUidReadTimeouts timeoutsBuffer[], size_t* bufferSize);
+
+IncFsErrorCode IncFs_GetFileBlockCountByPath(const IncFsControl* control, const char* path,
+                                             IncFsBlockCounts* blockCount);
+IncFsErrorCode IncFs_GetFileBlockCountById(const IncFsControl* control, IncFsFileId id,
+                                           IncFsBlockCounts* blockCount);
+
+IncFsErrorCode IncFs_ListIncompleteFiles(const IncFsControl* control, IncFsFileId ids[],
+                                         size_t* bufferSize);
+
+// Calls a passed callback for each file on the mounted filesystem, or, in the second case,
+// for each incomplete file (only for v2 IncFS).
+// Callback can stop the iteration early by returning |false|.
+// Return codes:
+// >=0      - number of files iterated,
+// <0       - -errno
+typedef bool (*FileCallback)(void* context, const IncFsControl* control, IncFsFileId fileId);
+IncFsErrorCode IncFs_ForEachFile(const IncFsControl* control, void* context, FileCallback cb);
+IncFsErrorCode IncFs_ForEachIncompleteFile(const IncFsControl* control, void* context,
+                                           FileCallback cb);
+
+IncFsErrorCode IncFs_WaitForLoadingComplete(const IncFsControl* control, int32_t timeoutMs);
+
 // Gets a collection of filled ranges in the file from IncFS. Uses the |outBuffer| memory, it has
 // to be big enough to fit all the ranges the caller is expecting.
 // Return codes:
@@ -214,6 +307,38 @@
 //  -ENODATA - some blocks are missing,
 //  <0       - error from the syscall.
 IncFsErrorCode IncFs_IsFullyLoaded(int fd);
+IncFsErrorCode IncFs_IsFullyLoadedByPath(const IncFsControl* control, const char* path);
+IncFsErrorCode IncFs_IsFullyLoadedById(const IncFsControl* control, IncFsFileId fileId);
+
+// Check if all files on the mount are fully loaded. Return codes:
+//  0        - fully loaded,
+//  -ENODATA - some blocks are missing,
+//  <0       - error from the syscall.
+IncFsErrorCode IncFs_IsEverythingFullyLoaded(const IncFsControl* control);
+
+// Reserve |size| bytes for the file. Trims reserved space to the current file size when |size = -1|
+static const IncFsSize kIncFsTrimReservedSpace = -1;
+IncFsErrorCode IncFs_ReserveSpaceByPath(const IncFsControl* control, const char* path,
+                                        IncFsSize size);
+IncFsErrorCode IncFs_ReserveSpaceById(const IncFsControl* control, IncFsFileId id, IncFsSize size);
+
+// Gets the metrics of a mount by specifying the corresponding sysfs subpath.
+// Return codes:
+// =0       - success
+// <0       - -errno
+IncFsErrorCode IncFs_GetMetrics(const char* sysfsName, IncFsMetrics* metrics);
+
+// Gets information about the last read error of a mount.
+// Return codes:
+// =0       - success
+// <0       - -errno
+// When there is no read error, still returns success. Fields in IncFsLastReadError will be all 0.
+// Possible values of IncFsLastReadError.errorNo:
+//   -ETIME for read timeout;
+//   -EBADMSG for hash verification failure;
+//   Other negative values for other types of errors.
+IncFsErrorCode IncFs_GetLastReadError(const IncFsControl* control,
+                                      IncFsLastReadError* lastReadError);
 
 __END_DECLS
 
diff --git a/incfs/path.h b/incfs/include/path.h
similarity index 62%
rename from incfs/path.h
rename to incfs/include/path.h
index f312d17..325eb1d 100644
--- a/incfs/path.h
+++ b/incfs/include/path.h
@@ -48,8 +48,43 @@
 std::string_view relativize(std::string_view parent, std::string&& nested) = delete;
 
 // Note: some system headers #define 'dirname' and 'basename' as macros
-std::string_view dirName(std::string_view path);
-std::string_view baseName(std::string_view path);
+
+inline std::string_view baseName(std::string_view path) {
+    using namespace std::literals;
+    if (path.empty()) {
+        return {};
+    }
+    if (path == "/"sv) {
+        return "/"sv;
+    }
+    auto pos = path.rfind('/');
+    while (!path.empty() && pos == path.size() - 1) {
+        path.remove_suffix(1);
+        pos = path.rfind('/');
+    }
+    if (pos == path.npos) {
+        return path.empty() ? "/"sv : path;
+    }
+    return path.substr(pos + 1);
+}
+
+inline std::string_view dirName(std::string_view path) {
+    using namespace std::literals;
+    if (path.empty()) {
+        return {};
+    }
+    if (path == "/"sv) {
+        return "/"sv;
+    }
+    const auto pos = path.rfind('/');
+    if (pos == 0) {
+        return "/"sv;
+    }
+    if (pos == path.npos) {
+        return "."sv;
+    }
+    return path.substr(0, pos);
+}
 
 // Split the |full| path into its directory and basename components.
 // This modifies the input string to null-terminate the output directory
@@ -60,20 +95,32 @@
 bool endsWith(std::string_view path, std::string_view prefix);
 
 inline auto openDir(const char* path) {
-    auto dir = std::unique_ptr<DIR, decltype(&closedir)>(::opendir(path), &::closedir);
+    struct closer {
+        void operator()(DIR* d) const { ::closedir(d); }
+    };
+    auto dir = std::unique_ptr<DIR, closer>(::opendir(path));
     return dir;
 }
 
 template <class... Paths>
-std::string join(std::string_view first, std::string_view second, Paths&&... paths) {
-    std::string result;
+std::string join(std::string&& first, std::string_view second, Paths&&... paths) {
+    std::string& result = first;
     {
         using std::size;
         result.reserve(first.size() + second.size() + 1 + (sizeof...(paths) + ... + size(paths)));
     }
-    result.assign(first);
-    (details::appendNextPath(result, second), ..., details::appendNextPath(result, paths));
+    (details::appendNextPath(result, second), ...,
+     details::appendNextPath(result, std::forward<Paths>(paths)));
     return result;
 }
 
+template <class... Paths>
+std::string join(std::string_view first, std::string_view second, Paths&&... paths) {
+    return join(std::string(), first, second, std::forward<Paths>(paths)...);
+}
+template <class... Paths>
+std::string join(const char* first, std::string_view second, Paths&&... paths) {
+    return path::join(std::string_view(first), second, std::forward<Paths>(paths)...);
+}
+
 } // namespace android::incfs::path
diff --git a/incfs/kernel-headers/linux/incrementalfs.h b/incfs/kernel-headers/linux/incrementalfs.h
index b9f4c7a..d164cd9 100644
--- a/incfs/kernel-headers/linux/incrementalfs.h
+++ b/incfs/kernel-headers/linux/incrementalfs.h
@@ -18,7 +18,7 @@
 
 /* ===== constants ===== */
 #define INCFS_NAME "incremental-fs"
-#define INCFS_MAGIC_NUMBER (0x5346434e49ul)
+#define INCFS_MAGIC_NUMBER (unsigned long)(0x5346434e49ul)
 #define INCFS_DATA_FILE_BLOCK_SIZE 4096
 #define INCFS_HEADER_VER 1
 
@@ -28,11 +28,15 @@
 #define INCFS_MAX_HASH_SIZE 32
 #define INCFS_MAX_FILE_ATTR_SIZE 512
 
+#define INCFS_INDEX_NAME ".index"
+#define INCFS_INCOMPLETE_NAME ".incomplete"
 #define INCFS_PENDING_READS_FILENAME ".pending_reads"
 #define INCFS_LOG_FILENAME ".log"
+#define INCFS_BLOCKS_WRITTEN_FILENAME ".blocks_written"
 #define INCFS_XATTR_ID_NAME (XATTR_USER_PREFIX "incfs.id")
 #define INCFS_XATTR_SIZE_NAME (XATTR_USER_PREFIX "incfs.size")
 #define INCFS_XATTR_METADATA_NAME (XATTR_USER_PREFIX "incfs.metadata")
+#define INCFS_XATTR_VERITY_NAME (XATTR_USER_PREFIX "incfs.verity")
 
 #define INCFS_MAX_SIGNATURE_SIZE 8096
 #define INCFS_SIGNATURE_VERSION 2
@@ -42,7 +46,10 @@
 
 /* ===== ioctl requests on the command dir ===== */
 
-/* Create a new file */
+/*
+ * Create a new file
+ * May only be called on .pending_reads file
+ */
 #define INCFS_IOC_CREATE_FILE \
 	_IOWR(INCFS_IOCTL_BASE_CODE, 30, struct incfs_new_file_args)
 
@@ -92,9 +99,73 @@
 #define INCFS_IOC_GET_FILLED_BLOCKS                                            \
 	_IOR(INCFS_IOCTL_BASE_CODE, 34, struct incfs_get_filled_blocks_args)
 
+/*
+ * Creates a new mapped file
+ * May only be called on .pending_reads file
+ */
+#define INCFS_IOC_CREATE_MAPPED_FILE \
+	_IOWR(INCFS_IOCTL_BASE_CODE, 35, struct incfs_create_mapped_file_args)
+
+/*
+ * Get number of blocks, total and filled
+ * May only be called on .pending_reads file
+ */
+#define INCFS_IOC_GET_BLOCK_COUNT \
+	_IOR(INCFS_IOCTL_BASE_CODE, 36, struct incfs_get_block_count_args)
+
+/*
+ * Get per UID read timeouts
+ * May only be called on .pending_reads file
+ */
+#define INCFS_IOC_GET_READ_TIMEOUTS \
+	_IOR(INCFS_IOCTL_BASE_CODE, 37, struct incfs_get_read_timeouts_args)
+
+/*
+ * Set per UID read timeouts
+ * May only be called on .pending_reads file
+ */
+#define INCFS_IOC_SET_READ_TIMEOUTS \
+	_IOW(INCFS_IOCTL_BASE_CODE, 38, struct incfs_set_read_timeouts_args)
+
+/*
+ * Get last read error
+ * May only be called on .pending_reads file
+ */
+#define INCFS_IOC_GET_LAST_READ_ERROR \
+	_IOW(INCFS_IOCTL_BASE_CODE, 39, struct incfs_get_last_read_error_args)
+
+/* ===== sysfs feature flags ===== */
+/*
+ * Each flag is represented by a file in /sys/fs/incremental-fs/features
+ * If the file exists the feature is supported
+ * Also the file contents will be the line "supported"
+ */
+
+/*
+ * Basic flag stating that the core incfs file system is available
+ */
+#define INCFS_FEATURE_FLAG_COREFS "corefs"
+
+/*
+ * zstd compression support
+ */
+#define INCFS_FEATURE_FLAG_ZSTD "zstd"
+
+/*
+ * v2 feature set support. Covers:
+ *   INCFS_IOC_CREATE_MAPPED_FILE
+ *   INCFS_IOC_GET_BLOCK_COUNT
+ *   INCFS_IOC_GET_READ_TIMEOUTS/INCFS_IOC_SET_READ_TIMEOUTS
+ *   .blocks_written status file
+ *   .incomplete folder
+ *   report_uid mount option
+ */
+#define INCFS_FEATURE_FLAG_V2 "v2"
+
 enum incfs_compression_alg {
 	COMPRESSION_NONE = 0,
-	COMPRESSION_LZ4 = 1
+	COMPRESSION_LZ4 = 1,
+	COMPRESSION_ZSTD = 2,
 };
 
 enum incfs_block_flags {
@@ -109,6 +180,8 @@
 /*
  * Description of a pending read. A pending read - a read call by
  * a userspace program for which the filesystem currently doesn't have data.
+ *
+ * Reads from .pending_reads and .log return an array of these structure
  */
 struct incfs_pending_read_info {
 	/* Id of a file that is being read from. */
@@ -125,6 +198,32 @@
 };
 
 /*
+ * Description of a pending read. A pending read - a read call by
+ * a userspace program for which the filesystem currently doesn't have data.
+ *
+ * This version of incfs_pending_read_info is used whenever the file system is
+ * mounted with the report_uid flag
+ */
+struct incfs_pending_read_info2 {
+	/* Id of a file that is being read from. */
+	incfs_uuid_t file_id;
+
+	/* A number of microseconds since system boot to the read. */
+	__aligned_u64 timestamp_us;
+
+	/* Index of a file block that is being read. */
+	__u32 block_index;
+
+	/* A serial number of this pending read. */
+	__u32 serial_number;
+
+	/* The UID of the reading process */
+	__u32 uid;
+
+	__u32 reserved;
+};
+
+/*
  * Description of a data or hash block to add to a data file.
  */
 struct incfs_fill_block {
@@ -321,7 +420,7 @@
 	/* Actual number of blocks in file */
 	__u32 total_blocks_out;
 
-	/* The number of data blocks in file */
+	/* The  number of data blocks in file */
 	__u32 data_blocks_out;
 
 	/* Number of bytes written to range buffer */
@@ -331,4 +430,154 @@
 	__u32 index_out;
 };
 
-#endif /* _UAPI_LINUX_INCREMENTALFS_H */
+/*
+ * Create a new mapped file
+ * Argument for INCFS_IOC_CREATE_MAPPED_FILE
+ */
+struct incfs_create_mapped_file_args {
+	/*
+	 * Total size of the new file.
+	 */
+	__aligned_u64 size;
+
+	/*
+	 * File mode. Permissions and dir flag.
+	 */
+	__u16 mode;
+
+	__u16 reserved1;
+
+	__u32 reserved2;
+
+	/*
+	 * A pointer to a null-terminated relative path to the incfs mount
+	 * point
+	 * Max length: PATH_MAX
+	 *
+	 * Equivalent to: char *directory_path;
+	 */
+	__aligned_u64 directory_path;
+
+	/*
+	 * A pointer to a null-terminated file name.
+	 * Max length: PATH_MAX
+	 *
+	 * Equivalent to: char *file_name;
+	 */
+	__aligned_u64 file_name;
+
+	/* Id of source file to map. */
+	incfs_uuid_t source_file_id;
+
+	/*
+	 * Offset in source file to start mapping. Must be a multiple of
+	 * INCFS_DATA_FILE_BLOCK_SIZE
+	 */
+	__aligned_u64 source_offset;
+};
+
+/*
+ * Get information about the blocks in this file
+ * Argument for INCFS_IOC_GET_BLOCK_COUNT
+ */
+struct incfs_get_block_count_args {
+	/* Total number of data blocks in the file */
+	__u32 total_data_blocks_out;
+
+	/* Number of filled data blocks in the file */
+	__u32 filled_data_blocks_out;
+
+	/* Total number of hash blocks in the file */
+	__u32 total_hash_blocks_out;
+
+	/* Number of filled hash blocks in the file */
+	__u32 filled_hash_blocks_out;
+};
+
+/* Description of timeouts for one UID */
+struct incfs_per_uid_read_timeouts {
+	/* UID to apply these timeouts to */
+	__u32 uid;
+
+	/*
+	 * Min time in microseconds to read any block. Note that this doesn't
+	 * apply to reads which are satisfied from the page cache.
+	 */
+	__u32 min_time_us;
+
+	/*
+	 * Min time in microseconds to satisfy a pending read. Any pending read
+	 * which is filled before this time will be delayed so that the total
+	 * read time >= this value.
+	 */
+	__u32 min_pending_time_us;
+
+	/*
+	 * Max time in microseconds to satisfy a pending read before the read
+	 * times out. If set to U32_MAX, defaults to mount options
+	 * read_timeout_ms * 1000. Must be >= min_pending_time_us
+	 */
+	__u32 max_pending_time_us;
+};
+
+/*
+ * Get the read timeouts array
+ * Argument for INCFS_IOC_GET_READ_TIMEOUTS
+ */
+struct incfs_get_read_timeouts_args {
+	/*
+	 * A pointer to a buffer to fill with the current timeouts
+	 *
+	 * Equivalent to struct incfs_per_uid_read_timeouts *
+	 */
+	__aligned_u64 timeouts_array;
+
+	/* Size of above buffer in bytes */
+	__u32 timeouts_array_size;
+
+	/* Size used in bytes, or size needed if -ENOMEM returned */
+	__u32 timeouts_array_size_out;
+};
+
+/*
+ * Set the read timeouts array
+ * Arguments for INCFS_IOC_SET_READ_TIMEOUTS
+ */
+struct incfs_set_read_timeouts_args {
+	/*
+	 * A pointer to an array containing the new timeouts
+	 * This will replace any existing timeouts
+	 *
+	 * Equivalent to struct incfs_per_uid_read_timeouts *
+	 */
+	__aligned_u64 timeouts_array;
+
+	/* Size of above array in bytes. Must be < 256 */
+	__u32 timeouts_array_size;
+};
+
+/*
+ * Get last read error struct
+ * Arguments for INCFS_IOC_GET_LAST_READ_ERROR
+ */
+struct incfs_get_last_read_error_args {
+	/* File id of last file that had a read error */
+	incfs_uuid_t	file_id_out;
+
+	/* Time of last read error, in us, from CLOCK_MONOTONIC */
+	__u64	time_us_out;
+
+	/* Index of page that was being read at last read error */
+	__u32	page_out;
+
+	/* errno of last read error */
+	__u32	errno_out;
+
+	/* uid of last read error */
+	__u32	uid_out;
+
+	__u32	reserved1;
+	__u64	reserved2;
+};
+
+#endif /* _UAPI_LINUX_INCREMENTALFS_H */
\ No newline at end of file
diff --git a/incfs/path.cpp b/incfs/path.cpp
index 8781571..f6f87ae 100644
--- a/incfs/path.cpp
+++ b/incfs/path.cpp
@@ -121,23 +121,19 @@
     snprintf(fdNameBuffer, std::size(fdNameBuffer), fdNameFormat, fd);
 
     std::string res;
-    // lstat() is supposed to return us exactly the needed buffer size, but
-    // somehow it may also return a smaller (but still >0) st_size field.
-    // That's why let's only use it for the initial estimate.
-    struct stat st = {};
-    if (::lstat(fdNameBuffer, &st) || st.st_size == 0) {
-        st.st_size = PATH_MAX;
-    }
-    auto bufSize = st.st_size;
+    // We used to call lstat() here to preallocate the buffer to the exact required size; turns out
+    // that call is significantly more expensive than anything else, so doing a couple extra
+    // iterations is worth the savings.
+    auto bufSize = 256;
     for (;;) {
-        res.resize(bufSize + 1, '\0');
+        res.resize(bufSize - 1, '\0');
         auto size = ::readlink(fdNameBuffer, &res[0], res.size());
         if (size < 0) {
             PLOG(ERROR) << "readlink failed for " << fdNameBuffer;
             return {};
         }
-        if (size > bufSize) {
-            // File got renamed in between lstat() and readlink() calls? Retry.
+        if (size >= ssize_t(res.size())) {
+            // can't tell if the name is exactly that long, or got truncated - just repeat the call.
             bufSize *= 2;
             continue;
         }
@@ -149,13 +145,14 @@
     }
 }
 
-static void preparePathComponent(std::string_view& path, bool trimFront) {
-    if (trimFront) {
-        while (!path.empty() && path.front() == '/') {
-            path.remove_prefix(1);
-        }
+static void preparePathComponent(std::string_view& path, bool trimAll) {
+    // need to check for double front slash as a single one has a separate meaning in front
+    while (!path.empty() && path.front() == '/' &&
+           (trimAll || (path.size() > 1 && path[1] == '/'))) {
+        path.remove_prefix(1);
     }
-    while (!path.empty() && path.back() == '/') {
+    // for the back we don't care about double-vs-single slash difference
+    while (path.size() > !trimAll && path.back() == '/') {
         path.remove_suffix(1);
     }
 }
@@ -178,7 +175,7 @@
 }
 
 void details::appendNextPath(std::string& res, std::string_view path) {
-    preparePathComponent(path, true);
+    preparePathComponent(path, !res.empty());
     if (path.empty()) {
         return;
     }
@@ -188,41 +185,6 @@
     res += path;
 }
 
-std::string_view baseName(std::string_view path) {
-    if (path.empty()) {
-        return {};
-    }
-    if (path == "/"sv) {
-        return "/"sv;
-    }
-    auto pos = path.rfind('/');
-    while (!path.empty() && pos == path.size() - 1) {
-        path.remove_suffix(1);
-        pos = path.rfind('/');
-    }
-    if (pos == path.npos) {
-        return path.empty() ? "/"sv : path;
-    }
-    return path.substr(pos + 1);
-}
-
-std::string_view dirName(std::string_view path) {
-    if (path.empty()) {
-        return {};
-    }
-    if (path == "/"sv) {
-        return "/"sv;
-    }
-    const auto pos = path.rfind('/');
-    if (pos == 0) {
-        return "/"sv;
-    }
-    if (pos == path.npos) {
-        return "."sv;
-    }
-    return path.substr(0, pos);
-}
-
 std::pair<std::string_view, std::string_view> splitDirBase(std::string& full) {
     auto res = std::pair(dirName(full), baseName(full));
     if (res.first.data() == full.data()) {
diff --git a/incfs/tests/MountRegistry_test.cpp b/incfs/tests/MountRegistry_test.cpp
index 2b82eb0..bb3ba49 100644
--- a/incfs/tests/MountRegistry_test.cpp
+++ b/incfs/tests/MountRegistry_test.cpp
@@ -14,6 +14,8 @@
  * limitations under the License.
  */
 
+#include "MountRegistry.h"
+
 #include <android-base/file.h>
 #include <android-base/logging.h>
 #include <android-base/unique_fd.h>
@@ -21,10 +23,11 @@
 #include <sys/select.h>
 #include <unistd.h>
 
+#include <iterator>
 #include <optional>
 #include <thread>
 
-#include "MountRegistry.h"
+#include "incfs.h"
 #include "path.h"
 
 using namespace android::incfs;
@@ -72,9 +75,97 @@
     ASSERT_STREQ("/root", r().rootFor("/bind").data());
     ASSERT_STREQ("/root", r().rootFor("/bind2").data());
     ASSERT_STREQ("/root", r().rootFor("/other/bind/dir").data());
-    ASSERT_EQ(std::pair("/root"sv, ""s), r().rootAndSubpathFor("/root"));
-    ASSERT_EQ(std::pair("/root"sv, "1"s), r().rootAndSubpathFor("/bind"));
-    ASSERT_EQ(std::pair("/root"sv, "2/3"s), r().rootAndSubpathFor("/bind2"));
-    ASSERT_EQ(std::pair("/root"sv, "2/3/blah"s), r().rootAndSubpathFor("/bind2/blah"));
-    ASSERT_EQ(std::pair("/root"sv, "2/3/blah"s), r().rootAndSubpathFor("/other/bind/blah"));
+    ASSERT_EQ("/root"s, r().rootAndSubpathFor("/root").first->path);
+    ASSERT_EQ(""s, r().rootAndSubpathFor("/root").second);
+    ASSERT_EQ("/root"s, r().rootAndSubpathFor("/bind").first->path);
+    ASSERT_EQ("1"s, r().rootAndSubpathFor("/bind").second);
+    ASSERT_EQ("/root"s, r().rootAndSubpathFor("/bind2").first->path);
+    ASSERT_EQ("2/3"s, r().rootAndSubpathFor("/bind2").second);
+    ASSERT_EQ("/root"s, r().rootAndSubpathFor("/bind2/blah").first->path);
+    ASSERT_EQ("2/3/blah"s, r().rootAndSubpathFor("/bind2/blah").second);
+    ASSERT_EQ("/root"s, r().rootAndSubpathFor("/other/bind/blah").first->path);
+    ASSERT_EQ("2/3/blah"s, r().rootAndSubpathFor("/other/bind/blah").second);
+}
+
+TEST_F(MountRegistryTest, MultiRoot) {
+    r().addRoot("/root", "/backing");
+    r().addBind("/root", "/bind");
+    ASSERT_STREQ("/root", r().rootFor("/root").data());
+    ASSERT_STREQ("/root", r().rootFor("/bind").data());
+    ASSERT_STREQ("/root", r().rootFor("/bind/2").data());
+}
+
+static MountRegistry::Mounts makeFrom(std::string_view str) {
+    TemporaryFile f;
+    EXPECT_TRUE(android::base::WriteFully(f.fd, str.data(), str.size()));
+    EXPECT_EQ(0, lseek(f.fd, 0, SEEK_SET)); // rewind
+
+    MountRegistry::Mounts m;
+    EXPECT_TRUE(m.loadFrom(f.fd, INCFS_NAME));
+    return std::move(m);
+}
+
+TEST_F(MountRegistryTest, MultiRootLoad) {
+    constexpr char mountsFile[] =
+            R"(4605 34 0:154 / /mnt/installer/0/0000000000000000000000000000CAFEF00D2019 rw,nosuid,nodev,noexec,noatime shared:45 master:43 - fuse /dev/fuse rw,lazytime,user_id=0,group_id=0,allow_other
+4561 35 0:154 / /mnt/androidwritable/0/0000000000000000000000000000CAFEF00D2019 rw,nosuid,nodev,noexec,noatime shared:44 master:43 - fuse /dev/fuse rw,lazytime,user_id=0,group_id=0,allow_other
+4560 99 0:154 / /storage/0000000000000000000000000000CAFEF00D2019 rw,nosuid,nodev,noexec,noatime master:43 - fuse /dev/fuse rw,lazytime,user_id=0,group_id=0,allow_other
+4650 30 0:44 /MyFiles /mnt/pass_through/0/0000000000000000000000000000CAFEF00D2019 rw,nosuid,nodev,noexec,relatime shared:31 - 9p media rw,sync,dirsync,access=client,trans=virtio
+3181 79 0:146 / /data/incremental/MT_data_app_vmdl703/mount rw,nosuid,nodev,noatime shared:46 - incremental-fs /data/incremental/MT_data_app_vmdl703/backing_store rw,seclabel,read_timeout_ms=10000,readahead=0
+3182 77 0:146 / /var/run/mount/data/mount/data/incremental/MT_data_app_vmdl703/mount rw,nosuid,nodev,noatime shared:46 - incremental-fs /data/incremental/MT_data_app_vmdl703/backing_store rw,seclabel,read_timeout_ms=10000,readahead=0
+)";
+
+    auto m = makeFrom(mountsFile);
+
+    EXPECT_EQ(size_t(1), m.size());
+    EXPECT_STREQ("/data/incremental/MT_data_app_vmdl703/mount",
+                 m.rootFor("/data/incremental/MT_data_app_vmdl703/mount/123/2").data());
+    EXPECT_STREQ("/data/incremental/MT_data_app_vmdl703/mount",
+                 m.rootFor("/var/run/mount/data/mount/data/incremental/MT_data_app_vmdl703/mount/"
+                           "some/thing")
+                         .data());
+}
+
+TEST_F(MountRegistryTest, MultiRootLoadReversed) {
+    constexpr char mountsFile[] =
+            R"(4605 34 0:154 / /mnt/installer/0/0000000000000000000000000000CAFEF00D2019 rw,nosuid,nodev,noexec,noatime shared:45 master:43 - fuse /dev/fuse rw,lazytime,user_id=0,group_id=0,allow_other
+4561 35 0:154 / /mnt/androidwritable/0/0000000000000000000000000000CAFEF00D2019 rw,nosuid,nodev,noexec,noatime shared:44 master:43 - fuse /dev/fuse rw,lazytime,user_id=0,group_id=0,allow_other
+4560 99 0:154 / /storage/0000000000000000000000000000CAFEF00D2019 rw,nosuid,nodev,noexec,noatime master:43 - fuse /dev/fuse rw,lazytime,user_id=0,group_id=0,allow_other
+4650 30 0:44 /MyFiles /mnt/pass_through/0/0000000000000000000000000000CAFEF00D2019 rw,nosuid,nodev,noexec,relatime shared:31 - 9p media rw,sync,dirsync,access=client,trans=virtio
+3182 77 0:146 / /var/run/mount/data/mount/data/incremental/MT_data_app_vmdl703/mount rw,nosuid,nodev,noatime shared:46 - incremental-fs /data/incremental/MT_data_app_vmdl703/backing_store rw,seclabel,read_timeout_ms=10000,readahead=0
+3181 79 0:146 / /data/incremental/MT_data_app_vmdl703/mount rw,nosuid,nodev,noatime shared:46 - incremental-fs /data/incremental/MT_data_app_vmdl703/backing_store rw,seclabel,read_timeout_ms=10000,readahead=0
+)";
+
+    auto m = makeFrom(mountsFile);
+
+    EXPECT_EQ(size_t(1), m.size());
+    EXPECT_STREQ("/data/incremental/MT_data_app_vmdl703/mount",
+                 m.rootFor("/data/incremental/MT_data_app_vmdl703/mount/123/2").data());
+    EXPECT_STREQ("/data/incremental/MT_data_app_vmdl703/mount",
+                 m.rootFor("/var/run/mount/data/mount/data/incremental/MT_data_app_vmdl703/mount/"
+                           "some/thing")
+                         .data());
+}
+
+TEST_F(MountRegistryTest, LoadInvalid) {
+    constexpr char mountsFile[] =
+            R"(9465 93 0:281 // /data/incremental1 shared:56 - incremental-fs /data/incremental2 rw,seclabel,read_timeout_ms=10000,readahead=0,report_uid
+9529 93 0:281 /st_232_0 /data/app/vmdl1998506227.tmp rw,nosuid,nodev,noatime shared:56 - incremental-fs /data/incremental/MT_data_app_vmdl199/backing_store rw,seclabel,read_timeout_ms=10000,readahead=0,report_uid
+9657 93 0:282 /st_233_0 /data/app/vmdl2034419270.tmp rw,nosuid,nodev,noatime shared:57 - incremental-fs /data/incremental/MT_data_app_vmdl203/backing_store rw,seclabel,read_timeout_ms=10000,readahead=0,report_uid
+9721 93 0:283 / /data/incremental/MT_data_app_vmdl154/mount rw,nosuid,nodev,noatime shared:58 - incremental-fs /data/incremental/MT_data_app_vmdl154/backing_store rw,seclabel,read_timeout_ms=10000,readahead=0,report_uid
+9785 93 0:283 /st_234_0 /data/app/vmdl1545425783.tmp rw,nosuid,nodev,noatime shared:58 - incremental-fs /data/incremental/MT_data_app_vmdl154/backing_store rw,seclabel,read_timeout_ms=10000,readahead=0,report_uid
+9849 93 0:284 / /data/incremental/MT_data_app_vmdl209/mount rw,nosuid,nodev,noatime shared:59 - incremental-fs /data/incremental/MT_data_app_vmdl209/backing_store rw,seclabel,read_timeout_ms=10000,readahead=0,report_uid
+9913 93 0:284 /st_235_0 /data/app/vmdl2099748756.tmp rw,nosuid,nodev,noatime shared:59 - incremental-fs /data/incremental/MT_data_app_vmdl209/backing_store rw,seclabel,read_timeout_ms=10000,readahead=0,report_uid
+4007 93 0:269 /st_240_1 /data/app/~~I499PLubwcOVbJaEFqpHHQ== rw,nosuid,nodev,noatime shared:44 - incremental-fs /data/incremental/MT_data_app_vmdl158/backing_store rw,seclabel,read_timeout_ms=10000,readahead=0,report_uid
+5285 93 0:270 /st_241_1 /data/app/~~CvgMdGm9eNJpvdq-62Jktg== rw,nosuid,nodev,noatime shared:45 - incremental-fs /data/incremental/MT_data_app_vmdl943/backing_store rw,seclabel,read_timeout_ms=10000,readahead=0,report_uid
+8786 93 0:271 /st_242_1 /data/app/~~_oNudLuBvqtSE78VoMVY5Q== rw,nosuid,nodev,noatime shared:46 - incremental-fs /data/incremental/MT_data_app_vmdl144/backing_store rw,seclabel,read_timeout_ms=10000,readahead=0,report_uid
+10358 93 0:272 /st_243_1 /data/app/~~o5-RadxV4DUe-mgPyv9SkQ== rw,nosuid,nodev,noatime shared:47 - incremental-fs /data/incremental/MT_data_app_vmdl642/backing_store rw,seclabel,read_timeout_ms=10000,readahead=0,report_uid
+10422 93 0:131 /st_244_1 /data/app/~~rTnKswx1F427UguGO9nDRA== rw,nosuid,nodev,noatime shared:48 - incremental-fs /data/incremental/MT_data_app_vmdl336/backing_store rw,seclabel,read_timeout_ms=10000,readahead=0,report_uid
+10486 93 0:132 /st_245_1 /data/app/~~ZIoVqPDBjLBeajD4thHsYA== rw,nosuid,nodev,noatime shared:49 - incremental-fs /data/incremental/MT_data_app_vmdl993/backing_store rw,seclabel,read_timeout_ms=10000,readahead=0,report_uid
+10550 93 0:133 /st_246_1 /data/app/~~2qjtCtx5rqPW2Hwlrciu2w== rw,nosuid,nodev,noatime shared:50 - incremental-fs /data/incremental/MT_data_app_vmdl827/backing_store rw,seclabel,read_timeout_ms=10000,readahead=0,report_uid
+)";
+
+    auto m = makeFrom(mountsFile);
+    // only two of the mounts in this file are valid
+    EXPECT_EQ(size_t(2), m.size());
 }
diff --git a/incfs/tests/hardening_benchmark.cpp b/incfs/tests/hardening_benchmark.cpp
new file mode 100644
index 0000000..8e4f047
--- /dev/null
+++ b/incfs/tests/hardening_benchmark.cpp
@@ -0,0 +1,165 @@
+/*
+ * Copyright (C) 2020 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.
+ */
+
+#include <android-base/file.h>
+#include <android-base/mapped_file.h>
+#include <benchmark/benchmark.h>
+#include <unistd.h>
+
+#include "incfs_support/access.h"
+#include "incfs_support/signal_handling.h"
+#include "util/map_ptr.h"
+
+static std::unique_ptr<TemporaryFile> makeFile() {
+    auto tmp = std::unique_ptr<TemporaryFile>(new TemporaryFile());
+    char c = 1;
+    write(tmp->fd, &c, sizeof(c));
+    return tmp;
+}
+
+static std::pair<std::unique_ptr<TemporaryFile>, std::unique_ptr<android::base::MappedFile>>
+makeEmptyFileMapping() {
+    auto tmp = makeFile();
+    // mmap() only works for non-empty files, but it's "ok" to resize it back to empty afterwards
+    auto mapping = android::base::MappedFile::FromFd(tmp->fd, 0, 1, PROT_READ);
+    ftruncate(tmp->fd, 0);
+    return {std::move(tmp), std::move(mapping)};
+}
+
+static void TestEmpty(benchmark::State& state) {
+    int val = 0;
+    for (auto _ : state) {
+        benchmark::DoNotOptimize(val += 1);
+    }
+}
+BENCHMARK(TestEmpty);
+
+static void TestSignal(benchmark::State& state) {
+    auto tmp = makeFile();
+    auto mapping = android::base::MappedFile::FromFd(tmp->fd, 0, 1, PROT_READ);
+
+    int val;
+    for (auto _ : state) {
+        SCOPED_SIGBUS_HANDLER({ break; });
+        val += *mapping->data();
+    }
+}
+BENCHMARK(TestSignal);
+
+static void TestRead(benchmark::State& state) {
+    auto tmp = makeFile();
+    int val = 0;
+    for (auto _ : state) {
+        char c;
+        pread(tmp->fd, &c, sizeof(c), 0);
+        val += c;
+    }
+}
+BENCHMARK(TestRead);
+
+static void TestMapPtrRaw(benchmark::State& state) {
+    auto tmp = makeFile();
+    android::incfs::IncFsFileMap map;
+    map.CreateForceVerification(tmp->fd, 0, 1, tmp->path, true);
+    int val = 0;
+    const uint8_t* prev_block = nullptr;
+    for (auto _ : state) {
+        auto start = static_cast<const uint8_t*>(map.unsafe_data());
+        auto end = start + map.length();
+        val += map.Verify(start, end, &prev_block);
+    }
+}
+BENCHMARK(TestMapPtrRaw);
+
+static void TestMapPtr(benchmark::State& state) {
+    auto tmp = makeFile();
+    android::incfs::IncFsFileMap map;
+    map.CreateForceVerification(tmp->fd, 0, 1, tmp->path, true);
+    int val = 0;
+    for (auto _ : state) {
+        val += map.data<char>().verify();
+    }
+}
+BENCHMARK(TestMapPtr);
+
+static void TestAccess(benchmark::State& state) {
+    auto tmp = makeFile();
+    auto mapping = android::base::MappedFile::FromFd(tmp->fd, 0, 1, PROT_READ);
+    int val = 0;
+    for (auto _ : state) {
+        incfs::access(mapping->data(), [&](auto ptr) { val += *ptr; });
+    }
+}
+BENCHMARK(TestAccess);
+
+static void TestAccessFast(benchmark::State& state) {
+    auto tmp = makeFile();
+    auto mapping = android::base::MappedFile::FromFd(tmp->fd, 0, 1, PROT_READ);
+    int val = 0;
+    incfs::access(mapping->data(), [&](auto ptr) {
+        for (auto _ : state) {
+            val += *ptr;
+        }
+    });
+}
+BENCHMARK(TestAccessFast);
+
+static void TestAccessVal(benchmark::State& state) {
+    auto tmp = makeFile();
+    auto mapping = android::base::MappedFile::FromFd(tmp->fd, 0, 1, PROT_READ);
+    int val = 0;
+    for (auto _ : state) {
+        incfs::access(mapping->data(), [&](auto ptr) { return val += *ptr; });
+    }
+}
+BENCHMARK(TestAccessVal);
+
+static void TestAccessNested(benchmark::State& state) {
+    auto tmp = makeFile();
+    auto mapping = android::base::MappedFile::FromFd(tmp->fd, 0, 1, PROT_READ);
+    int val = 0;
+    incfs::access(nullptr, [&](auto) {
+        for (auto _ : state) {
+            incfs::access(mapping->data(), [&](auto ptr) { val += *ptr; });
+        }
+    });
+}
+BENCHMARK(TestAccessNested);
+
+static void TestAccessDoubleNested(benchmark::State& state) {
+    auto tmp = makeFile();
+    auto mapping = android::base::MappedFile::FromFd(tmp->fd, 0, 1, PROT_READ);
+    int val = 0;
+    incfs::access(nullptr, [&](auto) {
+        incfs::access(nullptr, [&](auto) {
+            for (auto _ : state) {
+                incfs::access(mapping->data(), [&](auto ptr) { val += *ptr; });
+            }
+        });
+    });
+}
+BENCHMARK(TestAccessDoubleNested);
+
+static void TestAccessError(benchmark::State& state) {
+    auto [tmp, mapping] = makeEmptyFileMapping();
+    int val = 0;
+    for (auto _ : state) {
+        incfs::access(mapping->data(), [&](auto ptr) { val += *ptr; });
+    }
+}
+BENCHMARK(TestAccessError);
+
+BENCHMARK_MAIN();
diff --git a/incfs/tests/incfs_test.cpp b/incfs/tests/incfs_test.cpp
index bc02d37..e651f2e 100644
--- a/incfs/tests/incfs_test.cpp
+++ b/incfs/tests/incfs_test.cpp
@@ -15,6 +15,7 @@
  */
 
 #include <android-base/file.h>
+#include <android-base/stringprintf.h>
 #include <sys/select.h>
 
 #include <unistd.h>
@@ -26,6 +27,7 @@
 
 using namespace android::incfs;
 using namespace std::literals;
+namespace ab = android::base;
 
 struct ScopedUnmount {
     std::string path_;
@@ -43,32 +45,6 @@
         return {.data = sv.data(), .size = IncFsSize(sv.size())};
     }
 
-    int makeFileWithHash(int id) {
-        // calculate the required size for two leaf hash blocks
-        constexpr auto size =
-                (INCFS_DATA_FILE_BLOCK_SIZE / INCFS_MAX_HASH_SIZE + 1) * INCFS_DATA_FILE_BLOCK_SIZE;
-
-        // assemble a signature/hashing data for it
-        struct __attribute__((packed)) Signature {
-            uint32_t version = INCFS_SIGNATURE_VERSION;
-            uint32_t hashingSize = sizeof(hashing);
-            struct __attribute__((packed)) Hashing {
-                uint32_t algo = INCFS_HASH_TREE_SHA256;
-                uint8_t log2Blocksize = 12;
-                uint32_t saltSize = 0;
-                uint32_t rootHashSize = INCFS_MAX_HASH_SIZE;
-                char rootHash[INCFS_MAX_HASH_SIZE] = {};
-            } hashing;
-            uint32_t signingSize = 0;
-        } signature;
-
-        int res = makeFile(control_, mountPath(test_file_name_), 0555, fileId(id),
-                           {.size = size,
-                            .signature = {.data = (char*)&signature, .size = sizeof(signature)}});
-        EXPECT_EQ(0, res);
-        return res ? -1 : size;
-    }
-
     static int sizeToPages(int size) {
         return (size + INCFS_DATA_FILE_BLOCK_SIZE - 1) / INCFS_DATA_FILE_BLOCK_SIZE;
     }
@@ -129,6 +105,94 @@
         ASSERT_EQ((int)std::size(blocks), writeBlocks({blocks, std::size(blocks)}));
     }
 
+    template <class ReadStruct>
+    void testWriteBlockAndPageRead() {
+        const auto id = fileId(1);
+        ASSERT_TRUE(control_.logs() >= 0);
+        ASSERT_EQ(0,
+                  makeFile(control_, mountPath(test_file_name_), 0555, id,
+                           {.size = test_file_size_}));
+        auto fd = openForSpecialOps(control_, fileId(1));
+        ASSERT_GE(fd.get(), 0);
+
+        std::vector<char> data(INCFS_DATA_FILE_BLOCK_SIZE);
+        auto block = DataBlock{
+                .fileFd = fd.get(),
+                .pageIndex = 0,
+                .compression = INCFS_COMPRESSION_KIND_NONE,
+                .dataSize = (uint32_t)data.size(),
+                .data = data.data(),
+        };
+        ASSERT_EQ(1, writeBlocks({&block, 1}));
+
+        std::thread wait_page_read_thread([&]() {
+            std::vector<ReadStruct> reads;
+            auto res = waitForPageReads(control_, std::chrono::seconds(5), &reads);
+            ASSERT_EQ(WaitResult::HaveData, res) << (int)res;
+            ASSERT_FALSE(reads.empty());
+            EXPECT_EQ(0, memcmp(&id, &reads[0].id, sizeof(id)));
+            EXPECT_EQ(0, int(reads[0].block));
+            if constexpr (std::is_same_v<ReadStruct, ReadInfoWithUid>) {
+                if (features() & Features::v2) {
+                    EXPECT_NE(kIncFsNoUid, int(reads[0].uid));
+                } else {
+                    EXPECT_EQ(kIncFsNoUid, int(reads[0].uid));
+                }
+            }
+        });
+
+        const auto file_path = mountPath(test_file_name_);
+        const android::base::unique_fd readFd(
+                open(file_path.c_str(), O_RDONLY | O_CLOEXEC | O_BINARY));
+        ASSERT_TRUE(readFd >= 0);
+        char buf[INCFS_DATA_FILE_BLOCK_SIZE];
+        ASSERT_TRUE(android::base::ReadFully(readFd, buf, sizeof(buf)));
+        wait_page_read_thread.join();
+    }
+    template <class PendingRead>
+    void testWaitForPendingReads() {
+        const auto id = fileId(1);
+        ASSERT_EQ(0,
+                  makeFile(control_, mountPath(test_file_name_), 0555, id,
+                           {.size = test_file_size_}));
+
+        std::thread wait_pending_read_thread([&]() {
+            std::vector<PendingRead> pending_reads;
+            ASSERT_EQ(WaitResult::HaveData,
+                      waitForPendingReads(control_, std::chrono::seconds(10), &pending_reads));
+            ASSERT_GT(pending_reads.size(), 0u);
+            EXPECT_EQ(0, memcmp(&id, &pending_reads[0].id, sizeof(id)));
+            EXPECT_EQ(0, (int)pending_reads[0].block);
+            if constexpr (std::is_same_v<PendingRead, ReadInfoWithUid>) {
+                if (features() & Features::v2) {
+                    EXPECT_NE(kIncFsNoUid, int(pending_reads[0].uid));
+                } else {
+                    EXPECT_EQ(kIncFsNoUid, int(pending_reads[0].uid));
+                }
+            }
+
+            auto fd = openForSpecialOps(control_, fileId(1));
+            ASSERT_GE(fd.get(), 0);
+
+            std::vector<char> data(INCFS_DATA_FILE_BLOCK_SIZE);
+            auto block = DataBlock{
+                    .fileFd = fd.get(),
+                    .pageIndex = 0,
+                    .compression = INCFS_COMPRESSION_KIND_NONE,
+                    .dataSize = (uint32_t)data.size(),
+                    .data = data.data(),
+            };
+            ASSERT_EQ(1, writeBlocks({&block, 1}));
+        });
+
+        const auto file_path = mountPath(test_file_name_);
+        const android::base::unique_fd fd(open(file_path.c_str(), O_RDONLY | O_CLOEXEC | O_BINARY));
+        ASSERT_GE(fd.get(), 0);
+        char buf[INCFS_DATA_FILE_BLOCK_SIZE];
+        ASSERT_TRUE(android::base::ReadFully(fd, buf, sizeof(buf)));
+        wait_pending_read_thread.join();
+    }
+
     inline static const int test_file_size_ = INCFS_DATA_FILE_BLOCK_SIZE;
 };
 
@@ -175,9 +239,10 @@
     EXPECT_GE(IncFs_GetControlFd(control_, CMD), 0);
     EXPECT_GE(IncFs_GetControlFd(control_, PENDING_READS), 0);
     EXPECT_GE(IncFs_GetControlFd(control_, LOGS), 0);
+    EXPECT_EQ((features() & Features::v2) != 0, IncFs_GetControlFd(control_, BLOCKS_WRITTEN) >= 0);
 
     auto fds = control_.releaseFds();
-    EXPECT_GE(fds.size(), size_t(3));
+    EXPECT_GE(fds.size(), size_t(4));
     EXPECT_GE(fds[0].get(), 0);
     EXPECT_GE(fds[1].get(), 0);
     EXPECT_GE(fds[2].get(), 0);
@@ -185,26 +250,28 @@
     EXPECT_LT(IncFs_GetControlFd(control_, CMD), 0);
     EXPECT_LT(IncFs_GetControlFd(control_, PENDING_READS), 0);
     EXPECT_LT(IncFs_GetControlFd(control_, LOGS), 0);
+    EXPECT_LT(IncFs_GetControlFd(control_, BLOCKS_WRITTEN), 0);
 
     control_.close();
     EXPECT_FALSE(control_);
 
-    auto control = IncFs_CreateControl(fds[0].release(), fds[1].release(), fds[2].release());
+    auto control = IncFs_CreateControl(fds[0].release(), fds[1].release(), fds[2].release(), -1);
     ASSERT_TRUE(control);
     EXPECT_GE(IncFs_GetControlFd(control, CMD), 0);
     EXPECT_GE(IncFs_GetControlFd(control, PENDING_READS), 0);
     EXPECT_GE(IncFs_GetControlFd(control, LOGS), 0);
-    IncFsFd rawFds[3];
+    IncFsFd rawFds[4];
     EXPECT_EQ(-EINVAL, IncFs_ReleaseControlFds(nullptr, rawFds, 3));
     EXPECT_EQ(-EINVAL, IncFs_ReleaseControlFds(control, nullptr, 3));
     EXPECT_EQ(-ERANGE, IncFs_ReleaseControlFds(control, rawFds, 2));
-    EXPECT_EQ(3, IncFs_ReleaseControlFds(control, rawFds, 3));
+    EXPECT_EQ(4, IncFs_ReleaseControlFds(control, rawFds, 4));
     EXPECT_GE(rawFds[0], 0);
     EXPECT_GE(rawFds[1], 0);
     EXPECT_GE(rawFds[2], 0);
     ::close(rawFds[0]);
     ::close(rawFds[1]);
     ::close(rawFds[2]);
+    if (rawFds[3] >= 0) ::close(rawFds[3]);
     IncFs_DeleteControl(control);
 }
 
@@ -256,7 +323,7 @@
 
 TEST_F(IncFsTest, RootInvalidControl) {
     const TemporaryFile tmp_file;
-    auto control{createControl(tmp_file.fd, -1, -1)};
+    auto control{createControl(tmp_file.fd, -1, -1, -1)};
     ASSERT_EQ("", root(control)) << "Error: " << errno;
 }
 
@@ -342,73 +409,19 @@
 }
 
 TEST_F(IncFsTest, WriteBlocksAndPageRead) {
-    const auto id = fileId(1);
-    ASSERT_TRUE(control_.logs() >= 0);
-    ASSERT_EQ(0,
-              makeFile(control_, mountPath(test_file_name_), 0555, id, {.size = test_file_size_}));
-    auto fd = openForSpecialOps(control_, fileId(1));
-    ASSERT_GE(fd.get(), 0);
+    ASSERT_NO_FATAL_FAILURE(testWriteBlockAndPageRead<ReadInfo>());
+}
 
-    std::vector<char> data(INCFS_DATA_FILE_BLOCK_SIZE);
-    auto block = DataBlock{
-            .fileFd = fd.get(),
-            .pageIndex = 0,
-            .compression = INCFS_COMPRESSION_KIND_NONE,
-            .dataSize = (uint32_t)data.size(),
-            .data = data.data(),
-    };
-    ASSERT_EQ(1, writeBlocks({&block, 1}));
-
-    std::thread wait_page_read_thread([&]() {
-        std::vector<ReadInfo> reads;
-        ASSERT_EQ(WaitResult::HaveData,
-                  waitForPageReads(control_, std::chrono::seconds(5), &reads));
-        ASSERT_FALSE(reads.empty());
-        ASSERT_EQ(0, memcmp(&id, &reads[0].id, sizeof(id)));
-        ASSERT_EQ(0, int(reads[0].block));
-    });
-
-    const auto file_path = mountPath(test_file_name_);
-    const android::base::unique_fd readFd(open(file_path.c_str(), O_RDONLY | O_CLOEXEC | O_BINARY));
-    ASSERT_TRUE(readFd >= 0);
-    char buf[INCFS_DATA_FILE_BLOCK_SIZE];
-    ASSERT_TRUE(android::base::ReadFully(readFd, buf, sizeof(buf)));
-    wait_page_read_thread.join();
+TEST_F(IncFsTest, WriteBlocksAndPageReadWithUid) {
+    ASSERT_NO_FATAL_FAILURE(testWriteBlockAndPageRead<ReadInfoWithUid>());
 }
 
 TEST_F(IncFsTest, WaitForPendingReads) {
-    const auto id = fileId(1);
-    ASSERT_EQ(0,
-              makeFile(control_, mountPath(test_file_name_), 0555, id, {.size = test_file_size_}));
+    ASSERT_NO_FATAL_FAILURE(testWaitForPendingReads<ReadInfo>());
+}
 
-    std::thread wait_pending_read_thread([&]() {
-        std::vector<ReadInfo> pending_reads;
-        ASSERT_EQ(WaitResult::HaveData,
-                  waitForPendingReads(control_, std::chrono::seconds(10), &pending_reads));
-        ASSERT_GT(pending_reads.size(), 0u);
-        ASSERT_EQ(0, memcmp(&id, &pending_reads[0].id, sizeof(id)));
-        ASSERT_EQ(0, (int)pending_reads[0].block);
-
-        auto fd = openForSpecialOps(control_, fileId(1));
-        ASSERT_GE(fd.get(), 0);
-
-        std::vector<char> data(INCFS_DATA_FILE_BLOCK_SIZE);
-        auto block = DataBlock{
-                .fileFd = fd.get(),
-                .pageIndex = 0,
-                .compression = INCFS_COMPRESSION_KIND_NONE,
-                .dataSize = (uint32_t)data.size(),
-                .data = data.data(),
-        };
-        ASSERT_EQ(1, writeBlocks({&block, 1}));
-    });
-
-    const auto file_path = mountPath(test_file_name_);
-    const android::base::unique_fd fd(open(file_path.c_str(), O_RDONLY | O_CLOEXEC | O_BINARY));
-    ASSERT_GE(fd.get(), 0);
-    char buf[INCFS_DATA_FILE_BLOCK_SIZE];
-    ASSERT_TRUE(android::base::ReadFully(fd, buf, sizeof(buf)));
-    wait_pending_read_thread.join();
+TEST_F(IncFsTest, WaitForPendingReadsWithUid) {
+    ASSERT_NO_FATAL_FAILURE(testWaitForPendingReads<ReadInfoWithUid>());
 }
 
 TEST_F(IncFsTest, GetFilledRangesBad) {
@@ -458,6 +471,7 @@
     EXPECT_EQ(0, filledRanges.hashRangesCount);
 
     EXPECT_EQ(-ENODATA, IncFs_IsFullyLoaded(fd.get()));
+    EXPECT_EQ(-ENODATA, IncFs_IsEverythingFullyLoaded(control_));
 
     // write one block
     std::vector<char> data(INCFS_DATA_FILE_BLOCK_SIZE);
@@ -491,6 +505,7 @@
     EXPECT_EQ(0, filledRanges.hashRangesCount);
 
     EXPECT_EQ(-ENODATA, IncFs_IsFullyLoaded(fd.get()));
+    EXPECT_EQ(-ENODATA, IncFs_IsEverythingFullyLoaded(control_));
 
     // append one more block next to the first one
     block.pageIndex = 1;
@@ -519,6 +534,7 @@
     EXPECT_EQ(0, filledRanges.hashRangesCount);
 
     EXPECT_EQ(-ENODATA, IncFs_IsFullyLoaded(fd.get()));
+    EXPECT_EQ(-ENODATA, IncFs_IsEverythingFullyLoaded(control_));
 
     // now create a gap between filled blocks
     block.pageIndex = 3;
@@ -559,6 +575,7 @@
     EXPECT_EQ(0, filledRanges.hashRangesCount);
 
     EXPECT_EQ(-ENODATA, IncFs_IsFullyLoaded(fd.get()));
+    EXPECT_EQ(-ENODATA, IncFs_IsEverythingFullyLoaded(control_));
 
     // at last fill the whole file and make sure we report it as having a single range
     block.pageIndex = 2;
@@ -586,6 +603,7 @@
     EXPECT_EQ(0, filledRanges.hashRangesCount);
 
     EXPECT_EQ(0, IncFs_IsFullyLoaded(fd.get()));
+    EXPECT_EQ(0, IncFs_IsEverythingFullyLoaded(control_));
 }
 
 TEST_F(IncFsTest, GetFilledRangesSmallBuffer) {
@@ -717,6 +735,7 @@
     EXPECT_EQ(size_t(1), ranges3.hashRanges()[1].size());
 
     EXPECT_EQ(LoadingState::MissingBlocks, isFullyLoaded(fd.get()));
+    EXPECT_EQ(LoadingState::MissingBlocks, isEverythingFullyLoaded(control_));
 
     {
         std::vector<char> data(INCFS_DATA_FILE_BLOCK_SIZE);
@@ -736,4 +755,632 @@
         }
     }
     EXPECT_EQ(LoadingState::Full, isFullyLoaded(fd.get()));
+    EXPECT_EQ(LoadingState::Full, isEverythingFullyLoaded(control_));
+}
+
+TEST_F(IncFsTest, BlocksWritten) {
+    if (!(features() & Features::v2)) {
+        GTEST_SKIP() << "test not supported: IncFS is too old";
+        return;
+    }
+    const auto id = fileId(1);
+    ASSERT_EQ(0,
+              makeFile(control_, mountPath(test_file_name_), 0555, id, {.size = test_file_size_}));
+
+    IncFsSize blocksWritten = 0;
+    ASSERT_EQ(0, IncFs_WaitForFsWrittenBlocksChange(control_, 0, &blocksWritten));
+    EXPECT_EQ(0, blocksWritten);
+
+    auto fd = openForSpecialOps(control_, fileId(1));
+    ASSERT_GE(fd.get(), 0);
+
+    std::vector<char> data(INCFS_DATA_FILE_BLOCK_SIZE);
+    auto block = DataBlock{
+            .fileFd = fd.get(),
+            .pageIndex = 0,
+            .compression = INCFS_COMPRESSION_KIND_NONE,
+            .dataSize = (uint32_t)data.size(),
+            .data = data.data(),
+    };
+    ASSERT_EQ(1, writeBlocks({&block, 1}));
+
+    ASSERT_EQ(0, IncFs_WaitForFsWrittenBlocksChange(control_, 0, &blocksWritten));
+    EXPECT_EQ(1, blocksWritten);
+}
+
+TEST_F(IncFsTest, Timeouts) {
+    if (!(features() & Features::v2)) {
+        GTEST_SKIP() << "test not supported: IncFS is too old";
+        return;
+    }
+
+    IncFsUidReadTimeouts timeouts[2] = {{1, 1000, 2000, 3000}, {2, 1000, 3000, 4000}};
+
+    EXPECT_EQ(0, IncFs_SetUidReadTimeouts(control_, timeouts, std::size(timeouts)));
+
+    IncFsUidReadTimeouts outTimeouts[3];
+
+    size_t outSize = 1;
+    EXPECT_EQ(-E2BIG, IncFs_GetUidReadTimeouts(control_, outTimeouts, &outSize));
+    EXPECT_EQ(size_t(2), outSize);
+
+    outSize = 3;
+    EXPECT_EQ(0, IncFs_GetUidReadTimeouts(control_, outTimeouts, &outSize));
+    EXPECT_EQ(size_t(2), outSize);
+
+    EXPECT_EQ(0, memcmp(timeouts, outTimeouts, 2 * sizeof(timeouts[0])));
+}
+
+TEST_F(IncFsTest, CompletionNoFiles) {
+    if (!(features() & Features::v2)) {
+        GTEST_SKIP() << "test not supported: IncFS is too old";
+        return;
+    }
+
+    size_t count = 0;
+    EXPECT_EQ(0, IncFs_ListIncompleteFiles(control_, nullptr, &count));
+    EXPECT_EQ(size_t(0), count);
+    EXPECT_EQ(0, IncFs_WaitForLoadingComplete(control_, 0));
+}
+
+TEST_F(IncFsTest, CompletionOneFile) {
+    if (!(features() & Features::v2)) {
+        GTEST_SKIP() << "test not supported: IncFS is too old";
+        return;
+    }
+
+    const auto id = fileId(1);
+    ASSERT_EQ(0,
+              makeFile(control_, mountPath(test_file_name_), 0555, id, {.size = test_file_size_}));
+
+    size_t count = 0;
+    EXPECT_EQ(-E2BIG, IncFs_ListIncompleteFiles(control_, nullptr, &count));
+    EXPECT_EQ(size_t(1), count);
+    EXPECT_EQ(-ETIMEDOUT, IncFs_WaitForLoadingComplete(control_, 0));
+
+    IncFsFileId ids[2];
+    count = 2;
+    EXPECT_EQ(0, IncFs_ListIncompleteFiles(control_, ids, &count));
+    EXPECT_EQ(size_t(1), count);
+    EXPECT_EQ(id, ids[0]);
+
+    auto fd = openForSpecialOps(control_, id);
+    ASSERT_GE(fd.get(), 0);
+    std::vector<char> data(INCFS_DATA_FILE_BLOCK_SIZE);
+    auto block = DataBlock{
+            .fileFd = fd.get(),
+            .pageIndex = 0,
+            .compression = INCFS_COMPRESSION_KIND_NONE,
+            .dataSize = (uint32_t)data.size(),
+            .data = data.data(),
+    };
+    ASSERT_EQ(1, writeBlocks({&block, 1}));
+
+    count = 2;
+    EXPECT_EQ(0, IncFs_ListIncompleteFiles(control_, ids, &count));
+    EXPECT_EQ(size_t(0), count);
+    EXPECT_EQ(0, IncFs_WaitForLoadingComplete(control_, 0));
+}
+
+TEST_F(IncFsTest, CompletionMultiple) {
+    if (!(features() & Features::v2)) {
+        GTEST_SKIP() << "test not supported: IncFS is too old";
+        return;
+    }
+
+    const auto id = fileId(1);
+    ASSERT_EQ(0,
+              makeFile(control_, mountPath(test_file_name_), 0555, id, {.size = test_file_size_}));
+
+    size_t count = 0;
+    EXPECT_EQ(-E2BIG, IncFs_ListIncompleteFiles(control_, nullptr, &count));
+    EXPECT_EQ(size_t(1), count);
+    EXPECT_EQ(-ETIMEDOUT, IncFs_WaitForLoadingComplete(control_, 0));
+
+    // fill the existing file but add another one
+    const auto id2 = fileId(2);
+    ASSERT_EQ(0, makeFile(control_, mountPath("test2"), 0555, id2, {.size = test_file_size_}));
+
+    IncFsFileId ids[2];
+    count = 2;
+    EXPECT_EQ(0, IncFs_ListIncompleteFiles(control_, ids, &count));
+    EXPECT_EQ(size_t(2), count);
+    EXPECT_EQ(id, ids[0]);
+    EXPECT_EQ(id2, ids[1]);
+
+    auto fd = openForSpecialOps(control_, id);
+    ASSERT_GE(fd.get(), 0);
+    std::vector<char> data(INCFS_DATA_FILE_BLOCK_SIZE);
+    auto block = DataBlock{
+            .fileFd = fd.get(),
+            .pageIndex = 0,
+            .compression = INCFS_COMPRESSION_KIND_NONE,
+            .dataSize = (uint32_t)data.size(),
+            .data = data.data(),
+    };
+    ASSERT_EQ(1, writeBlocks({&block, 1}));
+
+    count = 2;
+    EXPECT_EQ(0, IncFs_ListIncompleteFiles(control_, ids, &count));
+    EXPECT_EQ(size_t(1), count);
+    EXPECT_EQ(id2, ids[0]);
+    EXPECT_EQ(-ETIMEDOUT, IncFs_WaitForLoadingComplete(control_, 0));
+}
+
+TEST_F(IncFsTest, CompletionWait) {
+    if (!(features() & Features::v2)) {
+        GTEST_SKIP() << "test not supported: IncFS is too old";
+        return;
+    }
+
+    ASSERT_EQ(0,
+              makeFile(control_, mountPath("test1"), 0555, fileId(1),
+                       {.size = INCFS_DATA_FILE_BLOCK_SIZE}));
+    ASSERT_EQ(0,
+              makeFile(control_, mountPath("test2"), 0555, fileId(2),
+                       {.size = INCFS_DATA_FILE_BLOCK_SIZE}));
+    ASSERT_EQ(0,
+              makeFile(control_, mountPath("test3"), 0555, fileId(3),
+                       {.size = INCFS_DATA_FILE_BLOCK_SIZE}));
+
+    std::atomic<int> res = -1;
+    auto waiter = std::thread([&] { res = IncFs_WaitForLoadingComplete(control_, 5 * 1000); });
+
+    std::vector<char> data(INCFS_DATA_FILE_BLOCK_SIZE);
+
+    {
+        auto fd = openForSpecialOps(control_, fileId(1));
+        ASSERT_GE(fd.get(), 0);
+        auto block = DataBlock{
+                .fileFd = fd.get(),
+                .pageIndex = 0,
+                .compression = INCFS_COMPRESSION_KIND_NONE,
+                .dataSize = (uint32_t)data.size(),
+                .data = data.data(),
+        };
+        ASSERT_EQ(1, writeBlocks({&block, 1}));
+    }
+    ASSERT_TRUE(res == -1);
+
+    {
+        auto fd = openForSpecialOps(control_, fileId(3));
+        ASSERT_GE(fd.get(), 0);
+        auto block = DataBlock{
+                .fileFd = fd.get(),
+                .pageIndex = 0,
+                .compression = INCFS_COMPRESSION_KIND_NONE,
+                .dataSize = (uint32_t)data.size(),
+                .data = data.data(),
+        };
+        ASSERT_EQ(1, writeBlocks({&block, 1}));
+    }
+    ASSERT_TRUE(res == -1);
+
+    {
+        auto fd = openForSpecialOps(control_, fileId(2));
+        ASSERT_GE(fd.get(), 0);
+        auto block = DataBlock{
+                .fileFd = fd.get(),
+                .pageIndex = 0,
+                .compression = INCFS_COMPRESSION_KIND_NONE,
+                .dataSize = (uint32_t)data.size(),
+                .data = data.data(),
+        };
+        ASSERT_EQ(1, writeBlocks({&block, 1}));
+    }
+
+    waiter.join();
+
+    auto listIncomplete = [&] {
+        IncFsFileId ids[3];
+        size_t count = 3;
+        if (IncFs_ListIncompleteFiles(control_, ids, &count) != 0) {
+            return "error listing incomplete files"s;
+        }
+        auto res = ab::StringPrintf("[%d]", int(count));
+        for (size_t i = 0; i < count; ++i) {
+            ab::StringAppendF(&res, " %s", toString(ids[i]).c_str());
+        }
+        return res;
+    };
+    EXPECT_EQ(0, res) << "Incomplete files: " << listIncomplete();
+}
+
+TEST_F(IncFsTest, GetBlockCounts) {
+    if (!(features() & Features::v2)) {
+        GTEST_SKIP() << "test not supported: IncFS is too old";
+        return;
+    }
+
+    const auto id = fileId(1);
+    ASSERT_EQ(0,
+              makeFile(control_, mountPath(test_file_name_), 0555, id,
+                       {.size = 20 * INCFS_DATA_FILE_BLOCK_SIZE + 3}));
+
+    IncFsBlockCounts counts = {};
+    EXPECT_EQ(0,
+              IncFs_GetFileBlockCountByPath(control_, mountPath(test_file_name_).c_str(), &counts));
+    EXPECT_EQ(21, counts.totalDataBlocks);
+    EXPECT_EQ(0, counts.filledDataBlocks);
+    EXPECT_EQ(0, counts.totalHashBlocks);
+    EXPECT_EQ(0, counts.filledHashBlocks);
+
+    EXPECT_EQ(0, IncFs_GetFileBlockCountById(control_, id, &counts));
+    EXPECT_EQ(21, counts.totalDataBlocks);
+    EXPECT_EQ(0, counts.filledDataBlocks);
+    EXPECT_EQ(0, counts.totalHashBlocks);
+    EXPECT_EQ(0, counts.filledHashBlocks);
+
+    auto fd = openForSpecialOps(control_, id);
+    ASSERT_GE(fd.get(), 0);
+    std::vector<char> data(INCFS_DATA_FILE_BLOCK_SIZE);
+    auto block = DataBlock{
+            .fileFd = fd.get(),
+            .pageIndex = 3,
+            .compression = INCFS_COMPRESSION_KIND_NONE,
+            .dataSize = (uint32_t)data.size(),
+            .data = data.data(),
+    };
+    ASSERT_EQ(1, writeBlocks({&block, 1}));
+
+    EXPECT_EQ(0,
+              IncFs_GetFileBlockCountByPath(control_, mountPath(test_file_name_).c_str(), &counts));
+    EXPECT_EQ(21, counts.totalDataBlocks);
+    EXPECT_EQ(1, counts.filledDataBlocks);
+    EXPECT_EQ(0, counts.totalHashBlocks);
+    EXPECT_EQ(0, counts.filledHashBlocks);
+
+    EXPECT_EQ(0, IncFs_GetFileBlockCountById(control_, id, &counts));
+    EXPECT_EQ(21, counts.totalDataBlocks);
+    EXPECT_EQ(1, counts.filledDataBlocks);
+    EXPECT_EQ(0, counts.totalHashBlocks);
+    EXPECT_EQ(0, counts.filledHashBlocks);
+}
+
+TEST_F(IncFsTest, GetBlockCountsHash) {
+    if (!(features() & Features::v2)) {
+        GTEST_SKIP() << "test not supported: IncFS is too old";
+        return;
+    }
+
+    auto size = makeFileWithHash(1);
+    ASSERT_GT(size, 0);
+
+    IncFsBlockCounts counts = {};
+    EXPECT_EQ(0,
+              IncFs_GetFileBlockCountByPath(control_, mountPath(test_file_name_).c_str(), &counts));
+    EXPECT_EQ(sizeToPages(size), counts.totalDataBlocks);
+    EXPECT_EQ(0, counts.filledDataBlocks);
+    EXPECT_EQ(3, counts.totalHashBlocks);
+    EXPECT_EQ(0, counts.filledHashBlocks);
+
+    ASSERT_NO_FATAL_FAILURE(writeTestRanges(1, size));
+
+    EXPECT_EQ(0,
+              IncFs_GetFileBlockCountByPath(control_, mountPath(test_file_name_).c_str(), &counts));
+    EXPECT_EQ(sizeToPages(size), counts.totalDataBlocks);
+    EXPECT_EQ(4, counts.filledDataBlocks);
+    EXPECT_EQ(3, counts.totalHashBlocks);
+    EXPECT_EQ(2, counts.filledHashBlocks);
+}
+
+TEST_F(IncFsTest, ReserveSpace) {
+    auto size = makeFileWithHash(1);
+    ASSERT_GT(size, 0);
+
+    EXPECT_EQ(-ENOENT,
+              IncFs_ReserveSpaceByPath(control_, mountPath("1"s += test_file_name_).c_str(), size));
+    EXPECT_EQ(0, IncFs_ReserveSpaceByPath(control_, mountPath(test_file_name_).c_str(), size));
+    EXPECT_EQ(0, IncFs_ReserveSpaceByPath(control_, mountPath(test_file_name_).c_str(), 2 * size));
+    EXPECT_EQ(0, IncFs_ReserveSpaceByPath(control_, mountPath(test_file_name_).c_str(), 2 * size));
+    EXPECT_EQ(0,
+              IncFs_ReserveSpaceByPath(control_, mountPath(test_file_name_).c_str(),
+                                       kTrimReservedSpace));
+    EXPECT_EQ(0,
+              IncFs_ReserveSpaceByPath(control_, mountPath(test_file_name_).c_str(),
+                                       kTrimReservedSpace));
+
+    EXPECT_EQ(-ENOENT, IncFs_ReserveSpaceById(control_, fileId(2), size));
+    EXPECT_EQ(0, IncFs_ReserveSpaceById(control_, fileId(1), size));
+    EXPECT_EQ(0, IncFs_ReserveSpaceById(control_, fileId(1), 2 * size));
+    EXPECT_EQ(0, IncFs_ReserveSpaceById(control_, fileId(1), 2 * size));
+    EXPECT_EQ(0, IncFs_ReserveSpaceById(control_, fileId(1), kTrimReservedSpace));
+    EXPECT_EQ(0, IncFs_ReserveSpaceById(control_, fileId(1), kTrimReservedSpace));
+}
+
+TEST_F(IncFsTest, ForEachFile) {
+    const auto incompleteSupported = (features() & Features::v2) != 0;
+    EXPECT_EQ(-EINVAL, IncFs_ForEachFile(nullptr, nullptr, nullptr));
+    EXPECT_EQ(-EINVAL, IncFs_ForEachIncompleteFile(nullptr, nullptr, nullptr));
+    EXPECT_EQ(-EINVAL, IncFs_ForEachFile(control_, nullptr, nullptr));
+    EXPECT_EQ(-EINVAL, IncFs_ForEachIncompleteFile(control_, nullptr, nullptr));
+    EXPECT_EQ(0, IncFs_ForEachFile(control_, nullptr, [](auto, auto, auto) { return true; }));
+    EXPECT_EQ(incompleteSupported ? 0 : -ENOTSUP,
+              IncFs_ForEachIncompleteFile(control_, nullptr,
+                                          [](auto, auto, auto) { return true; }));
+    EXPECT_EQ(0, IncFs_ForEachFile(control_, this, [](auto, auto, auto) { return true; }));
+    EXPECT_EQ(incompleteSupported ? 0 : -ENOTSUP,
+              IncFs_ForEachIncompleteFile(control_, this, [](auto, auto, auto) { return true; }));
+
+    int res = makeFile(control_, mountPath("incomplete.txt"), 0555, fileId(1),
+                       {.metadata = metadata("md")});
+    ASSERT_EQ(res, 0);
+
+    EXPECT_EQ(1, IncFs_ForEachFile(control_, this, [](auto, auto context, auto id) {
+                  auto self = (IncFsTest*)context;
+                  EXPECT_EQ(self->fileId(1), id);
+                  return true;
+              }));
+    EXPECT_EQ(incompleteSupported ? 0 : -ENOTSUP,
+              IncFs_ForEachIncompleteFile(control_, this, [](auto, auto, auto) { return true; }));
+
+    auto size = makeFileWithHash(2);
+    ASSERT_GT(size, 0);
+
+    EXPECT_EQ(1, IncFs_ForEachFile(control_, this, [](auto, auto context, auto id) {
+                  auto self = (IncFsTest*)context;
+                  EXPECT_TRUE(id == self->fileId(1) || id == self->fileId(2));
+                  return false;
+              }));
+    EXPECT_EQ(2, IncFs_ForEachFile(control_, this, [](auto, auto context, auto id) {
+                  auto self = (IncFsTest*)context;
+                  EXPECT_TRUE(id == self->fileId(1) || id == self->fileId(2));
+                  return true;
+              }));
+    EXPECT_EQ(incompleteSupported ? 1 : -ENOTSUP,
+              IncFs_ForEachIncompleteFile(control_, this, [](auto, auto context, auto id) {
+                  auto self = (IncFsTest*)context;
+                  EXPECT_EQ(self->fileId(2), id);
+                  return true;
+              }));
+}
+
+TEST(CStrWrapperTest, EmptyStringView) {
+    ASSERT_STREQ("", details::c_str({}).get());
+    ASSERT_STREQ("", details::c_str({nullptr, 0}).get());
+}
+
+class IncFsGetMetricsTest : public IncFsTestBase {
+protected:
+    int32_t getReadTimeout() override { return 100 /* 0.1 second */; }
+};
+
+TEST_F(IncFsGetMetricsTest, MetricsWithNoEvents) {
+    if (!(features() & Features::v2)) {
+        GTEST_SKIP() << "test not supported: IncFS is too old";
+        return;
+    }
+    IncFsLastReadError lastReadError = {.id = fileId(-1),
+                                        .timestampUs = static_cast<uint64_t>(-1),
+                                        .block = static_cast<IncFsBlockIndex>(-1),
+                                        .errorNo = static_cast<uint32_t>(-1),
+                                        .uid = static_cast<IncFsUid>(-1)};
+    EXPECT_EQ(0, IncFs_GetLastReadError(control_, &lastReadError));
+    // All fields should be zero
+    EXPECT_EQ(FileId{}, lastReadError.id);
+    EXPECT_EQ(0, (int)lastReadError.timestampUs);
+    EXPECT_EQ(0, (int)lastReadError.block);
+    EXPECT_EQ(0, (int)lastReadError.errorNo);
+    EXPECT_EQ(0, (int)lastReadError.uid);
+
+    IncFsMetrics incfsMetrics = {10, 10, 10, 10, 10, 10, 10, 10, 10};
+    EXPECT_EQ(0, IncFs_GetMetrics(metrics_key_.c_str(), &incfsMetrics));
+    EXPECT_EQ(0, (int)incfsMetrics.readsDelayedMin);
+    EXPECT_EQ(0, (int)incfsMetrics.readsDelayedMinUs);
+    EXPECT_EQ(0, (int)incfsMetrics.readsDelayedPending);
+    EXPECT_EQ(0, (int)incfsMetrics.readsDelayedPendingUs);
+    EXPECT_EQ(0, (int)incfsMetrics.readsFailedHashVerification);
+    EXPECT_EQ(0, (int)incfsMetrics.readsFailedOther);
+    EXPECT_EQ(0, (int)incfsMetrics.readsFailedTimedOut);
+}
+
+TEST_F(IncFsGetMetricsTest, MetricsWithReadsTimeOut) {
+    if (!(features() & Features::v2)) {
+        GTEST_SKIP() << "test not supported: IncFS is too old";
+        return;
+    }
+    const auto id = fileId(1);
+    ASSERT_EQ(0,
+              makeFile(control_, mountPath(test_file_name_), 0555, id,
+                       {.size = INCFS_DATA_FILE_BLOCK_SIZE}));
+
+    const auto file_path = mountPath(test_file_name_);
+    const android::base::unique_fd fd(open(file_path.c_str(), O_RDONLY | O_CLOEXEC | O_BINARY));
+    ASSERT_GE(fd.get(), 0);
+    // Read should timeout immediately
+    char buf[INCFS_DATA_FILE_BLOCK_SIZE];
+    EXPECT_FALSE(android::base::ReadFully(fd, buf, sizeof(buf)));
+    IncFsLastReadError lastReadError = {.id = fileId(-1),
+                                        .timestampUs = static_cast<uint64_t>(-1),
+                                        .block = static_cast<IncFsBlockIndex>(-1),
+                                        .errorNo = static_cast<uint32_t>(-1),
+                                        .uid = static_cast<IncFsUid>(-1)};
+    EXPECT_EQ(0, IncFs_GetLastReadError(control_, &lastReadError));
+    EXPECT_EQ(id, lastReadError.id);
+    EXPECT_TRUE(lastReadError.timestampUs > 0);
+    EXPECT_EQ(0, (int)lastReadError.block);
+    EXPECT_EQ(-ETIME, (int)lastReadError.errorNo);
+    EXPECT_EQ((int)getuid(), (int)lastReadError.uid);
+
+    IncFsMetrics incfsMetrics = {10, 10, 10, 10, 10, 10, 10, 10, 10};
+    EXPECT_EQ(0, IncFs_GetMetrics(metrics_key_.c_str(), &incfsMetrics));
+    EXPECT_EQ(0, (int)incfsMetrics.readsDelayedMin);
+    EXPECT_EQ(0, (int)incfsMetrics.readsDelayedMinUs);
+    EXPECT_EQ(0, (int)incfsMetrics.readsDelayedPending);
+    EXPECT_EQ(0, (int)incfsMetrics.readsDelayedPendingUs);
+    EXPECT_EQ(0, (int)incfsMetrics.readsFailedHashVerification);
+    EXPECT_EQ(0, (int)incfsMetrics.readsFailedOther);
+    EXPECT_EQ(1, (int)incfsMetrics.readsFailedTimedOut);
+}
+
+TEST_F(IncFsGetMetricsTest, MetricsWithHashFailure) {
+    if (!(features() & Features::v2)) {
+        GTEST_SKIP() << "test not supported: IncFS is too old";
+        return;
+    }
+    auto size = makeFileWithHash(1);
+    ASSERT_GT(size, 0);
+    // Make data and hash mismatch
+    const auto id = fileId(1);
+    char data[INCFS_DATA_FILE_BLOCK_SIZE]{static_cast<char>(-1)};
+    char hashData[INCFS_DATA_FILE_BLOCK_SIZE]{};
+    auto wfd = openForSpecialOps(control_, id);
+    ASSERT_GE(wfd.get(), 0);
+    DataBlock blocks[] = {{
+                                  .fileFd = wfd.get(),
+                                  .pageIndex = 0,
+                                  .compression = INCFS_COMPRESSION_KIND_NONE,
+                                  .dataSize = INCFS_DATA_FILE_BLOCK_SIZE,
+                                  .data = data,
+                          },
+                          {
+                                  .fileFd = wfd.get(),
+                                  // first hash page
+                                  .pageIndex = 0,
+                                  .compression = INCFS_COMPRESSION_KIND_NONE,
+                                  .dataSize = INCFS_DATA_FILE_BLOCK_SIZE,
+                                  .kind = INCFS_BLOCK_KIND_HASH,
+                                  .data = hashData,
+                          },
+                          {
+                                  .fileFd = wfd.get(),
+                                  .pageIndex = 2,
+                                  .compression = INCFS_COMPRESSION_KIND_NONE,
+                                  .dataSize = INCFS_DATA_FILE_BLOCK_SIZE,
+                                  .kind = INCFS_BLOCK_KIND_HASH,
+                                  .data = hashData,
+                          }};
+    ASSERT_EQ((int)std::size(blocks), writeBlocks({blocks, std::size(blocks)}));
+    const auto file_path = mountPath(test_file_name_);
+    const android::base::unique_fd fd(open(file_path.c_str(), O_RDONLY | O_CLOEXEC | O_BINARY));
+    ASSERT_GE(fd.get(), 0);
+    // Read should fail at reading the first block due to hash failure
+    char buf[INCFS_DATA_FILE_BLOCK_SIZE];
+    EXPECT_FALSE(android::base::ReadFully(fd, buf, sizeof(buf)));
+    IncFsLastReadError lastReadError = {.id = fileId(-1),
+                                        .timestampUs = static_cast<uint64_t>(-1),
+                                        .block = static_cast<IncFsBlockIndex>(-1),
+                                        .errorNo = static_cast<uint32_t>(-1),
+                                        .uid = static_cast<IncFsUid>(-1)};
+    EXPECT_EQ(0, IncFs_GetLastReadError(control_, &lastReadError));
+    EXPECT_EQ(0, std::strcmp(lastReadError.id.data, id.data));
+    EXPECT_TRUE(lastReadError.timestampUs > 0);
+    EXPECT_EQ(0, (int)lastReadError.block);
+    EXPECT_EQ(-EBADMSG, (int)lastReadError.errorNo);
+    EXPECT_EQ((int)getuid(), (int)lastReadError.uid);
+
+    IncFsMetrics incfsMetrics = {10, 10, 10, 10, 10, 10, 10, 10, 10};
+    EXPECT_EQ(0, IncFs_GetMetrics(metrics_key_.c_str(), &incfsMetrics));
+    EXPECT_EQ(0, (int)incfsMetrics.readsDelayedMin);
+    EXPECT_EQ(0, (int)incfsMetrics.readsDelayedMinUs);
+    EXPECT_EQ(0, (int)incfsMetrics.readsDelayedPending);
+    EXPECT_EQ(0, (int)incfsMetrics.readsDelayedPendingUs);
+    EXPECT_EQ(1, (int)incfsMetrics.readsFailedHashVerification);
+    EXPECT_EQ(0, (int)incfsMetrics.readsFailedOther);
+    EXPECT_EQ(0, (int)incfsMetrics.readsFailedTimedOut);
+}
+
+TEST_F(IncFsGetMetricsTest, MetricsWithReadsDelayed) {
+    if (!(features() & Features::v2)) {
+        GTEST_SKIP() << "test not supported: IncFS is too old";
+        return;
+    }
+    const auto id = fileId(1);
+    int testFileSize = INCFS_DATA_FILE_BLOCK_SIZE;
+    int waitBeforeWriteUs = 10000;
+    ASSERT_EQ(0, makeFile(control_, mountPath(test_file_name_), 0555, id, {.size = testFileSize}));
+    std::thread wait_before_write_thread([&]() {
+        std::vector<ReadInfoWithUid> pending_reads;
+        ASSERT_EQ(WaitResult::HaveData,
+                  waitForPendingReads(control_, std::chrono::seconds(1), &pending_reads));
+        // Additional wait is needed for the kernel jiffies counter to increment
+        usleep(waitBeforeWriteUs);
+        auto fd = openForSpecialOps(control_, fileId(1));
+        ASSERT_GE(fd.get(), 0);
+        std::vector<char> data(INCFS_DATA_FILE_BLOCK_SIZE);
+        auto block = DataBlock{
+                .fileFd = fd.get(),
+                .pageIndex = 0,
+                .compression = INCFS_COMPRESSION_KIND_NONE,
+                .dataSize = (uint32_t)data.size(),
+                .data = data.data(),
+        };
+        ASSERT_EQ(1, writeBlocks({&block, 1}));
+    });
+
+    const auto file_path = mountPath(test_file_name_);
+    const android::base::unique_fd fd(open(file_path.c_str(), O_RDONLY | O_CLOEXEC | O_BINARY));
+    ASSERT_GE(fd.get(), 0);
+    char buf[testFileSize];
+    EXPECT_TRUE(android::base::ReadFully(fd, buf, sizeof(buf)));
+    wait_before_write_thread.join();
+
+    IncFsLastReadError lastReadError = {.id = fileId(-1), 1, 1, 1, 1};
+    EXPECT_EQ(0, IncFs_GetLastReadError(control_, &lastReadError));
+    EXPECT_EQ(FileId{}, lastReadError.id);
+    EXPECT_EQ(0, (int)lastReadError.timestampUs);
+    EXPECT_EQ(0, (int)lastReadError.block);
+    EXPECT_EQ(0, (int)lastReadError.errorNo);
+    EXPECT_EQ(0, (int)lastReadError.uid);
+
+    IncFsMetrics incfsMetrics = {10, 10, 10, 10, 10, 10, 10, 10, 10};
+    EXPECT_EQ(0, IncFs_GetMetrics(metrics_key_.c_str(), &incfsMetrics));
+    EXPECT_EQ(0, (int)incfsMetrics.readsDelayedMin);
+    EXPECT_EQ(0, (int)incfsMetrics.readsDelayedMinUs);
+    EXPECT_EQ(1, (int)incfsMetrics.readsDelayedPending);
+    EXPECT_TRUE((int)incfsMetrics.readsDelayedPendingUs > 0);
+    EXPECT_EQ(0, (int)incfsMetrics.readsFailedHashVerification);
+    EXPECT_EQ(0, (int)incfsMetrics.readsFailedOther);
+    EXPECT_EQ(0, (int)incfsMetrics.readsFailedTimedOut);
+}
+
+TEST_F(IncFsGetMetricsTest, MetricsWithReadsDelayedPerUidTimeout) {
+    if (!(features() & Features::v2)) {
+        GTEST_SKIP() << "test not supported: IncFS is too old";
+        return;
+    }
+    const auto id = fileId(1);
+    int testFileSize = INCFS_DATA_FILE_BLOCK_SIZE;
+    ASSERT_EQ(0, makeFile(control_, mountPath(test_file_name_), 0555, id, {.size = testFileSize}));
+
+    auto fdToFill = openForSpecialOps(control_, fileId(1));
+    ASSERT_GE(fdToFill.get(), 0);
+    std::vector<char> data(INCFS_DATA_FILE_BLOCK_SIZE);
+    auto block = DataBlock{
+            .fileFd = fdToFill.get(),
+            .pageIndex = 0,
+            .compression = INCFS_COMPRESSION_KIND_NONE,
+            .dataSize = (uint32_t)data.size(),
+            .data = data.data(),
+    };
+    ASSERT_EQ(1, writeBlocks({&block, 1}));
+
+    // Set per-uid read timeout then read
+    uint32_t readTimeoutUs = 1000000;
+    IncFsUidReadTimeouts timeouts[1] = {
+            {static_cast<IncFsUid>(getuid()), readTimeoutUs, readTimeoutUs, readTimeoutUs}};
+    ASSERT_EQ(0, IncFs_SetUidReadTimeouts(control_, timeouts, std::size(timeouts)));
+    const auto file_path = mountPath(test_file_name_);
+    const android::base::unique_fd fd(open(file_path.c_str(), O_RDONLY | O_CLOEXEC | O_BINARY));
+    char buf[testFileSize];
+    ASSERT_GE(fd.get(), 0);
+    ASSERT_TRUE(android::base::ReadFully(fd, buf, sizeof(buf)));
+
+    IncFsLastReadError lastReadError = {.id = fileId(-1), 1, 1, 1, 1};
+    EXPECT_EQ(0, IncFs_GetLastReadError(control_, &lastReadError));
+    EXPECT_EQ(FileId{}, lastReadError.id);
+    EXPECT_EQ(0, (int)lastReadError.timestampUs);
+    EXPECT_EQ(0, (int)lastReadError.block);
+    EXPECT_EQ(0, (int)lastReadError.errorNo);
+    EXPECT_EQ(0, (int)lastReadError.uid);
+
+    IncFsMetrics incfsMetrics = {10, 10, 10, 10, 10, 10, 10, 10, 10};
+    EXPECT_EQ(0, IncFs_GetMetrics(metrics_key_.c_str(), &incfsMetrics));
+    EXPECT_EQ(1, (int)incfsMetrics.readsDelayedMin);
+    EXPECT_EQ(readTimeoutUs, (uint32_t)incfsMetrics.readsDelayedMinUs);
+    EXPECT_EQ(0, (int)incfsMetrics.readsDelayedPending);
+    EXPECT_EQ(0, (int)incfsMetrics.readsDelayedPendingUs);
+    EXPECT_EQ(0, (int)incfsMetrics.readsFailedHashVerification);
+    EXPECT_EQ(0, (int)incfsMetrics.readsFailedOther);
+    EXPECT_EQ(0, (int)incfsMetrics.readsFailedTimedOut);
 }
diff --git a/incfs/tests/include/IncFsTestBase.h b/incfs/tests/include/IncFsTestBase.h
index 520c736..5f8c311 100644
--- a/incfs/tests/include/IncFsTestBase.h
+++ b/incfs/tests/include/IncFsTestBase.h
@@ -42,9 +42,11 @@
         if (!enabled()) {
             GTEST_SKIP() << "test not supported: IncFS is not enabled";
         } else {
+            metrics_key_ = path::baseName(image_dir_path_);
             control_ = mount(image_dir_path_, mount_dir_path_,
                              MountOptions{.readLogBufferPages = 4,
-                                          .defaultReadTimeoutMs = getReadTimeout()});
+                                          .defaultReadTimeoutMs = getReadTimeout(),
+                                          .sysfsName = metrics_key_.c_str()});
             ASSERT_TRUE(control_.cmd() >= 0) << "Expected >= 0 got " << control_.cmd();
             ASSERT_TRUE(control_.pendingReads() >= 0);
             ASSERT_TRUE(control_.logs() >= 0);
@@ -84,13 +86,40 @@
         return path::join(mount_dir_path_, std::forward<Paths>(paths)...);
     }
 
+    virtual int makeFileWithHash(int id) {
+        // calculate the required size for two leaf hash blocks
+        constexpr auto size =
+                (INCFS_DATA_FILE_BLOCK_SIZE / INCFS_MAX_HASH_SIZE + 1) * INCFS_DATA_FILE_BLOCK_SIZE;
+
+        // assemble a signature/hashing data for it
+        struct __attribute__((packed)) Signature {
+            uint32_t version = INCFS_SIGNATURE_VERSION;
+            uint32_t hashingSize = sizeof(hashing);
+            struct __attribute__((packed)) Hashing {
+                uint32_t algo = INCFS_HASH_TREE_SHA256;
+                uint8_t log2Blocksize = 12;
+                uint32_t saltSize = 0;
+                uint32_t rootHashSize = INCFS_MAX_HASH_SIZE;
+                char rootHash[INCFS_MAX_HASH_SIZE] = {};
+            } hashing;
+            uint32_t signingSize = 0;
+        } signature;
+
+        int res = makeFile(control_, mountPath(test_file_name_), 0555, fileId(id),
+                           {.size = size,
+                            .signature = {.data = (char*)&signature, .size = sizeof(signature)}});
+        EXPECT_EQ(0, res);
+        return res ? -1 : size;
+    }
+
     std::string mount_dir_path_;
     std::optional<TemporaryDir> tmp_dir_for_mount_;
     std::string image_dir_path_;
     std::optional<TemporaryDir> tmp_dir_for_image_;
     inline static const std::string_view test_file_name_ = "test.txt";
     inline static const std::string_view test_dir_name_ = "test_dir";
+    std::string metrics_key_;
     Control control_;
 };
 
-} // namespace android::incfs
\ No newline at end of file
+} // namespace android::incfs
diff --git a/incfs/util/include/util/map_ptr.h b/incfs/util/include/util/map_ptr.h
index cda80f9..304540f 100644
--- a/incfs/util/include/util/map_ptr.h
+++ b/incfs/util/include/util/map_ptr.h
@@ -16,13 +16,16 @@
 
 #pragma once
 
-#include <memory>
-#include <shared_mutex>
-#include <vector>
-
 #include <android-base/logging.h>
 #include <android-base/off64_t.h>
 
+#include <atomic>
+#include <iterator>
+#include <memory>
+#include <shared_mutex>
+#include <type_traits>
+#include <vector>
+
 #ifdef __ANDROID__
 #include <linux/incrementalfs.h>
 #endif
@@ -54,15 +57,25 @@
 // This always uses MAP_SHARED.
 class IncFsFileMap final {
 public:
-    IncFsFileMap();
+    IncFsFileMap() noexcept;
     IncFsFileMap(IncFsFileMap&&) noexcept;
     IncFsFileMap& operator =(IncFsFileMap&&) noexcept;
-    ~IncFsFileMap();
+    ~IncFsFileMap() noexcept;
 
     // Initializes the map. Does not take ownership of the file descriptor.
     // Returns whether or not the file was able to be memory-mapped.
     bool Create(int fd, off64_t offset, size_t length, const char* file_name);
 
+    // Same thing, but allows verification to be disabled when `verify` is `false`, and enabled when
+    // `verify` is true and the file resides on IncFs.
+    bool Create(int fd, off64_t offset, size_t length, const char* file_name, bool verify);
+
+    // Same thing, but allows verification to be disabled when `verify` is `false`, and enabled when
+    // `verify` is true regardless of whether the file resides on IncFs (used for benchmarks and
+    // testing).
+    bool CreateForceVerification(int fd, off64_t offset, size_t length, const char* file_name,
+                                 bool verify);
+
     template <typename T = void>
     map_ptr<T> data() const {
         return map_ptr<T>(verification_enabled_ ? this : nullptr,
@@ -74,14 +87,13 @@
     off64_t offset() const;
     const char* file_name() const;
 
-private:
-    DISALLOW_COPY_AND_ASSIGN(IncFsFileMap);
-
-#ifdef __ANDROID__
+public:
     // Returns whether the data range is entirely present on IncFs.
     bool Verify(const uint8_t* const& data_start, const uint8_t* const& data_end,
                 const uint8_t** prev_verified_block) const;
-#endif
+
+private:
+    DISALLOW_COPY_AND_ASSIGN(IncFsFileMap);
 
     using bucket_t = uint8_t;
     static constexpr size_t kBucketBits = sizeof(bucket_t) * 8U;
@@ -342,15 +354,15 @@
     // Returns true if the elements are completely present; otherwise, returns false.
     template <typename T1 = T, NotVoid<T1> = 0, bool V1 = Verified, IsUnverified<V1> = 0>
     bool verify(size_t n = 1) const {
+        if (ptr_ == nullptr) {
+            return false;
+        }
+
 #ifdef __ANDROID__
         if (LIKELY(map_ == nullptr)) {
             return ptr_ != nullptr;
         }
 
-        if (ptr_ == nullptr) {
-            return false;
-        }
-
         const size_t verify_size = sizeof(T) * n;
         LIBINCFS_MAP_PTR_DEBUG_CODE(if (sizeof(T) <= verify_size) verified_ = true;);
 
diff --git a/incfs/util/map_ptr.cpp b/incfs/util/map_ptr.cpp
index 0b3e08b..f391506 100644
--- a/incfs/util/map_ptr.cpp
+++ b/incfs/util/map_ptr.cpp
@@ -25,10 +25,10 @@
 #include "util/map_ptr.h"
 
 namespace android::incfs {
-IncFsFileMap::IncFsFileMap() = default;
+IncFsFileMap::IncFsFileMap() noexcept = default;
 IncFsFileMap::IncFsFileMap(IncFsFileMap&&) noexcept = default;
 IncFsFileMap& IncFsFileMap::operator =(IncFsFileMap&&) noexcept = default;
-IncFsFileMap::~IncFsFileMap() = default;
+IncFsFileMap::~IncFsFileMap() noexcept = default;
 
 const void* IncFsFileMap::unsafe_data() const {
     return map_->getDataPtr();
@@ -46,25 +46,36 @@
     return map_->getFileName();
 }
 
+bool IncFsFileMap::Create(int fd, off64_t offset, size_t length, const char* file_name) {
+    return Create(fd, offset, length, file_name, true /* verify */);
+}
+
 #ifdef __ANDROID__
-bool IsVerificationEnabled(int fd) {
+static bool IsVerificationEnabled(int fd) {
     return isIncFsFd(fd) && isFullyLoaded(fd) != LoadingState::Full;
 }
 
 using data_block_index_t = uint32_t;
 
-data_block_index_t get_block_index(const uint8_t* ptr, const uint8_t* start_block_ptr) {
+static data_block_index_t get_block_index(const uint8_t* ptr, const uint8_t* start_block_ptr) {
     return (ptr - start_block_ptr) / INCFS_DATA_FILE_BLOCK_SIZE;
 }
 
-bool IncFsFileMap::Create(int fd, off64_t offset, size_t length, const char* file_name) {
+bool IncFsFileMap::Create(int fd, off64_t offset, size_t length, const char* file_name,
+                          bool verify) {
+    return CreateForceVerification(fd, offset, length, file_name,
+                                   verify && IsVerificationEnabled(fd));
+}
+
+bool IncFsFileMap::CreateForceVerification(int fd, off64_t offset, size_t length,
+                                           const char* file_name, bool verify) {
     map_ = std::make_unique<android::FileMap>();
     if (!map_->create(file_name, fd, offset, length, true /* readOnly */)) {
         return false;
     }
 
     fd_ = fd;
-    verification_enabled_ = IsVerificationEnabled(fd);
+    verification_enabled_ = verify;
     if (verification_enabled_) {
         // Initialize the block cache with enough buckets to hold all of the blocks within the
         // memory-mapped region.
@@ -125,10 +136,22 @@
 }
 
 #else
-bool IncFsFileMap::Create(int fd, off64_t offset, size_t length, const char* file_name) {
+bool IncFsFileMap::Create(int fd, off64_t offset, size_t length, const char* file_name,
+                          bool verify) {
+    return CreateForceVerification(fd, offset, length, file_name, verify);
+}
+
+bool IncFsFileMap::CreateForceVerification(int fd, off64_t offset, size_t length,
+                                           const char* file_name, bool /* verify */) {
     map_ = std::make_unique<android::FileMap>();
     return map_->create(file_name, fd, offset, length, true /* readOnly */);
 }
+
+bool IncFsFileMap::Verify(const uint8_t* const& /* data_start */,
+                          const uint8_t* const& /* data_end */,
+                          const uint8_t** /* prev_verified_block */) const {
+    return true;
+}
 #endif
 
 } // namespace android::incfs
\ No newline at end of file
diff --git a/libdataloader/Android.bp b/libdataloader/Android.bp
index 7bd2696..eb9adf4 100644
--- a/libdataloader/Android.bp
+++ b/libdataloader/Android.bp
@@ -44,7 +44,9 @@
         "liblog",
         "libutils",
     ],
-    static_libs: ["libnativehelper_lazy"],
+    static_libs: [
+        "libnativehelper_lazy",
+    ],
     tidy: true,
     tidy_checks: [
         "android-*",
diff --git a/libdataloader/DataLoaderConnector.cpp b/libdataloader/DataLoaderConnector.cpp
index da10484..f5f82ea 100644
--- a/libdataloader/DataLoaderConnector.cpp
+++ b/libdataloader/DataLoaderConnector.cpp
@@ -30,12 +30,14 @@
 #include "ManagedDataLoader.h"
 #include "dataloader.h"
 #include "incfs.h"
+#include "path.h"
 
 namespace {
 
 using namespace std::literals;
 
 using ReadInfo = android::dataloader::ReadInfo;
+using ReadInfoWithUid = android::dataloader::ReadInfoWithUid;
 
 using FileId = android::incfs::FileId;
 using RawMetadata = android::incfs::RawMetadata;
@@ -70,6 +72,7 @@
     jfieldID controlCmd;
     jfieldID controlPendingReads;
     jfieldID controlLog;
+    jfieldID controlBlocksWritten;
 
     jfieldID paramsType;
     jfieldID paramsPackageName;
@@ -114,8 +117,6 @@
         constants.DATA_LOADER_UNRECOVERABLE =
                 GetStaticIntFieldValueOrDie(env, listener, "DATA_LOADER_UNRECOVERABLE");
 
-        CHECK(constants.DATA_LOADER_UNRECOVERABLE == DATA_LOADER_UNRECOVERABLE);
-
         auto packageInstaller = (jclass)FindClassOrDie(env, "android/content/pm/PackageInstaller");
 
         constants.DATA_LOADER_TYPE_NONE =
@@ -161,6 +162,8 @@
         controlPendingReads = GetFieldIDOrDie(env, incControl, "pendingReads",
                                               "Landroid/os/ParcelFileDescriptor;");
         controlLog = GetFieldIDOrDie(env, incControl, "log", "Landroid/os/ParcelFileDescriptor;");
+        controlBlocksWritten = GetFieldIDOrDie(env, incControl, "blocksWritten",
+                                               "Landroid/os/ParcelFileDescriptor;");
 
         auto params = FindClassOrDie(env, "android/content/pm/DataLoaderParamsParcel");
         paramsType = GetFieldIDOrDie(env, params, "type", "I");
@@ -207,6 +210,17 @@
     return ids;
 }
 
+bool checkAndClearJavaException(JNIEnv* env, std::string_view method) {
+    if (!env->ExceptionCheck()) {
+        return false;
+    }
+
+    LOG(ERROR) << "Java exception during DataLoader::" << method;
+    env->ExceptionDescribe();
+    env->ExceptionClear();
+    return true;
+}
+
 bool reportStatusViaCallback(JNIEnv* env, jobject listener, jint storageId, jint status) {
     if (listener == nullptr) {
         ALOGE("No listener object to talk to IncrementalService. "
@@ -231,14 +245,10 @@
 using DataLoaderConnectorsMap = std::unordered_map<int, DataLoaderConnectorPtr>;
 
 struct Globals {
-    Globals() {
-        managedDataLoaderFactory =
-                new android::dataloader::details::DataLoaderFactoryImpl([](auto jvm, auto) {
-                    return std::make_unique<android::dataloader::ManagedDataLoader>(jvm);
-                });
-    }
+    Globals() { managedDataLoaderFactory = new android::dataloader::ManagedDataLoaderFactory(); }
 
     DataLoaderFactory* managedDataLoaderFactory = nullptr;
+    DataLoaderFactory* legacyDataLoaderFactory = nullptr;
     DataLoaderFactory* dataLoaderFactory = nullptr;
 
     std::mutex dataLoaderConnectorsLock;
@@ -250,6 +260,8 @@
     std::thread logLooperThread;
     std::vector<ReadInfo> pendingReads;
     std::vector<ReadInfo> pageReads;
+    std::vector<ReadInfoWithUid> pendingReadsWithUid;
+    std::vector<ReadInfoWithUid> pageReadsWithUid;
 };
 
 static Globals& globals() {
@@ -306,36 +318,68 @@
     DataLoaderConnector(const DataLoaderConnector&) = delete;
     DataLoaderConnector(const DataLoaderConnector&&) = delete;
     virtual ~DataLoaderConnector() {
+        if (mDataLoader && mDataLoader->onDestroy) {
+            mDataLoader->onDestroy(mDataLoader);
+            checkAndClearJavaException(__func__);
+        }
+        mDataLoader = nullptr;
+
         JNIEnv* env = GetOrAttachJNIEnvironment(mJvm);
 
+        const auto& jni = jniIds(env);
+        reportStatusViaCallback(env, mListener, mStorageId, jni.constants.DATA_LOADER_DESTROYED);
+
         env->DeleteGlobalRef(mService);
         env->DeleteGlobalRef(mServiceConnector);
         env->DeleteGlobalRef(mCallbackControl);
         env->DeleteGlobalRef(mListener);
     } // to avoid delete-non-virtual-dtor
 
+    bool tryFactory(DataLoaderFactory* factory, bool withFeatures,
+                    const DataLoaderParamsPair& params, jobject managedParams) {
+        if (!factory) {
+            return true;
+        }
+
+        // Let's try the non-default first.
+        mDataLoader = factory->onCreate(factory, &params.ndkDataLoaderParams(), this, this, mJvm,
+                                        mService, managedParams);
+        if (checkAndClearJavaException(__func__)) {
+            return false;
+        }
+        if (!mDataLoader) {
+            return true;
+        }
+
+        mDataLoaderFeatures = withFeatures && mDataLoader->getFeatures
+                ? mDataLoader->getFeatures(mDataLoader)
+                : DATA_LOADER_FEATURE_NONE;
+        if (mDataLoaderFeatures & DATA_LOADER_FEATURE_UID) {
+            ALOGE("DataLoader supports UID");
+            CHECK(mDataLoader->onPageReadsWithUid);
+            CHECK(mDataLoader->onPendingReadsWithUid);
+        }
+        return true;
+    }
+
     bool onCreate(const DataLoaderParamsPair& params, jobject managedParams) {
         CHECK(mDataLoader == nullptr);
 
-        if (auto factory = globals().dataLoaderFactory) {
-            // Let's try the non-default first.
-            mDataLoader = factory->onCreate(factory, &params.ndkDataLoaderParams(), this, this,
-                                            mJvm, mService, managedParams);
-            if (checkAndClearJavaException(__func__)) {
-                return false;
-            }
+        if (!mDataLoader &&
+            !tryFactory(globals().dataLoaderFactory, /*withFeatures=*/true, params,
+                        managedParams)) {
+            return false;
         }
-
-        if (!mDataLoader) {
-            // Didn't work, let's try the default.
-            auto factory = globals().managedDataLoaderFactory;
-            mDataLoader = factory->onCreate(factory, &params.ndkDataLoaderParams(), this, this,
-                                            mJvm, mService, managedParams);
-            if (checkAndClearJavaException(__func__)) {
-                return false;
-            }
+        if (!mDataLoader &&
+            !tryFactory(globals().legacyDataLoaderFactory, /*withFeatures=*/false, params,
+                        managedParams)) {
+            return false;
         }
-
+        if (!mDataLoader &&
+            !tryFactory(globals().managedDataLoaderFactory, /*withFeatures=*/false, params,
+                        managedParams)) {
+            return false;
+        }
         if (!mDataLoader) {
             return false;
         }
@@ -344,7 +388,7 @@
     }
     bool onStart() {
         CHECK(mDataLoader);
-        bool result = mDataLoader->onStart(mDataLoader);
+        bool result = !mDataLoader->onStart || mDataLoader->onStart(mDataLoader);
         if (checkAndClearJavaException(__func__)) {
             result = false;
         }
@@ -360,26 +404,21 @@
         std::lock_guard{mPendingReadsLooperBusy}; // NOLINT
         std::lock_guard{mLogLooperBusy}; // NOLINT
 
-        mDataLoader->onStop(mDataLoader);
-        checkAndClearJavaException(__func__);
-    }
-    void onDestroy() {
-        CHECK(mDataLoader);
-        mDataLoader->onDestroy(mDataLoader);
+        if (mDataLoader->onStop) {
+            mDataLoader->onStop(mDataLoader);
+        }
         checkAndClearJavaException(__func__);
     }
 
     bool onPrepareImage(const android::dataloader::DataLoaderInstallationFiles& addedFiles) {
         CHECK(mDataLoader);
-        bool result =
+        bool result = !mDataLoader->onPrepareImage ||
                 mDataLoader->onPrepareImage(mDataLoader, addedFiles.data(), addedFiles.size());
-        if (checkAndClearJavaException(__func__)) {
-            result = false;
-        }
         return result;
     }
 
-    int onPendingReadsLooperEvent(std::vector<ReadInfo>& pendingReads) {
+    template <class ReadInfoType>
+    int onPendingReadsLooperEvent(std::vector<ReadInfoType>& pendingReads) {
         CHECK(mDataLoader);
         std::lock_guard lock{mPendingReadsLooperBusy};
         while (mRunning.load(std::memory_order_relaxed)) {
@@ -389,11 +428,23 @@
                 pendingReads.empty()) {
                 return 1;
             }
-            mDataLoader->onPendingReads(mDataLoader, pendingReads.data(), pendingReads.size());
+            if constexpr (std::is_same_v<ReadInfoType, ReadInfo>) {
+                if (mDataLoader->onPendingReads) {
+                    mDataLoader->onPendingReads(mDataLoader, pendingReads.data(),
+                                                pendingReads.size());
+                }
+            } else {
+                if (mDataLoader->onPendingReadsWithUid) {
+                    mDataLoader->onPendingReadsWithUid(mDataLoader, pendingReads.data(),
+                                                       pendingReads.size());
+                }
+            }
         }
         return 1;
     }
-    int onLogLooperEvent(std::vector<ReadInfo>& pageReads) {
+
+    template <class ReadInfoType>
+    int onLogLooperEvent(std::vector<ReadInfoType>& pageReads) {
         CHECK(mDataLoader);
         std::lock_guard lock{mLogLooperBusy};
         while (mRunning.load(std::memory_order_relaxed)) {
@@ -403,11 +454,40 @@
                 pageReads.empty()) {
                 return 1;
             }
-            mDataLoader->onPageReads(mDataLoader, pageReads.data(), pageReads.size());
+            if constexpr (std::is_same_v<ReadInfoType, ReadInfo>) {
+                if (mDataLoader->onPageReads) {
+                    mDataLoader->onPageReads(mDataLoader, pageReads.data(), pageReads.size());
+                }
+            } else {
+                if (mDataLoader->onPageReadsWithUid) {
+                    mDataLoader->onPageReadsWithUid(mDataLoader, pageReads.data(),
+                                                    pageReads.size());
+                }
+            }
         }
         return 1;
     }
 
+    int onPendingReadsLooperEvent(std::vector<ReadInfo>& pendingReads,
+                                  std::vector<ReadInfoWithUid>& pendingReadsWithUid) {
+        CHECK(mDataLoader);
+        if (mDataLoaderFeatures & DATA_LOADER_FEATURE_UID) {
+            return this->onPendingReadsLooperEvent(pendingReadsWithUid);
+        } else {
+            return this->onPendingReadsLooperEvent(pendingReads);
+        }
+    }
+
+    int onLogLooperEvent(std::vector<ReadInfo>& pageReads,
+                         std::vector<ReadInfoWithUid>& pageReadsWithUid) {
+        CHECK(mDataLoader);
+        if (mDataLoaderFeatures & DATA_LOADER_FEATURE_UID) {
+            return this->onLogLooperEvent(pageReadsWithUid);
+        } else {
+            return this->onLogLooperEvent(pageReads);
+        }
+    }
+
     void writeData(jstring name, jlong offsetBytes, jlong lengthBytes, jobject incomingFd) const {
         CHECK(mCallbackControl);
         JNIEnv* env = GetOrAttachJNIEnvironment(mJvm);
@@ -445,25 +525,28 @@
     }
 
     bool reportStatus(DataLoaderStatus status) {
-        if (status < DATA_LOADER_FIRST_STATUS || DATA_LOADER_LAST_STATUS < status) {
-            ALOGE("Unable to report invalid status. status=%d", status);
-            return false;
-        }
         JNIEnv* env = GetOrAttachJNIEnvironment(mJvm);
-        return reportStatusViaCallback(env, mListener, mStorageId, status);
+        const auto& jni = jniIds(env);
+
+        jint osStatus;
+        switch (status) {
+            case DATA_LOADER_UNAVAILABLE:
+                osStatus = jni.constants.DATA_LOADER_UNAVAILABLE;
+                break;
+            case DATA_LOADER_UNRECOVERABLE:
+                osStatus = jni.constants.DATA_LOADER_UNRECOVERABLE;
+                break;
+            default: {
+                ALOGE("Unable to report invalid status. status=%d", status);
+                return false;
+            }
+        }
+        return reportStatusViaCallback(env, mListener, mStorageId, osStatus);
     }
 
     bool checkAndClearJavaException(std::string_view method) const {
         JNIEnv* env = GetOrAttachJNIEnvironment(mJvm);
-
-        if (!env->ExceptionCheck()) {
-            return false;
-        }
-
-        LOG(ERROR) << "Java exception during DataLoader::" << method;
-        env->ExceptionDescribe();
-        env->ExceptionClear();
-        return true;
+        return ::checkAndClearJavaException(env, method);
     }
 
     const UniqueControl& control() const { return mControl; }
@@ -480,6 +563,7 @@
     UniqueControl const mControl;
 
     ::DataLoader* mDataLoader = nullptr;
+    DataLoaderFeatures mDataLoaderFeatures = DATA_LOADER_FEATURE_NONE;
 
     std::mutex mPendingReadsLooperBusy;
     std::mutex mLogLooperBusy;
@@ -492,7 +576,8 @@
         return 0;
     }
     auto&& dataLoaderConnector = (DataLoaderConnector*)data;
-    return dataLoaderConnector->onPendingReadsLooperEvent(globals().pendingReads);
+    return dataLoaderConnector->onPendingReadsLooperEvent(globals().pendingReads,
+                                                          globals().pendingReadsWithUid);
 }
 
 static int onLogLooperEvent(int fd, int events, void* data) {
@@ -501,7 +586,7 @@
         return 0;
     }
     auto&& dataLoaderConnector = (DataLoaderConnector*)data;
-    return dataLoaderConnector->onLogLooperEvent(globals().pageReads);
+    return dataLoaderConnector->onLogLooperEvent(globals().pageReads, globals().pageReadsWithUid);
 }
 
 static int createFdFromManaged(JNIEnv* env, jobject pfd) {
@@ -534,7 +619,10 @@
     auto pr = createFdFromManaged(env,
                                   env->GetObjectField(managedIncControl, jni.controlPendingReads));
     auto log = createFdFromManaged(env, env->GetObjectField(managedIncControl, jni.controlLog));
-    return android::incfs::createControl(cmd, pr, log);
+    auto blocksWritten =
+            createFdFromManaged(env,
+                                env->GetObjectField(managedIncControl, jni.controlBlocksWritten));
+    return android::incfs::createControl(cmd, pr, log, blocksWritten);
 }
 
 DataLoaderParamsPair::DataLoaderParamsPair(android::dataloader::DataLoaderParams&& dataLoaderParams)
@@ -568,14 +656,14 @@
 }
 
 static void pendingReadsLooperThread() {
-    constexpr auto kTimeoutMsecs = 60 * 1000;
+    constexpr auto kTimeoutMsecs = -1;
     while (!globals().stopped) {
         pendingReadsLooper().pollAll(kTimeoutMsecs);
     }
 }
 
 static void logLooperThread() {
-    constexpr auto kTimeoutMsecs = 60 * 1000;
+    constexpr auto kTimeoutMsecs = -1;
     while (!globals().stopped) {
         logLooper().pollAll(kTimeoutMsecs);
     }
@@ -615,6 +703,11 @@
 
 void DataLoader_Initialize(struct ::DataLoaderFactory* factory) {
     CHECK(factory) << "DataLoader factory is invalid.";
+    globals().legacyDataLoaderFactory = factory;
+}
+
+void DataLoader_Initialize_WithFeatures(struct ::DataLoaderFactory* factory) {
+    CHECK(factory) << "DataLoader factory is invalid.";
     globals().dataLoaderFactory = factory;
 }
 
@@ -667,15 +760,23 @@
         }
     }
     auto nativeControl = createIncFsControlFromManaged(env, control);
-    ALOGI("DataLoader::create1 cmd: %d|%s", nativeControl.cmd(),
-          pathFromFd(nativeControl.cmd()).c_str());
-    ALOGI("DataLoader::create1 pendingReads: %d|%s", nativeControl.pendingReads(),
-          pathFromFd(nativeControl.pendingReads()).c_str());
-    ALOGI("DataLoader::create1 log: %d|%s", nativeControl.logs(),
-          pathFromFd(nativeControl.logs()).c_str());
+    if (nativeControl) {
+        using namespace android::incfs;
+        ALOGI("DataLoader::create incremental fds: %d/%d/%d/%d", nativeControl.cmd(),
+              nativeControl.pendingReads(), nativeControl.logs(), nativeControl.blocksWritten());
+        auto cmdPath = pathFromFd(nativeControl.cmd());
+        auto dir = path::dirName(cmdPath);
+        ALOGI("DataLoader::create incremental dir: %s, files: %s/%s/%s/%s",
+              details::c_str(dir).get(), details::c_str(path::baseName(cmdPath)).get(),
+              details::c_str(path::baseName(pathFromFd(nativeControl.pendingReads()))).get(),
+              details::c_str(path::baseName(pathFromFd(nativeControl.logs()))).get(),
+              details::c_str(path::baseName(pathFromFd(nativeControl.blocksWritten()))).get());
+    } else {
+        ALOGI("DataLoader::create no incremental control");
+    }
 
     auto nativeParams = DataLoaderParamsPair::createFromManaged(env, params);
-    ALOGI("DataLoader::create2: %d|%s|%s|%s", nativeParams.dataLoaderParams().type(),
+    ALOGI("DataLoader::create params: %d|%s|%s|%s", nativeParams.dataLoaderParams().type(),
           nativeParams.dataLoaderParams().packageName().c_str(),
           nativeParams.dataLoaderParams().className().c_str(),
           nativeParams.dataLoaderParams().arguments().c_str());
@@ -694,6 +795,7 @@
     auto dataLoaderConnector =
             std::make_unique<DataLoaderConnector>(env, service, storageId, std::move(nativeControl),
                                                   serviceConnector, callbackControl, listener);
+    bool created = dataLoaderConnector->onCreate(nativeParams, params);
     {
         std::lock_guard lock{globals().dataLoaderConnectorsLock};
         auto [dlIt, dlInserted] =
@@ -703,7 +805,8 @@
             ALOGE("id(%d): already exist, skipping creation.", storageId);
             return false;
         }
-        if (!dlIt->second->onCreate(nativeParams, params)) {
+
+        if (!created) {
             globals().dataLoaderConnectors.erase(dlIt);
             // Enable the reporter.
             reportUnavailableOnExit.reset(listener);
@@ -729,8 +832,6 @@
     std::unique_ptr<_jobject, decltype(destroyAndReportUnavailable)>
             destroyAndReportUnavailableOnExit(nullptr, destroyAndReportUnavailable);
 
-    const UniqueControl* control;
-    jobject listener;
     DataLoaderConnectorPtr dataLoaderConnector;
     {
         std::lock_guard lock{globals().dataLoaderConnectorsLock};
@@ -739,41 +840,44 @@
             ALOGE("Failed to start id(%d): not found", storageId);
             return false;
         }
-
-        listener = dlIt->second->getListenerLocalRef(env);
-
         dataLoaderConnector = dlIt->second;
-        if (!dataLoaderConnector->onStart()) {
-            ALOGE("Failed to start id(%d): onStart returned false", storageId);
-            destroyAndReportUnavailableOnExit.reset(listener);
-            return false;
-        }
+    }
+    const UniqueControl* control = &(dataLoaderConnector->control());
+    jobject listener = dataLoaderConnector->getListenerLocalRef(env);
 
-        control = &(dataLoaderConnector->control());
-
-        // Create loopers while we are under lock.
-        if (control->pendingReads() >= 0 && !globals().pendingReadsLooperThread.joinable()) {
-            pendingReadsLooper();
-            globals().pendingReadsLooperThread = std::thread(&pendingReadsLooperThread);
-        }
-        if (control->logs() >= 0 && !globals().logLooperThread.joinable()) {
-            logLooper();
-            globals().logLooperThread = std::thread(&logLooperThread);
-        }
+    if (!dataLoaderConnector->onStart()) {
+        ALOGE("Failed to start id(%d): onStart returned false", storageId);
+        destroyAndReportUnavailableOnExit.reset(listener);
+        return false;
     }
 
     if (control->pendingReads() >= 0) {
-        pendingReadsLooper().addFd(control->pendingReads(), android::Looper::POLL_CALLBACK,
-                                   android::Looper::EVENT_INPUT, &onPendingReadsLooperEvent,
-                                   dataLoaderConnector.get());
-        pendingReadsLooper().wake();
+        auto&& looper = pendingReadsLooper();
+        if (!globals().pendingReadsLooperThread.joinable()) {
+            std::lock_guard lock{globals().dataLoaderConnectorsLock};
+            if (!globals().pendingReadsLooperThread.joinable()) {
+                globals().pendingReadsLooperThread = std::thread(&pendingReadsLooperThread);
+            }
+        }
+
+        looper.addFd(control->pendingReads(), android::Looper::POLL_CALLBACK,
+                     android::Looper::EVENT_INPUT, &onPendingReadsLooperEvent,
+                     dataLoaderConnector.get());
+        looper.wake();
     }
 
     if (control->logs() >= 0) {
-        logLooper().addFd(control->logs(), android::Looper::POLL_CALLBACK,
-                          android::Looper::EVENT_INPUT, &onLogLooperEvent,
-                          dataLoaderConnector.get());
-        logLooper().wake();
+        auto&& looper = logLooper();
+        if (!globals().logLooperThread.joinable()) {
+            std::lock_guard lock{globals().dataLoaderConnectorsLock};
+            if (!globals().logLooperThread.joinable()) {
+                globals().logLooperThread = std::thread(&logLooperThread);
+            }
+        }
+
+        looper.addFd(control->logs(), android::Looper::POLL_CALLBACK, android::Looper::EVENT_INPUT,
+                     &onLogLooperEvent, dataLoaderConnector.get());
+        looper.wake();
     }
 
     const auto& jni = jniIds(env);
@@ -782,17 +886,8 @@
     return true;
 }
 
-jobject DataLoaderService_OnStop_NoStatus(JNIEnv* env, jint storageId) {
-    const UniqueControl* control;
-    {
-        std::lock_guard lock{globals().dataLoaderConnectorsLock};
-        auto dlIt = globals().dataLoaderConnectors.find(storageId);
-        if (dlIt == globals().dataLoaderConnectors.end()) {
-            return nullptr;
-        }
-        control = &(dlIt->second->control());
-    }
-
+static void DataLoaderService_OnStop_NoStatus(const UniqueControl* control,
+                                              const DataLoaderConnectorPtr& dataLoaderConnector) {
     if (control->pendingReads() >= 0) {
         pendingReadsLooper().removeFd(control->pendingReads());
         pendingReadsLooper().wake();
@@ -801,30 +896,25 @@
         logLooper().removeFd(control->logs());
         logLooper().wake();
     }
+    dataLoaderConnector->onStop();
+}
 
-    jobject listener = nullptr;
+bool DataLoaderService_OnStop(JNIEnv* env, jint storageId) {
+    DataLoaderConnectorPtr dataLoaderConnector;
     {
         std::lock_guard lock{globals().dataLoaderConnectorsLock};
         auto dlIt = globals().dataLoaderConnectors.find(storageId);
         if (dlIt == globals().dataLoaderConnectors.end()) {
             ALOGI("Failed to stop id(%d): not found", storageId);
-            return nullptr;
+            return true;
         }
-
-        listener = dlIt->second->getListenerLocalRef(env);
-
-        auto&& dataLoaderConnector = dlIt->second;
-        dataLoaderConnector->onStop();
+        dataLoaderConnector = dlIt->second;
     }
-    return listener;
-}
+    const UniqueControl* control = &(dataLoaderConnector->control());
+    jobject listener = dataLoaderConnector->getListenerLocalRef(env);
 
-bool DataLoaderService_OnStop(JNIEnv* env, jint storageId) {
-    auto listener = DataLoaderService_OnStop_NoStatus(env, storageId);
-    if (listener == nullptr) {
-        ALOGI("Failed to stop id(%d): not found", storageId);
-        return true;
-    }
+    // Just stop.
+    DataLoaderService_OnStop_NoStatus(control, dataLoaderConnector);
 
     const auto& jni = jniIds(env);
     reportStatusViaCallback(env, listener, storageId, jni.constants.DATA_LOADER_STOPPED);
@@ -832,36 +922,26 @@
     return true;
 }
 
-jobject DataLoaderService_OnDestroy_NoStatus(JNIEnv* env, jint storageId) {
-    jobject listener = DataLoaderService_OnStop_NoStatus(env, storageId);
-    if (!listener) {
-        return nullptr;
-    }
-
+bool DataLoaderService_OnDestroy(JNIEnv* env, jint storageId) {
+    DataLoaderConnectorPtr dataLoaderConnector;
     {
         std::lock_guard lock{globals().dataLoaderConnectorsLock};
         auto dlIt = globals().dataLoaderConnectors.find(storageId);
         if (dlIt == globals().dataLoaderConnectors.end()) {
-            return nullptr;
+            ALOGI("Failed to destroy id(%d): not found", storageId);
+            return true;
         }
-
-        auto&& dataLoaderConnector = dlIt->second;
-        dataLoaderConnector->onDestroy();
+        dataLoaderConnector = std::move(dlIt->second);
         globals().dataLoaderConnectors.erase(dlIt);
     }
+    const UniqueControl* control = &(dataLoaderConnector->control());
 
-    return listener;
-}
-
-bool DataLoaderService_OnDestroy(JNIEnv* env, jint storageId) {
-    jobject listener = DataLoaderService_OnDestroy_NoStatus(env, storageId);
-    if (!listener) {
-        ALOGI("Failed to remove id(%d): not found", storageId);
-        return true;
-    }
-
-    const auto& jni = jniIds(env);
-    reportStatusViaCallback(env, listener, storageId, jni.constants.DATA_LOADER_DESTROYED);
+    // Stop/destroy.
+    DataLoaderService_OnStop_NoStatus(control, dataLoaderConnector);
+    // This will destroy the last instance of the DataLoaderConnectorPtr and should trigger the
+    // destruction of the DataLoader. However if there are any hanging instances, the destruction
+    // will be postponed. E.g. OnPrepareImage in progress at the same time we call OnDestroy.
+    dataLoaderConnector = {};
 
     return true;
 }
@@ -931,7 +1011,6 @@
 
 bool DataLoaderService_OnPrepareImage(JNIEnv* env, jint storageId, jobjectArray addedFiles,
                                       jobjectArray removedFiles) {
-    jobject listener;
     DataLoaderConnectorPtr dataLoaderConnector;
     {
         std::lock_guard lock{globals().dataLoaderConnectorsLock};
@@ -940,14 +1019,20 @@
             ALOGE("Failed to handle onPrepareImage for id(%d): not found", storageId);
             return false;
         }
-        listener = dlIt->second->getListenerLocalRef(env);
         dataLoaderConnector = dlIt->second;
     }
+    jobject listener = dataLoaderConnector->getListenerLocalRef(env);
 
     auto addedFilesPair = DataLoaderInstallationFilesPair::createFromManaged(env, addedFiles);
     bool result = dataLoaderConnector->onPrepareImage(addedFilesPair.ndkFiles());
 
     const auto& jni = jniIds(env);
+
+    if (checkAndClearJavaException(env, "onPrepareImage")) {
+        reportStatusViaCallback(env, listener, storageId, jni.constants.DATA_LOADER_UNAVAILABLE);
+        return false;
+    }
+
     reportStatusViaCallback(env, listener, storageId,
                             result ? jni.constants.DATA_LOADER_IMAGE_READY
                                    : jni.constants.DATA_LOADER_IMAGE_NOT_READY);
diff --git a/libdataloader/ManagedDataLoader.cpp b/libdataloader/ManagedDataLoader.cpp
index 8f3e888..c277d45 100644
--- a/libdataloader/ManagedDataLoader.cpp
+++ b/libdataloader/ManagedDataLoader.cpp
@@ -91,18 +91,32 @@
 
 } // namespace
 
-ManagedDataLoader::ManagedDataLoader(JavaVM* jvm) : mJvm(jvm) {
+ManagedDataLoader::ManagedDataLoader(JavaVM* jvm, jobject dataLoader)
+      : mJvm(jvm), mDataLoader(dataLoader) {
     CHECK(mJvm);
+
+    LegacyDataLoader::onStart = [](auto) -> bool { return true; };
+    LegacyDataLoader::onStop = [](auto) {};
+    LegacyDataLoader::onDestroy = [](LegacyDataLoader* self) {
+        auto me = static_cast<ManagedDataLoader*>(self);
+        me->onDestroy();
+        delete me;
+    };
+    LegacyDataLoader::onPrepareImage = [](auto* self, const auto addedFiles[],
+                                          int addedFilesCount) -> bool {
+        return static_cast<ManagedDataLoader*>(self)->onPrepareImage(
+                DataLoaderInstallationFiles(addedFiles, addedFilesCount));
+    };
+    LegacyDataLoader::onPendingReads = [](auto, auto, auto) {};
+    LegacyDataLoader::onPageReads = [](auto, auto, auto) {};
 }
 
-bool ManagedDataLoader::onCreate(const android::dataloader::DataLoaderParams&,
-                                 android::dataloader::FilesystemConnectorPtr ifs,
-                                 android::dataloader::StatusListenerPtr listener,
-                                 android::dataloader::ServiceConnectorPtr service,
-                                 android::dataloader::ServiceParamsPtr params) {
-    CHECK(!mDataLoader);
-
-    JNIEnv* env = GetJNIEnvironment(mJvm);
+LegacyDataLoader* ManagedDataLoader::create(JavaVM* jvm,
+                                            android::dataloader::FilesystemConnectorPtr ifs,
+                                            android::dataloader::StatusListenerPtr listener,
+                                            android::dataloader::ServiceConnectorPtr service,
+                                            android::dataloader::ServiceParamsPtr params) {
+    JNIEnv* env = GetJNIEnvironment(jvm);
     const auto& jni = jniIds(env);
 
     jobject dlp = env->NewObject(jni.dataLoaderParams, jni.dataLoaderParamsConstruct, params);
@@ -112,14 +126,16 @@
     auto dataLoader = env->CallObjectMethod(service, jni.dataLoaderServiceOnCreateDataLoader, dlp);
     if (!dataLoader) {
         LOG(ERROR) << "Failed to create Java DataLoader.";
-        return false;
+        return nullptr;
     }
     if (env->ExceptionCheck()) {
-        return false;
+        return nullptr;
+    }
+    if (!env->CallBooleanMethod(dataLoader, jni.dataLoaderOnCreate, dlp, ifsc)) {
+        return nullptr;
     }
 
-    mDataLoader = env->NewGlobalRef(dataLoader);
-    return env->CallBooleanMethod(mDataLoader, jni.dataLoaderOnCreate, dlp, ifsc);
+    return new ManagedDataLoader(jvm, env->NewGlobalRef(dataLoader));
 }
 
 void ManagedDataLoader::onDestroy() {
@@ -163,4 +179,18 @@
     return env->CallBooleanMethod(mDataLoader, jni.dataLoaderOnPrepareImage, jaddedFiles, nullptr);
 }
 
+ManagedDataLoaderFactory::ManagedDataLoaderFactory() {
+    ::DataLoaderFactory::onCreate =
+            [](::DataLoaderFactory* self, const ::DataLoaderParams* ndkParams,
+               ::DataLoaderFilesystemConnectorPtr fsConnector,
+               ::DataLoaderStatusListenerPtr statusListener, ::DataLoaderServiceVmPtr vm,
+               ::DataLoaderServiceConnectorPtr serviceConnector,
+               ::DataLoaderServiceParamsPtr serviceParams) -> ::DataLoader* {
+        return reinterpret_cast<::DataLoader*>(
+                ManagedDataLoader::create(vm, static_cast<FilesystemConnector*>(fsConnector),
+                                          static_cast<StatusListener*>(statusListener),
+                                          serviceConnector, serviceParams));
+    };
+}
+
 } // namespace android::dataloader
diff --git a/libdataloader/ManagedDataLoader.h b/libdataloader/ManagedDataLoader.h
index 33d00c6..b9f714f 100644
--- a/libdataloader/ManagedDataLoader.h
+++ b/libdataloader/ManagedDataLoader.h
@@ -17,30 +17,50 @@
 
 #include <dataloader.h>
 
+__BEGIN_DECLS
+
+// This simulates legacy dataloader (compiled with previous version of libincfs_dataloader).
+// We still need to be able to support them.
+struct LegacyDataLoader {
+    bool (*onStart)(struct LegacyDataLoader* self);
+    void (*onStop)(struct LegacyDataLoader* self);
+    void (*onDestroy)(struct LegacyDataLoader* self);
+
+    bool (*onPrepareImage)(struct LegacyDataLoader* self,
+                           const DataLoaderInstallationFile addedFiles[], int addedFilesCount);
+
+    void (*onPendingReads)(struct LegacyDataLoader* self, const IncFsReadInfo pendingReads[],
+                           int pendingReadsCount);
+    void (*onPageReads)(struct LegacyDataLoader* self, const IncFsReadInfo pageReads[],
+                        int pageReadsCount);
+};
+
+__END_DECLS
+
 namespace android::dataloader {
 
 // Default DataLoader redirects everything back to Java.
-struct ManagedDataLoader : public DataLoader {
-    ManagedDataLoader(JavaVM* jvm);
+struct ManagedDataLoader : private LegacyDataLoader {
+    static LegacyDataLoader* create(JavaVM* jvm, android::dataloader::FilesystemConnectorPtr ifs,
+                                    android::dataloader::StatusListenerPtr listener,
+                                    android::dataloader::ServiceConnectorPtr service,
+                                    android::dataloader::ServiceParamsPtr params);
 
 private:
+    ManagedDataLoader(JavaVM* jvm, jobject dataLoader);
+
     // Lifecycle.
-    bool onCreate(const android::dataloader::DataLoaderParams&,
-                  android::dataloader::FilesystemConnectorPtr ifs,
-                  android::dataloader::StatusListenerPtr listener,
-                  android::dataloader::ServiceConnectorPtr service,
-                  android::dataloader::ServiceParamsPtr params) final;
-    bool onStart() final { return true; }
-    void onStop() final {}
-    void onDestroy() final;
+    void onDestroy();
 
-    bool onPrepareImage(DataLoaderInstallationFiles addedFiles) final;
-
-    void onPendingReads(PendingReads pendingReads) final {}
-    void onPageReads(PageReads pageReads) final {}
+    // Installation.
+    bool onPrepareImage(DataLoaderInstallationFiles addedFiles);
 
     JavaVM* const mJvm;
     jobject mDataLoader = nullptr;
 };
 
+struct ManagedDataLoaderFactory : public ::DataLoaderFactory {
+    ManagedDataLoaderFactory();
+};
+
 } // namespace android::dataloader
diff --git a/libdataloader/include/dataloader.h b/libdataloader/include/dataloader.h
index a46167d..2cfd9ff 100644
--- a/libdataloader/include/dataloader.h
+++ b/libdataloader/include/dataloader.h
@@ -36,7 +36,9 @@
 struct StatusListener;
 
 using FileId = IncFsFileId;
+using Uid = IncFsUid;
 using ReadInfo = IncFsReadInfo;
+using ReadInfoWithUid = IncFsReadInfoWithUid;
 using DataBlock = IncFsDataBlock;
 
 using FilesystemConnectorPtr = FilesystemConnector*;
@@ -47,7 +49,9 @@
 using DataLoaderPtr = std::unique_ptr<DataLoader>;
 using DataLoaderInstallationFiles = Span<const ::DataLoaderInstallationFile>;
 using PendingReads = Span<const ReadInfo>;
+using PendingReadsWithUid = Span<const ReadInfoWithUid>;
 using PageReads = Span<const ReadInfo>;
+using PageReadsWithUid = Span<const ReadInfoWithUid>;
 using RawMetadata = std::vector<char>;
 using DataBlocks = Span<const DataBlock>;
 
@@ -59,6 +63,9 @@
 
     virtual ~DataLoader() {}
 
+    // Bitmask of supported features.
+    virtual DataLoaderFeatures getFeatures() const = 0;
+
     // Lifecycle.
     virtual bool onCreate(const DataLoaderParams&, FilesystemConnectorPtr, StatusListenerPtr,
                           ServiceConnectorPtr, ServiceParamsPtr) = 0;
@@ -72,6 +79,9 @@
     // IFS callbacks.
     virtual void onPendingReads(PendingReads pendingReads) = 0;
     virtual void onPageReads(PageReads pageReads) = 0;
+
+    virtual void onPendingReadsWithUid(PageReadsWithUid pendingReads) = 0;
+    virtual void onPageReadsWithUid(PendingReadsWithUid pageReads) = 0;
 };
 
 struct DataLoaderParams {
diff --git a/libdataloader/include/dataloader_inline.h b/libdataloader/include/dataloader_inline.h
index 26da530..10a5c46 100644
--- a/libdataloader/include/dataloader_inline.h
+++ b/libdataloader/include/dataloader_inline.h
@@ -22,6 +22,9 @@
 
 struct DataLoaderImpl : public ::DataLoader {
     DataLoaderImpl(DataLoaderPtr&& dataLoader) : mDataLoader(std::move(dataLoader)) {
+        getFeatures = [](DataLoader* self) -> DataLoaderFeatures {
+            return static_cast<DataLoaderImpl*>(self)->mDataLoader->getFeatures();
+        };
         onStart = [](DataLoader* self) -> bool {
             return static_cast<DataLoaderImpl*>(self)->mDataLoader->onStart();
         };
@@ -47,6 +50,16 @@
             return static_cast<DataLoaderImpl*>(self)->mDataLoader->onPageReads(
                     PageReads(pageReads, pageReadsCount));
         };
+        onPendingReadsWithUid = [](DataLoader* self, const IncFsReadInfoWithUid pendingReads[],
+                                   int pendingReadsCount) {
+            return static_cast<DataLoaderImpl*>(self)->mDataLoader->onPendingReadsWithUid(
+                    PendingReadsWithUid(pendingReads, pendingReadsCount));
+        };
+        onPageReadsWithUid = [](DataLoader* self, const IncFsReadInfoWithUid pageReads[],
+                                int pageReadsCount) {
+            return static_cast<DataLoaderImpl*>(self)->mDataLoader->onPageReadsWithUid(
+                    PageReadsWithUid(pageReads, pageReadsCount));
+        };
     }
 
 private:
@@ -99,7 +112,7 @@
 } // namespace details
 
 inline void DataLoader::initialize(DataLoader::Factory&& factory) {
-    DataLoader_Initialize(new details::DataLoaderFactoryImpl(std::move(factory)));
+    DataLoader_Initialize_WithFeatures(new details::DataLoaderFactoryImpl(std::move(factory)));
 }
 
 inline DataLoaderParams::DataLoaderParams(DataLoaderType type, std::string&& packageName,
diff --git a/libdataloader/include/dataloader_ndk.h b/libdataloader/include/dataloader_ndk.h
index 67013ec..14dc3df 100644
--- a/libdataloader/include/dataloader_ndk.h
+++ b/libdataloader/include/dataloader_ndk.h
@@ -24,12 +24,9 @@
 
 #define DATALOADER_LIBRARY_NAME "libdataloader.so"
 
-// Keep in sync with IDataLoaderStatusListener.aidl
 typedef enum {
+    DATA_LOADER_UNAVAILABLE = 7,
     DATA_LOADER_UNRECOVERABLE = 8,
-
-    DATA_LOADER_FIRST_STATUS = DATA_LOADER_UNRECOVERABLE,
-    DATA_LOADER_LAST_STATUS = DATA_LOADER_UNRECOVERABLE,
 } DataLoaderStatus;
 
 typedef enum {
@@ -44,6 +41,11 @@
     DATA_LOADER_LOCATION_MEDIA_DATA = 2,
 } DataLoaderLocation;
 
+typedef enum {
+    DATA_LOADER_FEATURE_NONE = 0,
+    DATA_LOADER_FEATURE_UID = 1 << 0,
+} DataLoaderFeatures;
+
 struct DataLoaderParams {
     int type;
     const char* packageName;
@@ -81,6 +83,7 @@
 typedef jobject DataLoaderServiceParamsPtr;
 
 struct DataLoader {
+    // DataLoader v1.
     bool (*onStart)(struct DataLoader* self);
     void (*onStop)(struct DataLoader* self);
     void (*onDestroy)(struct DataLoader* self);
@@ -92,6 +95,15 @@
                            int pendingReadsCount);
     void (*onPageReads)(struct DataLoader* self, const IncFsReadInfo pageReads[],
                         int pageReadsCount);
+
+    // DataLoader v2, with features.
+    // Use DataLoader_Initialize_WithFeatures to set a factory for v2 DataLoader.
+    DataLoaderFeatures (*getFeatures)(struct DataLoader* self);
+
+    void (*onPendingReadsWithUid)(struct DataLoader* self,
+                                  const IncFsReadInfoWithUid pendingReads[], int pendingReadsCount);
+    void (*onPageReadsWithUid)(struct DataLoader* self, const IncFsReadInfoWithUid pageReads[],
+                               int pageReadsCount);
 };
 
 struct DataLoaderFactory {
@@ -101,6 +113,7 @@
                                    DataLoaderServiceParamsPtr);
 };
 void DataLoader_Initialize(struct DataLoaderFactory*);
+void DataLoader_Initialize_WithFeatures(struct DataLoaderFactory*);
 
 void DataLoader_FilesystemConnector_writeData(DataLoaderFilesystemConnectorPtr, jstring name,
                                               jlong offsetBytes, jlong lengthBytes,