Skip to main content

container_device_interface/
container_edits_unix.rs

1use std::{
2    fmt,
3    io::{Error, ErrorKind},
4    os::unix::fs::{FileTypeExt, MetadataExt},
5    path::Path,
6};
7
8use anyhow::Result;
9
10pub enum DeviceType {
11    Block,
12    Char,
13    Fifo,
14}
15
16impl fmt::Display for DeviceType {
17    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
18        let s = match self {
19            DeviceType::Block => "b",
20            DeviceType::Char => "c",
21            DeviceType::Fifo => "p",
22        };
23        write!(f, "{}", s)
24    }
25}
26
27// deviceInfoFromPath takes the path to a device and returns its type, major and minor device numbers.
28// It was adapted from https://github.com/opencontainers/runc/blob/v1.1.9/libcontainer/devices/device_unix.go#L30-L69
29pub fn device_info_from_path<P: AsRef<Path>>(path: P) -> Result<(String, i64, i64)> {
30    let metadata = std::fs::metadata(path)?;
31    let file_type = metadata.file_type();
32
33    let (dev_type, major, minor) = if file_type.is_block_device() {
34        (
35            DeviceType::Block.to_string(),
36            libc::major(metadata.rdev()),
37            libc::minor(metadata.rdev()),
38        )
39    } else if file_type.is_char_device() {
40        (
41            DeviceType::Char.to_string(),
42            libc::major(metadata.rdev()),
43            libc::minor(metadata.rdev()),
44        )
45    } else if file_type.is_fifo() {
46        (DeviceType::Fifo.to_string(), 0, 0)
47    } else {
48        return Err(Error::new(ErrorKind::InvalidInput, "It's not a device node").into());
49    };
50
51    Ok((dev_type, major.into(), minor.into()))
52}
53
54#[cfg(test)]
55mod tests {
56    use super::*;
57
58    use std::ffi::CString;
59
60    use anyhow::Result;
61    use nix::libc::{self, dev_t, mknodat, mode_t, S_IFBLK, S_IFCHR, S_IFIFO};
62    use tempfile::TempDir;
63
64    use crate::{container_edits::DeviceNode, specs::config::DeviceNode as CDIDeviceNode};
65
66    fn is_root() -> bool {
67        unsafe { libc::geteuid() == 0 }
68    }
69
70    fn create_device(path: &str, mode: mode_t, dev: u64, dev_type: &str) -> Result<()> {
71        let path_c = CString::new(path)?;
72
73        // Set the appropriate mode for block or char or fifo device
74        let mode = match dev_type {
75            "b" => mode | S_IFBLK,
76            "c" => mode | S_IFCHR,
77            "p" => mode | S_IFIFO,
78            _ => 0,
79        };
80
81        // Create the device
82        let res = unsafe { mknodat(libc::AT_FDCWD, path_c.as_ptr(), mode, dev as dev_t) };
83        if res < 0 {
84            println!("create device with path: {:?} failed", path);
85            return Err(nix::Error::last().into());
86        }
87
88        println!("create device with path: {:?} successfully", path);
89        Ok(())
90    }
91
92    #[test]
93    fn test_fill_missing_info_block_device() {
94        if !is_root() {
95            println!("INFO: skipping, needs root");
96            return;
97        }
98
99        let temp_dir = TempDir::new().unwrap();
100        let block_device_path = temp_dir.path().join("block_device").display().to_string();
101
102        // Create a block device
103        let res = create_device(&block_device_path, 0o666, 0x0101, "b");
104        assert!(res.is_ok(), "Failed to create block device: {:?}", res);
105
106        let mut dev_node = DeviceNode {
107            node: CDIDeviceNode {
108                path: block_device_path,
109                ..Default::default()
110            },
111        };
112
113        assert!(dev_node.fill_missing_info().is_ok());
114
115        assert_eq!(dev_node.node.r#type, Some(DeviceType::Block.to_string()));
116        assert_eq!(dev_node.node.major, Some(1));
117        assert_eq!(dev_node.node.minor, Some(1));
118    }
119
120    #[test]
121    fn test_fill_missing_info_char_device() {
122        if !is_root() {
123            println!("INFO: skipping, needs root");
124            return;
125        }
126
127        let temp_dir = TempDir::new().unwrap();
128        let char_device_path = temp_dir.path().join("char_device").display().to_string();
129
130        // Create a character device
131        let res = create_device(&char_device_path, 0o666, 0x0202, "c");
132        assert!(res.is_ok(), "Failed to create char device: {:?}", res);
133
134        let mut dev_node = DeviceNode {
135            node: CDIDeviceNode {
136                path: char_device_path,
137                ..Default::default()
138            },
139        };
140
141        assert!(dev_node.fill_missing_info().is_ok());
142
143        assert_eq!(dev_node.node.r#type, Some(DeviceType::Char.to_string()));
144        assert_eq!(dev_node.node.major, Some(2));
145        assert_eq!(dev_node.node.minor, Some(2));
146    }
147
148    #[test]
149    fn test_fill_missing_info_block_device_large_major_minor() {
150        if !is_root() {
151            println!("INFO: skipping, needs root");
152            return;
153        }
154
155        let temp_dir = TempDir::new().unwrap();
156        let block_device_path = temp_dir
157            .path()
158            .join("block_device_large")
159            .display()
160            .to_string();
161
162        let dev = libc::makedev(259, 513) as u64;
163        let res = create_device(&block_device_path, 0o666, dev, "b");
164        assert!(res.is_ok(), "Failed to create block device: {:?}", res);
165
166        let mut dev_node = DeviceNode {
167            node: CDIDeviceNode {
168                path: block_device_path,
169                ..Default::default()
170            },
171        };
172
173        assert!(dev_node.fill_missing_info().is_ok());
174
175        assert_eq!(dev_node.node.r#type, Some(DeviceType::Block.to_string()));
176        assert_eq!(dev_node.node.major, Some(259));
177        assert_eq!(dev_node.node.minor, Some(513));
178    }
179
180    #[test]
181    fn test_fill_missing_info_fifo() {
182        if !is_root() {
183            println!("INFO: skipping which needs root");
184            return;
185        }
186
187        let temp_dir = TempDir::new().unwrap();
188        let fifo_device_path = temp_dir.path().join("fifo_device").display().to_string();
189
190        // Create a character device
191        let res = create_device(&fifo_device_path, 0o666, 0x0, "p");
192        assert!(res.is_ok(), "Failed to create fifo device: {:?}", res);
193
194        let mut dev_node = DeviceNode {
195            node: CDIDeviceNode {
196                path: fifo_device_path,
197                ..Default::default()
198            },
199        };
200
201        dev_node.fill_missing_info().unwrap();
202
203        assert_eq!(dev_node.node.r#type, Some(DeviceType::Fifo.to_string()));
204        assert_eq!(dev_node.node.major, None);
205        assert_eq!(dev_node.node.minor, None);
206    }
207
208    // No root needed: /dev/null exists everywhere as char 1:3.
209    #[test]
210    #[cfg_attr(
211        miri,
212        ignore = "miri's stat shim does not populate rdev; /dev/null major/minor read as 0"
213    )]
214    fn device_info_from_char_device_without_root() {
215        let (typ, major, minor) = device_info_from_path("/dev/null").unwrap();
216        assert_eq!(typ, "c");
217        assert_eq!((major, minor), (1, 3));
218    }
219
220    // No root needed: mkfifo is an unprivileged syscall.
221    #[test]
222    #[cfg_attr(miri, ignore = "mkfifo is FFI miri cannot emulate")]
223    fn device_info_from_fifo_without_root() {
224        let temp_dir = TempDir::new().unwrap();
225        let fifo = temp_dir.path().join("fifo");
226        let path_c = CString::new(fifo.to_str().unwrap()).unwrap();
227        assert_eq!(unsafe { libc::mkfifo(path_c.as_ptr(), 0o600) }, 0);
228
229        let (typ, major, minor) = device_info_from_path(&fifo).unwrap();
230        assert_eq!(typ, "p");
231        assert_eq!((major, minor), (0, 0));
232    }
233
234    #[test]
235    fn device_info_rejects_regular_files() {
236        let temp_dir = TempDir::new().unwrap();
237        let file = temp_dir.path().join("plain");
238        std::fs::File::create(&file).unwrap();
239        let err = device_info_from_path(&file).unwrap_err();
240        assert!(err.to_string().contains("not a device node"));
241    }
242
243    #[test]
244    fn test_fill_missing_info_regular_file() {
245        let temp_dir = TempDir::new().unwrap();
246        let file_path = temp_dir.path().join("regular_file");
247        std::fs::File::create(&file_path).unwrap();
248
249        let mut dev_node = DeviceNode {
250            node: CDIDeviceNode {
251                path: file_path.to_string_lossy().to_string(),
252                ..Default::default()
253            },
254        };
255
256        let result = dev_node.fill_missing_info();
257        assert!(result.is_err());
258    }
259
260    #[test]
261    fn create_device_reports_mknod_failures() {
262        if !is_root() {
263            println!("INFO: skipping, needs root");
264            return;
265        }
266        // mknod in a nonexistent directory fails: the error branch of the
267        // helper every root test depends on.
268        assert!(create_device("/nonexistent-dir/dev", 0o666, 0x0101, "b").is_err());
269    }
270}