Skip to main content

libcontainer/rootfs/
device.rs

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}