container_device_interface/
container_edits_unix.rs1use 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
27pub 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 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 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 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 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 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 #[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 #[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 assert!(create_device("/nonexistent-dir/dev", 0o666, 0x0101, "b").is_err());
269 }
270}