Merge "Use new LOOP_CONFIGURE ioctl for configuring loop devices."
diff --git a/apexd/apexd_loop.cpp b/apexd/apexd_loop.cpp
index 20c3ccc..d97ab34 100644
--- a/apexd/apexd_loop.cpp
+++ b/apexd/apexd_loop.cpp
@@ -18,6 +18,8 @@
 
 #include "apexd_loop.h"
 
+#include <mutex>
+
 #include <dirent.h>
 #include <fcntl.h>
 #include <linux/fs.h>
@@ -42,6 +44,17 @@
 using android::base::StringPrintf;
 using android::base::unique_fd;
 
+#ifndef LOOP_CONFIGURE
+// These can be removed whenever we pull in the Linux v5.8 UAPI headers
+struct loop_config {
+  __u32 fd;
+  __u32 block_size;
+  struct loop_info64 info;
+  __u64 __reserved[8];
+};
+#define LOOP_CONFIGURE 0x4C0A
+#endif
+
 namespace android {
 namespace apex {
 namespace loop {
@@ -126,20 +139,25 @@
   return {};
 }
 
-Result<LoopbackDeviceUniqueFd> createLoopDevice(const std::string& target,
-                                                const int32_t imageOffset,
-                                                const size_t imageSize) {
-  unique_fd ctl_fd(open("/dev/loop-control", O_RDWR | O_CLOEXEC));
-  if (ctl_fd.get() == -1) {
-    return ErrnoError() << "Failed to open loop-control";
-  }
-
-  int num = ioctl(ctl_fd.get(), LOOP_CTL_GET_FREE);
-  if (num == -1) {
-    return ErrnoError() << "Failed LOOP_CTL_GET_FREE";
-  }
-
-  std::string device = StringPrintf("/dev/block/loop%d", num);
+Result<void> configureLoopDevice(const int device_fd, const std::string& target,
+                                 const int32_t imageOffset,
+                                 const size_t imageSize) {
+  static bool useLoopConfigure;
+  static std::once_flag onceFlag;
+  std::call_once(onceFlag, [&]() {
+    // LOOP_CONFIGURE is a new ioctl in Linux 5.8 (and backported in Android
+    // common) that allows atomically configuring a loop device. It is a lot
+    // faster than the traditional LOOP_SET_FD/LOOP_SET_STATUS64 combo, but
+    // it may not be available on updating devices, so try once before
+    // deciding.
+    struct loop_config config;
+    memset(&config, 0, sizeof(config));
+    config.fd = -1;
+    if (ioctl(device_fd, LOOP_CONFIGURE, &config) == -1 && errno == EBADF) {
+      // If the IOCTL exists, it will fail with EBADF for the -1 fd
+      useLoopConfigure = true;
+    }
+  });
 
   /*
    * Using O_DIRECT will tell the kernel that we want to use Direct I/O
@@ -165,6 +183,81 @@
       return ErrnoError() << "Failed to open " << target;
     }
   }
+
+  struct loop_info64 li;
+  memset(&li, 0, sizeof(li));
+  strlcpy((char*)li.lo_crypt_name, kApexLoopIdPrefix, LO_NAME_SIZE);
+  li.lo_offset = imageOffset;
+  li.lo_sizelimit = imageSize;
+
+  if (useLoopConfigure) {
+    struct loop_config config;
+    memset(&config, 0, sizeof(config));
+    li.lo_flags |= LO_FLAGS_DIRECT_IO;
+    config.fd = target_fd.get();
+    config.info = li;
+    config.block_size = 4096;
+
+    if (ioctl(device_fd, LOOP_CONFIGURE, &config) == -1) {
+      return ErrnoError() << "Failed to LOOP_CONFIGURE";
+    }
+
+    return {};
+  } else {
+    if (ioctl(device_fd, LOOP_SET_FD, target_fd.get()) == -1) {
+      return ErrnoError() << "Failed to LOOP_SET_FD";
+    }
+
+    if (ioctl(device_fd, LOOP_SET_STATUS64, &li) == -1) {
+      return ErrnoError() << "Failed to LOOP_SET_STATUS64";
+    }
+
+    if (ioctl(device_fd, BLKFLSBUF, 0) == -1) {
+      // This works around a kernel bug where the following happens.
+      // 1) The device runs with a value of loop.max_part > 0
+      // 2) As part of LOOP_SET_FD above, we do a partition scan, which loads
+      //    the first 2 pages of the underlying file into the buffer cache
+      // 3) When we then change the offset with LOOP_SET_STATUS64, those pages
+      //    are not invalidated from the cache.
+      // 4) When we try to mount an ext4 filesystem on the loop device, the ext4
+      //    code will try to find a superblock by reading 4k at offset 0; but,
+      //    because we still have the old pages at offset 0 lying in the cache,
+      //    those pages will be returned directly. However, those pages contain
+      //    the data at offset 0 in the underlying file, not at the offset that
+      //    we configured
+      // 5) the ext4 driver fails to find a superblock in the (wrong) data, and
+      //    fails to mount the filesystem.
+      //
+      // To work around this, explicitly flush the block device, which will
+      // flush the buffer cache and make sure we actually read the data at the
+      // correct offset.
+      return ErrnoError() << "Failed to flush buffers on the loop device";
+    }
+
+    // Direct-IO requires the loop device to have the same block size as the
+    // underlying filesystem.
+    if (ioctl(device_fd, LOOP_SET_BLOCK_SIZE, 4096) == -1) {
+      PLOG(WARNING) << "Failed to LOOP_SET_BLOCK_SIZE";
+    }
+  }
+  return {};
+}
+
+Result<LoopbackDeviceUniqueFd> createLoopDevice(const std::string& target,
+                                                const int32_t imageOffset,
+                                                const size_t imageSize) {
+  unique_fd ctl_fd(open("/dev/loop-control", O_RDWR | O_CLOEXEC));
+  if (ctl_fd.get() == -1) {
+    return ErrnoError() << "Failed to open loop-control";
+  }
+
+  int num = ioctl(ctl_fd.get(), LOOP_CTL_GET_FREE);
+  if (num == -1) {
+    return ErrnoError() << "Failed LOOP_CTL_GET_FREE";
+  }
+
+  std::string device = StringPrintf("/dev/block/loop%d", num);
+
   LoopbackDeviceUniqueFd device_fd;
   {
     // See comment on kLoopDeviceRetryAttempts.
@@ -185,45 +278,10 @@
     CHECK_NE(device_fd.get(), -1);
   }
 
-  if (ioctl(device_fd.get(), LOOP_SET_FD, target_fd.get()) == -1) {
-    return ErrnoError() << "Failed to LOOP_SET_FD";
-  }
-
-  struct loop_info64 li;
-  memset(&li, 0, sizeof(li));
-  strlcpy((char*)li.lo_crypt_name, kApexLoopIdPrefix, LO_NAME_SIZE);
-  li.lo_offset = imageOffset;
-  li.lo_sizelimit = imageSize;
-  if (ioctl(device_fd.get(), LOOP_SET_STATUS64, &li) == -1) {
-    return ErrnoError() << "Failed to LOOP_SET_STATUS64";
-  }
-
-  if (ioctl(device_fd.get(), BLKFLSBUF, 0) == -1) {
-    // This works around a kernel bug where the following happens.
-    // 1) The device runs with a value of loop.max_part > 0
-    // 2) As part of LOOP_SET_FD above, we do a partition scan, which loads
-    //    the first 2 pages of the underlying file into the buffer cache
-    // 3) When we then change the offset with LOOP_SET_STATUS64, those pages
-    //    are not invalidated from the cache.
-    // 4) When we try to mount an ext4 filesystem on the loop device, the ext4
-    //    code will try to find a superblock by reading 4k at offset 0; but,
-    //    because we still have the old pages at offset 0 lying in the cache,
-    //    those pages will be returned directly. However, those pages contain
-    //    the data at offset 0 in the underlying file, not at the offset that
-    //    we configured
-    // 5) the ext4 driver fails to find a superblock in the (wrong) data, and
-    //    fails to mount the filesystem.
-    //
-    // To work around this, explicitly flush the block device, which will flush
-    // the buffer cache and make sure we actually read the data at the correct
-    // offset.
-    return ErrnoError() << "Failed to flush buffers on the loop device";
-  }
-
-  // Direct-IO requires the loop device to have the same block size as the
-  // underlying filesystem.
-  if (ioctl(device_fd.get(), LOOP_SET_BLOCK_SIZE, 4096) == -1) {
-    PLOG(WARNING) << "Failed to LOOP_SET_BLOCK_SIZE";
+  Result<void> configureStatus =
+      configureLoopDevice(device_fd.get(), target, imageOffset, imageSize);
+  if (!configureStatus.ok()) {
+    return configureStatus.error();
   }
 
   Result<void> readAheadStatus = configureReadAhead(device);