1use std::os::unix::io::{AsRawFd, FromRawFd, OwnedFd};
2use std::path::{Path, PathBuf};
3
4use libc;
5use nix::fcntl::{OFlag, open};
6use nix::mount::MsFlags;
7use nix::sys::stat::{FileStat, Mode, fstat, umask};
8use nix::unistd::{Gid, Uid, close};
9use oci_spec::runtime::LinuxDevice;
10
11use super::utils::to_sflag;
12use crate::syscall::Syscall;
13use crate::syscall::syscall::create_syscall;
14use crate::utils::PathBufExt;
15
16#[derive(Debug, thiserror::Error)]
17pub enum DeviceError {
18 #[error("{0:?} is not a valid device path")]
19 InvalidDevicePath(std::path::PathBuf),
20 #[error("failed syscall to create device")]
21 Syscall(#[from] crate::syscall::SyscallError),
22 #[error(transparent)]
23 Nix(#[from] nix::Error),
24 #[error(transparent)]
25 Other(Box<dyn std::error::Error + Send + Sync>),
26 #[error("{0}")]
27 Custom(String),
28}
29
30type Result<T> = std::result::Result<T, DeviceError>;
31
32pub(crate) fn open_device_fd(dev_path: &Path) -> nix::Result<(OwnedFd, FileStat)> {
33 let fd = open(
34 dev_path,
35 OFlag::O_PATH | OFlag::O_CLOEXEC,
36 Mode::from_bits_truncate(0o000),
37 )?;
38 let owned = unsafe { OwnedFd::from_raw_fd(fd) };
39 let stat = fstat(owned.as_raw_fd())?;
40 Ok((owned, stat))
41}
42
43pub(crate) fn verify_dev_null(stat: &FileStat) -> Result<()> {
44 if stat.st_mode & libc::S_IFMT != libc::S_IFCHR {
45 return Err(DeviceError::Custom(
46 "device is not a character device".to_string(),
47 ));
48 }
49
50 let actual_major = libc::major(stat.st_rdev) as i64;
51 let actual_minor = libc::minor(stat.st_rdev) as i64;
52 if actual_major != 1 || actual_minor != 3 {
53 return Err(DeviceError::Custom(format!(
54 "device dev null major/minor mismatch: expected 1/3, actual {}/{}",
55 actual_major, actual_minor
56 )));
57 }
58 Ok(())
59}
60
61pub struct Device {
62 syscall: Box<dyn Syscall>,
63}
64
65impl Default for Device {
66 fn default() -> Self {
67 Self::new()
68 }
69}
70
71impl Device {
72 pub fn new() -> Device {
73 Device {
74 syscall: create_syscall(),
75 }
76 }
77
78 pub fn new_with_syscall(syscall: Box<dyn Syscall>) -> Device {
79 Device { syscall }
80 }
81
82 pub fn create_devices<'a, I>(&self, rootfs: &Path, devices: I, bind: bool) -> Result<()>
83 where
84 I: IntoIterator<Item = &'a LinuxDevice>,
85 {
86 let old_mode = umask(Mode::from_bits_truncate(0o000));
87 devices
88 .into_iter()
89 .map(|dev| {
90 if !dev.path().starts_with("/dev") {
91 tracing::error!(
92 "{:?} is not a valid device path starting with /dev",
93 dev.path()
94 );
95 return Err(DeviceError::InvalidDevicePath(dev.path().to_path_buf()));
96 }
97
98 if bind {
99 self.bind_dev(rootfs, dev)
100 } else {
101 self.mknod_dev(rootfs, dev)
102 }
103 })
104 .collect::<Result<Vec<_>>>()?;
105 umask(old_mode);
106
107 Ok(())
108 }
109
110 fn bind_dev(&self, rootfs: &Path, dev: &LinuxDevice) -> Result<()> {
111 let full_container_path = create_container_dev_path(rootfs, dev)?;
112 tracing::debug!(
113 "bind_dev with full container path {:?}",
114 full_container_path
115 );
116
117 let fd = open(
118 &full_container_path,
119 OFlag::O_RDWR | OFlag::O_CREAT,
120 Mode::from_bits_truncate(0o644),
121 )
122 .inspect_err(|err| {
123 tracing::error!("failed to open bind dev {:?}: {}", full_container_path, err);
124 })?;
125 close(fd)?;
126 self.syscall
127 .mount(
128 Some(dev.path()),
129 &full_container_path,
130 Some("bind"),
131 MsFlags::MS_BIND,
132 None,
133 )
134 .map_err(|err| {
135 tracing::error!(
136 ?err,
137 path = ?full_container_path,
138 "failed to mount bind dev",
139 );
140 err
141 })?;
142
143 Ok(())
144 }
145
146 fn mknod_dev(&self, rootfs: &Path, dev: &LinuxDevice) -> Result<()> {
147 fn makedev(major: i64, minor: i64) -> u64 {
148 ((minor & 0xff)
149 | ((major & 0xfff) << 8)
150 | ((minor & !0xff) << 12)
151 | ((major & !0xfff) << 32)) as u64
152 }
153
154 let full_container_path = create_container_dev_path(rootfs, dev)?;
155
156 self.syscall
157 .mknod(
158 &full_container_path,
159 to_sflag(dev.typ()),
160 Mode::from_bits_truncate(dev.file_mode().unwrap_or(0o666)),
161 makedev(dev.major(), dev.minor()),
162 )
163 .map_err(|err| {
164 tracing::error!(
165 ?err,
166 path = ?full_container_path,
167 major = ?dev.major(),
168 minor = ?dev.minor(),
169 "failed to mknod device"
170 );
171
172 err
173 })?;
174 self.syscall
175 .chown(
176 &full_container_path,
177 dev.uid().map(Uid::from_raw),
178 dev.gid().map(Gid::from_raw),
179 )
180 .map_err(|err| {
181 tracing::error!(
182 path = ?full_container_path,
183 ?err,
184 uid = ?dev.uid(),
185 gid = ?dev.gid(),
186 "failed to chown device"
187 );
188
189 err
190 })?;
191
192 Ok(())
193 }
194}
195
196fn create_container_dev_path(rootfs: &Path, dev: &LinuxDevice) -> Result<PathBuf> {
197 let relative_dev_path = dev.path().as_relative().map_err(|err| {
198 tracing::error!(
199 "failed to convert {:?} to relative path: {}",
200 dev.path(),
201 err
202 );
203 DeviceError::Other(err.into())
204 })?;
205 let full_container_path = safe_path::scoped_join(rootfs, relative_dev_path).map_err(|err| {
206 tracing::error!("failed to join {rootfs:?} with {:?}: {err}", dev.path());
207 DeviceError::Other(err.into())
208 })?;
209 std::fs::create_dir_all(
210 full_container_path
211 .parent()
212 .unwrap_or_else(|| Path::new("")),
213 )
214 .map_err(|err| {
215 tracing::error!(
216 "failed to create parent dir of {:?}: {}",
217 full_container_path,
218 err
219 );
220 DeviceError::Other(err.into())
221 })?;
222
223 Ok(full_container_path)
224}
225
226#[cfg(test)]
227mod tests {
228 use std::path::PathBuf;
229
230 use anyhow::Result;
231 use nix::sys::stat::SFlag;
232 use nix::unistd::{Gid, Uid};
233 use oci_spec::runtime::{LinuxDeviceBuilder, LinuxDeviceType};
234
235 use super::*;
236 use crate::syscall::test::{ChownArgs, MknodArgs, TestHelperSyscall};
237
238 #[test]
239 fn test_bind_dev() -> Result<()> {
240 let tmp_dir = tempfile::tempdir()?;
241 let device = Device::new_with_syscall(Box::<TestHelperSyscall>::default());
242 assert!(
243 device
244 .bind_dev(
245 tmp_dir.path(),
246 &LinuxDeviceBuilder::default()
247 .path(PathBuf::from("/dev/null"))
248 .build()
249 .unwrap(),
250 )
251 .is_ok()
252 );
253
254 let helper = device
255 .syscall
256 .as_any()
257 .downcast_ref::<TestHelperSyscall>()
258 .unwrap();
259 let mount_args = helper.get_mount_args();
260 assert_eq!(1, mount_args.len());
261 let got = &mount_args[0];
262 assert_eq!(Some(PathBuf::from("/dev/null")), got.source);
263 assert_eq!(tmp_dir.path().join("dev").join("null"), got.target);
264 assert_eq!(MsFlags::MS_BIND, got.flags);
265 assert!(got.data.is_none());
266 Ok(())
267 }
268
269 #[test]
270 fn test_mknod_dev() -> Result<()> {
271 let tmp_dir = tempfile::tempdir()?;
272 let device = Device::new_with_syscall(Box::<TestHelperSyscall>::default());
273 assert!(
274 device
275 .mknod_dev(
276 tmp_dir.path(),
277 &LinuxDeviceBuilder::default()
278 .path(PathBuf::from("/null"))
279 .major(1)
280 .minor(3)
281 .typ(LinuxDeviceType::C)
282 .file_mode(0o644u32)
283 .uid(1000u32)
284 .gid(1000u32)
285 .build()
286 .unwrap(),
287 )
288 .is_ok()
289 );
290
291 let want_mknod = MknodArgs {
292 path: tmp_dir.path().join("null"),
293 kind: SFlag::S_IFCHR,
294 perm: Mode::S_IRUSR | Mode::S_IWUSR | Mode::S_IRGRP | Mode::S_IROTH,
295 dev: 259,
296 };
297 let got_mknod = &device
298 .syscall
299 .as_any()
300 .downcast_ref::<TestHelperSyscall>()
301 .unwrap()
302 .get_mknod_args()[0];
303 assert_eq!(want_mknod, *got_mknod);
304
305 let want_chown = ChownArgs {
306 path: tmp_dir.path().join("null"),
307 owner: Some(Uid::from_raw(1000)),
308 group: Some(Gid::from_raw(1000)),
309 };
310 let got_chown = &device
311 .syscall
312 .as_any()
313 .downcast_ref::<TestHelperSyscall>()
314 .unwrap()
315 .get_chown_args()[0];
316 assert_eq!(want_chown, *got_chown);
317
318 Ok(())
319 }
320
321 #[test]
322 fn test_create_devices() -> Result<()> {
323 let tmp_dir = tempfile::tempdir()?;
324 let device = Device::new_with_syscall(Box::<TestHelperSyscall>::default());
325
326 let devices = vec![
327 LinuxDeviceBuilder::default()
328 .path(PathBuf::from("/dev/null"))
329 .major(1)
330 .minor(3)
331 .typ(LinuxDeviceType::C)
332 .file_mode(0o644u32)
333 .uid(1000u32)
334 .gid(1000u32)
335 .build()
336 .unwrap(),
337 ];
338
339 assert!(
340 device
341 .create_devices(tmp_dir.path(), &devices, true)
342 .is_ok()
343 );
344
345 let mount_args = device
346 .syscall
347 .as_any()
348 .downcast_ref::<TestHelperSyscall>()
349 .unwrap()
350 .get_mount_args();
351 assert_eq!(1, mount_args.len());
352 let bind = &mount_args[0];
353 assert_eq!(Some(PathBuf::from("/dev/null")), bind.source);
354 assert_eq!(tmp_dir.path().join("dev/null"), bind.target);
355 assert_eq!(MsFlags::MS_BIND, bind.flags);
356 assert!(bind.data.is_none());
357
358 assert!(
359 device
360 .create_devices(tmp_dir.path(), &devices, false)
361 .is_ok()
362 );
363
364 let want = MknodArgs {
365 path: tmp_dir.path().join("dev/null"),
366 kind: SFlag::S_IFCHR,
367 perm: Mode::S_IRUSR | Mode::S_IWUSR | Mode::S_IRGRP | Mode::S_IROTH,
368 dev: 259,
369 };
370 let got = &device
371 .syscall
372 .as_any()
373 .downcast_ref::<TestHelperSyscall>()
374 .unwrap()
375 .get_mknod_args()[0];
376 assert_eq!(want, *got);
377
378 Ok(())
379 }
380}