• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2021 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 // `loopdevice` module provides `attach` and `detach` functions that are for attaching and
18 // detaching a regular file to and from a loop device. Note that
19 // `loopdev`(https://crates.io/crates/loopdev) is a public alternative to this. In-house
20 // implementation was chosen to make Android-specific changes (like the use of the new
21 // LOOP_CONFIGURE instead of the legacy LOOP_SET_FD + LOOP_SET_STATUS64 combo which is considerably
22 // slower than the former).
23 
24 mod sys;
25 
26 use crate::util::*;
27 use anyhow::{Context, Result};
28 use libc::O_DIRECT;
29 use std::fs::{File, OpenOptions};
30 use std::os::unix::fs::OpenOptionsExt;
31 use std::os::unix::io::AsRawFd;
32 use std::path::{Path, PathBuf};
33 use std::thread;
34 use std::time::{Duration, Instant};
35 use zerocopy::FromZeros;
36 
37 use crate::loopdevice::sys::*;
38 
39 // These are old-style ioctls, thus *_bad.
40 nix::ioctl_none_bad!(_loop_ctl_get_free, LOOP_CTL_GET_FREE);
41 nix::ioctl_write_ptr_bad!(_loop_configure, LOOP_CONFIGURE, loop_config);
42 nix::ioctl_none_bad!(_loop_clr_fd, LOOP_CLR_FD);
43 
loop_ctl_get_free(ctrl_file: &File) -> Result<i32>44 fn loop_ctl_get_free(ctrl_file: &File) -> Result<i32> {
45     // SAFETY: this ioctl changes the state in kernel, but not the state in this process.
46     // The returned device number is a global resource; not tied to this process. So, we don't
47     // need to keep track of it.
48     Ok(unsafe { _loop_ctl_get_free(ctrl_file.as_raw_fd()) }?)
49 }
50 
loop_configure(device_file: &File, config: &loop_config) -> Result<i32>51 fn loop_configure(device_file: &File, config: &loop_config) -> Result<i32> {
52     // SAFETY: this ioctl changes the state in kernel, but not the state in this process.
53     Ok(unsafe { _loop_configure(device_file.as_raw_fd(), config) }?)
54 }
55 
loop_clr_fd(device_file: &File) -> Result<i32>56 pub fn loop_clr_fd(device_file: &File) -> Result<i32> {
57     // SAFETY: this ioctl disassociates the loop device with `device_file`, where the FD will
58     // remain opened afterward. The association itself is kept for open FDs.
59     Ok(unsafe { _loop_clr_fd(device_file.as_raw_fd()) }?)
60 }
61 
62 /// LOOP_CONFIGURE ioctl operation flags.
63 #[derive(Default)]
64 pub struct LoopConfigOptions {
65     /// Whether to use direct I/O
66     pub direct_io: bool,
67     /// Whether the device is writable
68     pub writable: bool,
69     /// Whether to autodestruct the device on last close
70     pub autoclear: bool,
71 }
72 
73 pub struct LoopDevice {
74     /// The loop device file
75     pub file: File,
76     /// Path to the loop device
77     pub path: PathBuf,
78 }
79 
80 /// Creates a loop device and attach the given file at `path` as the backing store.
attach<P: AsRef<Path>>( path: P, offset: u64, size_limit: u64, options: &LoopConfigOptions, ) -> Result<LoopDevice>81 pub fn attach<P: AsRef<Path>>(
82     path: P,
83     offset: u64,
84     size_limit: u64,
85     options: &LoopConfigOptions,
86 ) -> Result<LoopDevice> {
87     // Attaching a file to a loop device can make a race condition; a loop device number obtained
88     // from LOOP_CTL_GET_FREE might have been used by another thread or process. In that case the
89     // subsequent LOOP_CONFIGURE ioctl returns with EBUSY. Try until it succeeds.
90     //
91     // Note that the timing parameters below are chosen rather arbitrarily. In practice (i.e.
92     // inside Microdroid) we can't experience the race condition because `apkverity` is the only
93     // user of /dev/loop-control at the moment. This loop is mostly for testing where multiple
94     // tests run concurrently.
95     const TIMEOUT: Duration = Duration::from_secs(1);
96     const INTERVAL: Duration = Duration::from_millis(10);
97 
98     let begin = Instant::now();
99     loop {
100         match try_attach(&path, offset, size_limit, options) {
101             Ok(loop_device) => return Ok(loop_device),
102             Err(e) => {
103                 if begin.elapsed() > TIMEOUT {
104                     return Err(e);
105                 }
106             }
107         };
108         thread::sleep(INTERVAL);
109     }
110 }
111 
112 #[cfg(not(target_os = "android"))]
113 const LOOP_DEV_PREFIX: &str = "/dev/loop";
114 
115 #[cfg(target_os = "android")]
116 const LOOP_DEV_PREFIX: &str = "/dev/block/loop";
117 
try_attach<P: AsRef<Path>>( path: P, offset: u64, size_limit: u64, options: &LoopConfigOptions, ) -> Result<LoopDevice>118 fn try_attach<P: AsRef<Path>>(
119     path: P,
120     offset: u64,
121     size_limit: u64,
122     options: &LoopConfigOptions,
123 ) -> Result<LoopDevice> {
124     // Get a free loop device
125     wait_for_path(LOOP_CONTROL)?;
126     let ctrl_file = OpenOptions::new()
127         .read(true)
128         .write(true)
129         .open(LOOP_CONTROL)
130         .context("Failed to open loop control")?;
131     let num = loop_ctl_get_free(&ctrl_file).context("Failed to get free loop device")?;
132 
133     // Construct the loop_info64 struct
134     let backing_file = OpenOptions::new()
135         .read(true)
136         .write(options.writable)
137         .custom_flags(if options.direct_io { O_DIRECT } else { 0 })
138         .open(&path)
139         .context(format!("failed to open {:?}", path.as_ref()))?;
140     let mut config = loop_config::new_zeroed();
141     config.fd = backing_file.as_raw_fd() as u32;
142     config.block_size = 4096;
143     config.info.lo_offset = offset;
144     config.info.lo_sizelimit = size_limit;
145 
146     if !options.writable {
147         config.info.lo_flags = Flag::LO_FLAGS_READ_ONLY;
148     }
149 
150     if options.direct_io {
151         config.info.lo_flags.insert(Flag::LO_FLAGS_DIRECT_IO);
152     }
153 
154     if options.autoclear {
155         config.info.lo_flags.insert(Flag::LO_FLAGS_AUTOCLEAR);
156     }
157 
158     // Configure the loop device to attach the backing file
159     let device_path = format!("{}{}", LOOP_DEV_PREFIX, num);
160     wait_for_path(&device_path)?;
161     let device_file = OpenOptions::new()
162         .read(true)
163         .write(true)
164         .open(&device_path)
165         .context(format!("failed to open {:?}", &device_path))?;
166     loop_configure(&device_file, &config)
167         .context(format!("Failed to configure {:?}", &device_path))?;
168 
169     Ok(LoopDevice { file: device_file, path: PathBuf::from(device_path) })
170 }
171 
172 /// Detaches backing file from the loop device `path`.
detach<P: AsRef<Path>>(path: P) -> Result<()>173 pub fn detach<P: AsRef<Path>>(path: P) -> Result<()> {
174     let device_file = OpenOptions::new().read(true).write(true).open(&path)?;
175     loop_clr_fd(&device_file)?;
176     Ok(())
177 }
178 
179 #[cfg(test)]
180 mod tests {
181     use super::*;
182     use std::fs;
183     use std::path::Path;
184 
create_empty_file(path: &Path, size: u64)185     fn create_empty_file(path: &Path, size: u64) {
186         let f = File::create(path).unwrap();
187         f.set_len(size).unwrap();
188     }
189 
is_direct_io(dev: &Path) -> bool190     fn is_direct_io(dev: &Path) -> bool {
191         let dio = Path::new("/sys/block").join(dev.file_name().unwrap()).join("loop/dio");
192         "1" == fs::read_to_string(dio).unwrap().trim()
193     }
194 
195     // kernel exposes /sys/block/loop*/ro which gives the read-only value
is_direct_io_writable(dev: &Path) -> bool196     fn is_direct_io_writable(dev: &Path) -> bool {
197         let ro = Path::new("/sys/block").join(dev.file_name().unwrap()).join("ro");
198         "0" == fs::read_to_string(ro).unwrap().trim()
199     }
200 
is_autoclear(dev: &Path) -> bool201     fn is_autoclear(dev: &Path) -> bool {
202         let autoclear =
203             Path::new("/sys/block").join(dev.file_name().unwrap()).join("loop/autoclear");
204         "1" == fs::read_to_string(autoclear).unwrap().trim()
205     }
206 
207     #[test]
attach_loop_device_with_dio()208     fn attach_loop_device_with_dio() {
209         let a_dir = tempfile::TempDir::new().unwrap();
210         let a_file = a_dir.path().join("test");
211         let a_size = 4096u64;
212         create_empty_file(&a_file, a_size);
213         let dev =
214             attach(a_file, 0, a_size, &LoopConfigOptions { direct_io: true, ..Default::default() })
215                 .unwrap()
216                 .path;
217         scopeguard::defer! {
218             detach(&dev).unwrap();
219         }
220         assert!(is_direct_io(&dev));
221     }
222 
223     #[test]
attach_loop_device_without_dio()224     fn attach_loop_device_without_dio() {
225         let a_dir = tempfile::TempDir::new().unwrap();
226         let a_file = a_dir.path().join("test");
227         let a_size = 4096u64;
228         create_empty_file(&a_file, a_size);
229         let dev = attach(a_file, 0, a_size, &LoopConfigOptions::default()).unwrap().path;
230         scopeguard::defer! {
231             detach(&dev).unwrap();
232         }
233         assert!(!is_direct_io(&dev));
234     }
235 
236     #[test]
attach_loop_device_with_dio_writable()237     fn attach_loop_device_with_dio_writable() {
238         let a_dir = tempfile::TempDir::new().unwrap();
239         let a_file = a_dir.path().join("test");
240         let a_size = 4096u64;
241         create_empty_file(&a_file, a_size);
242         let dev = attach(
243             a_file,
244             0,
245             a_size,
246             &LoopConfigOptions { direct_io: true, writable: true, ..Default::default() },
247         )
248         .unwrap()
249         .path;
250         scopeguard::defer! {
251             detach(&dev).unwrap();
252         }
253         assert!(is_direct_io(&dev));
254         assert!(is_direct_io_writable(&dev));
255     }
256 
257     #[test]
attach_loop_device_autoclear()258     fn attach_loop_device_autoclear() {
259         let a_dir = tempfile::TempDir::new().unwrap();
260         let a_file = a_dir.path().join("test");
261         let a_size = 4096u64;
262         create_empty_file(&a_file, a_size);
263         let dev =
264             attach(a_file, 0, a_size, &LoopConfigOptions { autoclear: true, ..Default::default() })
265                 .unwrap();
266 
267         assert!(is_autoclear(&dev.path));
268     }
269 }
270