xref: /aosp_15_r20/system/apex/apexd/apexd_loop.cpp (revision 33f3758387333dbd2962d7edbd98681940d895da)
1 /*
2  * Copyright (C) 2018 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 #define ATRACE_TAG ATRACE_TAG_PACKAGE_MANAGER
18 
19 #include "apexd_loop.h"
20 
21 #include <ApexProperties.sysprop.h>
22 #include <android-base/file.h>
23 #include <android-base/logging.h>
24 #include <android-base/parseint.h>
25 #include <android-base/properties.h>
26 #include <android-base/stringprintf.h>
27 #include <android-base/strings.h>
28 #include <dirent.h>
29 #include <fcntl.h>
30 #include <libdm/dm.h>
31 #include <linux/fs.h>
32 #include <linux/loop.h>
33 #include <string>
34 #include <sys/ioctl.h>
35 #include <sys/stat.h>
36 #include <sys/statfs.h>
37 #include <sys/sysmacros.h>
38 #include <sys/types.h>
39 #include <unistd.h>
40 #include <utils/Trace.h>
41 
42 #include <array>
43 #include <filesystem>
44 #include <mutex>
45 #include <string_view>
46 
47 #include "apexd_utils.h"
48 
49 using android::base::Basename;
50 using android::base::ErrnoError;
51 using android::base::Error;
52 using android::base::GetBoolProperty;
53 using android::base::ParseUint;
54 using android::base::ReadFileToString;
55 using android::base::Result;
56 using android::base::StartsWith;
57 using android::base::StringPrintf;
58 using android::base::unique_fd;
59 using android::dm::DeviceMapper;
60 
61 namespace android {
62 namespace apex {
63 namespace loop {
64 
65 static constexpr const char* kApexLoopIdPrefix = "apex:";
66 
67 // 128 kB read-ahead, which we currently use for /system as well
68 static constexpr const unsigned int kReadAheadKb = 128;
69 
MaybeCloseBad()70 void LoopbackDeviceUniqueFd::MaybeCloseBad() {
71   if (device_fd.get() != -1) {
72     // Disassociate any files.
73     if (ioctl(device_fd.get(), LOOP_CLR_FD) == -1) {
74       PLOG(ERROR) << "Unable to clear fd for loopback device";
75     }
76   }
77 }
78 
ConfigureScheduler(const std::string & device_path)79 Result<void> ConfigureScheduler(const std::string& device_path) {
80   ATRACE_NAME("ConfigureScheduler");
81   if (!StartsWith(device_path, "/dev/")) {
82     return Error() << "Invalid argument " << device_path;
83   }
84 
85   const std::string device_name = Basename(device_path);
86 
87   const std::string sysfs_path =
88       StringPrintf("/sys/block/%s/queue/scheduler", device_name.c_str());
89   unique_fd sysfs_fd(open(sysfs_path.c_str(), O_RDWR | O_CLOEXEC));
90   if (sysfs_fd.get() == -1) {
91     return ErrnoError() << "Failed to open " << sysfs_path;
92   }
93 
94   // Kernels before v4.1 only support 'noop'. Kernels [v4.1, v5.0) support
95   // 'noop' and 'none'. Kernels v5.0 and later only support 'none'.
96   static constexpr const std::array<std::string_view, 2> kNoScheduler = {
97       "none", "noop"};
98 
99   int ret = 0;
100   std::string cur_sched_str;
101   if (!ReadFileToString(sysfs_path, &cur_sched_str)) {
102     return ErrnoError() << "Failed to read " << sysfs_path;
103   }
104   cur_sched_str = android::base::Trim(cur_sched_str);
105   if (std::count(kNoScheduler.begin(), kNoScheduler.end(), cur_sched_str)) {
106     return {};
107   }
108 
109   for (const std::string_view& scheduler : kNoScheduler) {
110     ret = write(sysfs_fd.get(), scheduler.data(), scheduler.size());
111     if (ret > 0) {
112       break;
113     }
114   }
115 
116   if (ret <= 0) {
117     return ErrnoError() << "Failed to write to " << sysfs_path;
118   }
119 
120   return {};
121 }
122 
123 // Return the parent device of a partition. Converts e.g. "sda26" into "sda".
PartitionParent(const std::string & blockdev)124 static Result<std::string> PartitionParent(const std::string& blockdev) {
125   if (blockdev.find('/') != std::string::npos) {
126     return Error() << "Invalid argument " << blockdev;
127   }
128   std::error_code ec;
129   for (const auto& entry :
130        std::filesystem::directory_iterator("/sys/class/block", ec)) {
131     const std::string path = entry.path().string();
132     if (std::filesystem::exists(
133             StringPrintf("%s/%s", path.c_str(), blockdev.c_str()))) {
134       return Basename(path);
135     }
136   }
137   return blockdev;
138 }
139 
140 // Convert a major:minor pair into a block device name.
BlockdevName(dev_t dev)141 static std::string BlockdevName(dev_t dev) {
142   std::error_code ec;
143   for (const auto& entry :
144        std::filesystem::directory_iterator("/dev/block", ec)) {
145     struct stat statbuf;
146     if (stat(entry.path().string().c_str(), &statbuf) < 0) {
147       continue;
148     }
149     if (dev == statbuf.st_rdev) {
150       return Basename(entry.path().string());
151     }
152   }
153   return {};
154 }
155 
156 // For file `file_path`, retrieve the block device backing the filesystem on
157 // which the file exists and return the queue depth of the block device. The
158 // loop in this function may e.g. traverse the following hierarchy:
159 // /dev/block/dm-9 (system-verity; dm-verity)
160 // -> /dev/block/dm-1 (system_b; dm-linear)
161 // -> /dev/sda26
BlockDeviceQueueDepth(const std::string & file_path)162 static Result<uint32_t> BlockDeviceQueueDepth(const std::string& file_path) {
163   struct stat statbuf;
164   int res = stat(file_path.c_str(), &statbuf);
165   if (res < 0) {
166     return ErrnoErrorf("stat({})", file_path.c_str());
167   }
168   std::string blockdev = "/dev/block/" + BlockdevName(statbuf.st_dev);
169   LOG(VERBOSE) << file_path << " -> " << blockdev;
170   if (blockdev.empty()) {
171     return Errorf("Failed to convert {}:{} (path {})", major(statbuf.st_dev),
172                   minor(statbuf.st_dev), file_path.c_str());
173   }
174   auto& dm = DeviceMapper::Instance();
175   for (;;) {
176     std::optional<std::string> child = dm.GetParentBlockDeviceByPath(blockdev);
177     if (!child) {
178       break;
179     }
180     LOG(VERBOSE) << blockdev << " -> " << *child;
181     blockdev = *child;
182   }
183   std::optional<std::string> maybe_blockdev =
184       android::dm::ExtractBlockDeviceName(blockdev);
185   if (!maybe_blockdev) {
186     return Error() << "Failed to remove /dev/block/ prefix from " << blockdev;
187   }
188   Result<std::string> maybe_parent = PartitionParent(*maybe_blockdev);
189   if (!maybe_parent.ok()) {
190     return Error() << "Failed to determine parent of " << *maybe_blockdev;
191   }
192   blockdev = *maybe_parent;
193   LOG(VERBOSE) << "Partition parent: " << blockdev;
194   const std::string nr_tags_path =
195       StringPrintf("/sys/class/block/%s/mq/0/nr_tags", blockdev.c_str());
196   std::string nr_tags;
197   if (!ReadFileToString(nr_tags_path, &nr_tags)) {
198     return ErrnoError() << "Failed to read " << nr_tags_path;
199   }
200   nr_tags = android::base::Trim(nr_tags);
201   LOG(VERBOSE) << file_path << " is backed by /dev/" << blockdev
202                << " and that block device supports queue depth " << nr_tags;
203   return strtol(nr_tags.c_str(), NULL, 0);
204 }
205 
206 // Set 'nr_requests' of `loop_device_path` equal to the queue depth of
207 // the block device backing `file_path`.
ConfigureQueueDepth(const std::string & loop_device_path,const std::string & file_path)208 Result<void> ConfigureQueueDepth(const std::string& loop_device_path,
209                                  const std::string& file_path) {
210   ATRACE_NAME("ConfigureQueueDepth");
211   if (!StartsWith(loop_device_path, "/dev/")) {
212     return Error() << "Invalid argument " << loop_device_path;
213   }
214 
215   const std::string loop_device_name = Basename(loop_device_path);
216 
217   const std::string sysfs_path =
218       StringPrintf("/sys/block/%s/queue/nr_requests", loop_device_name.c_str());
219   std::string cur_nr_requests_str;
220   if (!ReadFileToString(sysfs_path, &cur_nr_requests_str)) {
221     return ErrnoError() << "Failed to read " << sysfs_path;
222   }
223   cur_nr_requests_str = android::base::Trim(cur_nr_requests_str);
224   uint32_t cur_nr_requests = 0;
225   if (!ParseUint(cur_nr_requests_str.c_str(), &cur_nr_requests)) {
226     return Error() << "Failed to parse " << cur_nr_requests_str;
227   }
228 
229   unique_fd sysfs_fd(open(sysfs_path.c_str(), O_RDWR | O_CLOEXEC));
230   if (sysfs_fd.get() == -1) {
231     return ErrnoErrorf("Failed to open {}", sysfs_path);
232   }
233 
234   const auto qd = BlockDeviceQueueDepth(file_path);
235   if (!qd.ok()) {
236     return qd.error();
237   }
238   if (*qd == cur_nr_requests) {
239     return {};
240   }
241   // Only report write failures if reducing the queue depth. Attempts to
242   // increase the queue depth are rejected by the kernel if no I/O scheduler
243   // is associated with the request queue.
244   if (!WriteStringToFd(StringPrintf("%u", *qd), sysfs_fd) &&
245       *qd < cur_nr_requests) {
246     return ErrnoErrorf("Failed to write {} to {}", *qd, sysfs_path);
247   }
248   return {};
249 }
250 
ConfigureReadAhead(const std::string & device_path)251 Result<void> ConfigureReadAhead(const std::string& device_path) {
252   ATRACE_NAME("ConfigureReadAhead");
253   CHECK(StartsWith(device_path, "/dev/"));
254   std::string device_name = Basename(device_path);
255 
256   std::string sysfs_device =
257       StringPrintf("/sys/block/%s/queue/read_ahead_kb", device_name.c_str());
258   unique_fd sysfs_fd(open(sysfs_device.c_str(), O_RDWR | O_CLOEXEC));
259   if (sysfs_fd.get() == -1) {
260     return ErrnoError() << "Failed to open " << sysfs_device;
261   }
262 
263   std::string readAheadKb = std::to_string(
264       android::sysprop::ApexProperties::loopback_readahead().value_or(kReadAheadKb));
265 
266   int ret = TEMP_FAILURE_RETRY(
267       write(sysfs_fd.get(), readAheadKb.c_str(), readAheadKb.length()));
268   if (ret < 0) {
269     return ErrnoError() << "Failed to write to " << sysfs_device;
270   }
271 
272   return {};
273 }
274 
PreAllocateLoopDevices(size_t num)275 Result<void> PreAllocateLoopDevices(size_t num) {
276   Result<void> loop_ready = WaitForFile("/dev/loop-control", 20s);
277   if (!loop_ready.ok()) {
278     return loop_ready;
279   }
280   unique_fd ctl_fd(
281       TEMP_FAILURE_RETRY(open("/dev/loop-control", O_RDWR | O_CLOEXEC)));
282   if (ctl_fd.get() == -1) {
283     return ErrnoError() << "Failed to open loop-control";
284   }
285 
286   int new_allocations = 0;  // for logging purpose
287 
288   // Assumption: loop device ID [0..num) is valid.
289   // This is because pre-allocation happens during bootstrap.
290   // Anyway Kernel pre-allocated loop devices
291   // as many as CONFIG_BLK_DEV_LOOP_MIN_COUNT,
292   // Within the amount of kernel-pre-allocation,
293   // LOOP_CTL_ADD will fail with EEXIST
294   for (size_t id = 0ul, cnt = 0; cnt < num; ++id) {
295     int ret = ioctl(ctl_fd.get(), LOOP_CTL_ADD, id);
296     if (ret > 0) {
297       new_allocations++;
298       cnt++;
299     } else if (errno == EEXIST) {
300       // When LOOP_CTL_ADD failed with EEXIST, it can check
301       // whether it is already in use.
302       // Otherwise, the loop devices pre-allocated by the kernel can be used.
303       std::string loop_device = StringPrintf("/sys/block/loop%zu/loop", id);
304       if (access(loop_device.c_str(), F_OK) == 0) {
305         LOG(WARNING) << "Loop device " << id << " already in use";
306       } else {
307         cnt++;
308       }
309     } else {
310       return ErrnoError() << "Failed LOOP_CTL_ADD id = " << id;
311     }
312   }
313 
314   // Don't wait until the dev nodes are actually created, which
315   // will delay the boot. By simply returing here, the creation of the dev
316   // nodes will be done in parallel with other boot processes, and we
317   // just optimistally hope that they are all created when we actually
318   // access them for activating APEXes. If the dev nodes are not ready
319   // even then, we wait 50ms and warning message will be printed (see below
320   // CreateLoopDevice()).
321   LOG(INFO) << "Found " << (num - new_allocations)
322             << " idle loopback devices that were "
323             << "pre-allocated by kernel. Allocated " << new_allocations
324             << " more.";
325   return {};
326 }
327 
328 // This is a temporary/empty object for a loop device before the backing file is
329 // set.
330 struct EmptyLoopDevice {
331   unique_fd fd;
332   std::string name;
ToOwnedandroid::apex::loop::EmptyLoopDevice333   LoopbackDeviceUniqueFd ToOwned() { return {std::move(fd), std::move(name)}; }
334 };
335 
ConfigureLoopDevice(EmptyLoopDevice && inner,const std::string & target,const uint32_t image_offset,const size_t image_size)336 static Result<LoopbackDeviceUniqueFd> ConfigureLoopDevice(
337     EmptyLoopDevice&& inner, const std::string& target,
338     const uint32_t image_offset, const size_t image_size) {
339   static bool use_loop_configure;
340   static std::once_flag once_flag;
341   auto device_fd = inner.fd.get();
342   std::call_once(once_flag, [&]() {
343     // LOOP_CONFIGURE is a new ioctl in Linux 5.8 (and backported in Android
344     // common) that allows atomically configuring a loop device. It is a lot
345     // faster than the traditional LOOP_SET_FD/LOOP_SET_STATUS64 combo, but
346     // it may not be available on updating devices, so try once before
347     // deciding.
348     struct loop_config config;
349     memset(&config, 0, sizeof(config));
350     config.fd = -1;
351     if (ioctl(device_fd, LOOP_CONFIGURE, &config) == -1 && errno == EBADF) {
352       // If the IOCTL exists, it will fail with EBADF for the -1 fd
353       use_loop_configure = true;
354     }
355   });
356 
357   /*
358    * Using O_DIRECT will tell the kernel that we want to use Direct I/O
359    * on the underlying file, which we want to do to avoid double caching.
360    * Note that Direct I/O won't be enabled immediately, because the block
361    * size of the underlying block device may not match the default loop
362    * device block size (512); when we call LOOP_SET_BLOCK_SIZE below, the
363    * kernel driver will automatically enable Direct I/O when it sees that
364    * condition is now met.
365    */
366   bool use_buffered_io = false;
367   unique_fd target_fd(open(target.c_str(), O_RDONLY | O_CLOEXEC | O_DIRECT));
368   if (target_fd.get() == -1) {
369     struct statfs stbuf;
370     int saved_errno = errno;
371     // let's give another try with buffered I/O for EROFS and squashfs
372     if (statfs(target.c_str(), &stbuf) != 0 ||
373         (stbuf.f_type != EROFS_SUPER_MAGIC_V1 &&
374          stbuf.f_type != SQUASHFS_MAGIC &&
375          stbuf.f_type != OVERLAYFS_SUPER_MAGIC)) {
376       return Error(saved_errno) << "Failed to open " << target;
377     }
378     LOG(WARNING) << "Fallback to buffered I/O for " << target;
379     use_buffered_io = true;
380     target_fd.reset(open(target.c_str(), O_RDONLY | O_CLOEXEC));
381     if (target_fd.get() == -1) {
382       return ErrnoError() << "Failed to open " << target;
383     }
384   }
385 
386   struct loop_info64 li;
387   memset(&li, 0, sizeof(li));
388   strlcpy((char*)li.lo_crypt_name, kApexLoopIdPrefix, LO_NAME_SIZE);
389   li.lo_offset = image_offset;
390   li.lo_sizelimit = image_size;
391   // Automatically free loop device on last close.
392   li.lo_flags |= LO_FLAGS_AUTOCLEAR;
393 
394   if (use_loop_configure) {
395     struct loop_config config;
396     memset(&config, 0, sizeof(config));
397     config.fd = target_fd.get();
398     config.info = li;
399     config.block_size = 4096;
400     if (!use_buffered_io) {
401         li.lo_flags |= LO_FLAGS_DIRECT_IO;
402     }
403 
404     if (ioctl(device_fd, LOOP_CONFIGURE, &config) == -1) {
405       return ErrnoError() << "Failed to LOOP_CONFIGURE";
406     }
407 
408     return inner.ToOwned();
409   } else {
410     if (ioctl(device_fd, LOOP_SET_FD, target_fd.get()) == -1) {
411       return ErrnoError() << "Failed to LOOP_SET_FD";
412     }
413     // Now, we have a fully-owned loop device.
414     LoopbackDeviceUniqueFd loop_device = inner.ToOwned();
415 
416     if (ioctl(device_fd, LOOP_SET_STATUS64, &li) == -1) {
417       return ErrnoError() << "Failed to LOOP_SET_STATUS64";
418     }
419 
420     if (ioctl(device_fd, BLKFLSBUF, 0) == -1) {
421       // This works around a kernel bug where the following happens.
422       // 1) The device runs with a value of loop.max_part > 0
423       // 2) As part of LOOP_SET_FD above, we do a partition scan, which loads
424       //    the first 2 pages of the underlying file into the buffer cache
425       // 3) When we then change the offset with LOOP_SET_STATUS64, those pages
426       //    are not invalidated from the cache.
427       // 4) When we try to mount an ext4 filesystem on the loop device, the ext4
428       //    code will try to find a superblock by reading 4k at offset 0; but,
429       //    because we still have the old pages at offset 0 lying in the cache,
430       //    those pages will be returned directly. However, those pages contain
431       //    the data at offset 0 in the underlying file, not at the offset that
432       //    we configured
433       // 5) the ext4 driver fails to find a superblock in the (wrong) data, and
434       //    fails to mount the filesystem.
435       //
436       // To work around this, explicitly flush the block device, which will
437       // flush the buffer cache and make sure we actually read the data at the
438       // correct offset.
439       return ErrnoError() << "Failed to flush buffers on the loop device";
440     }
441 
442     // Direct-IO requires the loop device to have the same block size as the
443     // underlying filesystem.
444     if (ioctl(device_fd, LOOP_SET_BLOCK_SIZE, 4096) == -1) {
445       PLOG(WARNING) << "Failed to LOOP_SET_BLOCK_SIZE";
446     }
447     return loop_device;
448   }
449 }
450 
WaitForLoopDevice(int num)451 static Result<EmptyLoopDevice> WaitForLoopDevice(int num) {
452   std::vector<std::string> candidate_devices = {
453       StringPrintf("/dev/block/loop%d", num),
454       StringPrintf("/dev/loop%d", num),
455   };
456 
457   // apexd-bootstrap runs in parallel with ueventd to optimize boot time. In
458   // rare cases apexd would try attempt to mount an apex before ueventd created
459   // a loop device for it. To work around this we keep polling for loop device
460   // to be created until ueventd's cold boot sequence is done.
461   bool cold_boot_done = GetBoolProperty("ro.cold_boot_done", false);
462 
463   // Even though the kernel has created the loop device, we still depend on
464   // ueventd to run to actually create the device node in userspace. To solve
465   // this properly we should listen on the netlink socket for uevents, or use
466   // inotify. For now, this will have to do.
467   size_t attempts =
468       android::sysprop::ApexProperties::loop_wait_attempts().value_or(3u);
469   for (size_t i = 0; i != attempts; ++i) {
470     if (!cold_boot_done) {
471       cold_boot_done = GetBoolProperty("ro.cold_boot_done", false);
472     }
473     for (const auto& device : candidate_devices) {
474       unique_fd sysfs_fd(open(device.c_str(), O_RDWR | O_CLOEXEC));
475       if (sysfs_fd.get() != -1) {
476         return EmptyLoopDevice{std::move(sysfs_fd), std::move(device)};
477       }
478     }
479     PLOG(WARNING) << "Loopback device " << num << " not ready. Waiting 50ms...";
480     usleep(50000);
481     if (!cold_boot_done) {
482       // ueventd hasn't finished cold boot yet, keep trying.
483       i = 0;
484     }
485   }
486 
487   return Error() << "Failed to open loopback device " << num;
488 }
489 
CreateLoopDevice(const std::string & target,uint32_t image_offset,size_t image_size)490 static Result<LoopbackDeviceUniqueFd> CreateLoopDevice(
491     const std::string& target, uint32_t image_offset, size_t image_size) {
492   ATRACE_NAME("CreateLoopDevice");
493 
494   unique_fd ctl_fd(open("/dev/loop-control", O_RDWR | O_CLOEXEC));
495   if (ctl_fd.get() == -1) {
496     return ErrnoError() << "Failed to open loop-control";
497   }
498 
499   static std::mutex mtx;
500   std::lock_guard lock(mtx);
501   int num = ioctl(ctl_fd.get(), LOOP_CTL_GET_FREE);
502   if (num == -1) {
503     return ErrnoError() << "Failed LOOP_CTL_GET_FREE";
504   }
505 
506   auto loop_device = OR_RETURN(WaitForLoopDevice(num));
507   CHECK_NE(loop_device.fd.get(), -1);
508 
509   return ConfigureLoopDevice(std::move(loop_device), target, image_offset,
510                              image_size);
511 }
512 
CreateAndConfigureLoopDevice(const std::string & target,uint32_t image_offset,size_t image_size)513 Result<LoopbackDeviceUniqueFd> CreateAndConfigureLoopDevice(
514     const std::string& target, uint32_t image_offset, size_t image_size) {
515   ATRACE_NAME("CreateAndConfigureLoopDevice");
516   // Do minimal amount of work while holding a mutex. We need it because
517   // acquiring + configuring a loop device is not atomic. Ideally we should
518   // pre-acquire all the loop devices in advance, so that when we run APEX
519   // activation in-parallel, we can do it without holding any lock.
520   // Unfortunately, this will require some refactoring of how we manage loop
521   // devices, and probably some new loop-control ioctls, so for the time being
522   // we just limit the scope that requires locking.
523   android::base::Timer timer;
524   Result<LoopbackDeviceUniqueFd> loop_device;
525   while (timer.duration() < 1s) {
526     loop_device = CreateLoopDevice(target, image_offset, image_size);
527     if (loop_device.ok()) {
528       break;
529     }
530     std::this_thread::sleep_for(5ms);
531   }
532 
533   if (!loop_device.ok()) {
534     return loop_device.error();
535   }
536 
537   Result<void> sched_status = ConfigureScheduler(loop_device->name);
538   if (!sched_status.ok()) {
539     LOG(WARNING) << "Configuring I/O scheduler failed: "
540                  << sched_status.error();
541   }
542 
543   Result<void> qd_status = ConfigureQueueDepth(loop_device->name, target);
544   if (!qd_status.ok()) {
545     LOG(WARNING) << qd_status.error();
546   }
547 
548   Result<void> read_ahead_status = ConfigureReadAhead(loop_device->name);
549   if (!read_ahead_status.ok()) {
550     return read_ahead_status.error();
551   }
552 
553   return loop_device;
554 }
555 
DestroyLoopDevice(const std::string & path,const DestroyLoopFn & extra)556 void DestroyLoopDevice(const std::string& path, const DestroyLoopFn& extra) {
557   unique_fd fd(open(path.c_str(), O_RDWR | O_CLOEXEC));
558   if (fd.get() == -1) {
559     if (errno != ENOENT) {
560       PLOG(WARNING) << "Failed to open " << path;
561     }
562     return;
563   }
564 
565   struct loop_info64 li;
566   if (ioctl(fd.get(), LOOP_GET_STATUS64, &li) < 0) {
567     if (errno != ENXIO) {
568       PLOG(WARNING) << "Failed to LOOP_GET_STATUS64 " << path;
569     }
570     return;
571   }
572 
573   auto id = std::string((char*)li.lo_crypt_name);
574   if (StartsWith(id, kApexLoopIdPrefix)) {
575     extra(path, id);
576 
577     if (ioctl(fd.get(), LOOP_CLR_FD, 0) < 0) {
578       PLOG(WARNING) << "Failed to LOOP_CLR_FD " << path;
579     }
580   }
581 }
582 
583 }  // namespace loop
584 }  // namespace apex
585 }  // namespace android
586