Skip to main content

hermit/fs/
uhyve.rs

1use alloc::borrow::ToOwned;
2use alloc::boxed::Box;
3use alloc::ffi::CString;
4use alloc::string::String;
5use alloc::sync::Arc;
6use core::mem::MaybeUninit;
7
8use async_lock::Mutex;
9use embedded_io::{ErrorType, Read, Write};
10use memory_addresses::VirtAddr;
11use uhyve_interface::GuestPhysAddr;
12use uhyve_interface::v2::Hypercall;
13use uhyve_interface::v2::parameters::{
14	CloseParams, FstatParams, GetdentParams, GetdentResult, LseekParams, OpenParams, ReadParams,
15	StatKind, StatParams, StatResult, UnlinkParams, WriteParams,
16};
17
18use crate::arch::mm::paging::virtual_to_physical;
19use crate::env::FdtStartInfo;
20use crate::errno::Errno;
21use crate::fd::{Fd, RawFd};
22use crate::fs::{
23	self, AccessPermission, FileAttr, NodeKind, ObjectInterface, OpenOption, SeekWhence, VfsNode,
24};
25use crate::syscalls::DirentFormat;
26use crate::uhyve::uhyve_hypercall;
27use crate::{env, io};
28
29fn fstat_hypercall(fd: i32) -> io::Result<FileAttr> {
30	let mut attr = FileAttr::default();
31	let mut fstat_params = FstatParams {
32		fd,
33		attr: GuestPhysAddr::new(
34			virtual_to_physical(VirtAddr::from_ptr((&raw mut attr).cast::<FileAttr>()))
35				.unwrap()
36				.as_u64(),
37		),
38		ret: StatResult::None,
39	};
40	uhyve_hypercall(Hypercall::FileFstat(&mut fstat_params));
41	match fstat_params.ret {
42		StatResult::None => Err(Errno::Nosys),
43		StatResult::Success => Ok(attr),
44		StatResult::Error(errno) => Err(Errno::try_from(errno).unwrap()),
45	}
46}
47
48#[derive(Debug)]
49struct UhyveFileHandleInner(i32);
50
51impl UhyveFileHandleInner {
52	pub fn new(fd: RawFd) -> Self {
53		Self(fd)
54	}
55
56	fn lseek(&self, offset: isize, whence: SeekWhence) -> io::Result<isize> {
57		let mut lseek_params = LseekParams {
58			fd: self.0,
59			offset: offset.try_into().unwrap(),
60			whence: u8::from(whence).into(),
61		};
62		uhyve_hypercall(Hypercall::FileLseek(&mut lseek_params));
63		// TODO: Although we can generally assume that what Uhyve delivers should be
64		// correct for now, it might make sense to build in checks (or at least debug_assert's)
65		match lseek_params.offset {
66			offset if offset >= 0 => Ok(offset.try_into().unwrap()),
67			errno if errno < 0 => Err((errno as i32).abs().try_into().unwrap()),
68			_ => {
69				debug!("Uhyve lseek hypercall yielded a zero.");
70				Err(Errno::Inval)
71			}
72		}
73	}
74}
75
76impl ErrorType for UhyveFileHandleInner {
77	type Error = Errno;
78}
79
80impl Read for UhyveFileHandleInner {
81	fn read(&mut self, buf: &mut [u8]) -> Result<usize, Self::Error> {
82		let mut read_params = ReadParams {
83			fd: self.0,
84			buf: GuestPhysAddr::new(
85				virtual_to_physical(VirtAddr::from_ptr(buf.as_mut_ptr()))
86					.unwrap()
87					.as_u64(),
88			),
89			len: buf.len().try_into().unwrap(),
90			ret: 0i64,
91		};
92		uhyve_hypercall(Hypercall::FileRead(&mut read_params));
93		match read_params.ret {
94			ret if ret >= 0 => Ok(ret.try_into().unwrap()),
95			_ => Err((read_params.ret as i32).abs().try_into().unwrap()),
96		}
97	}
98}
99
100impl Write for UhyveFileHandleInner {
101	fn write(&mut self, buf: &[u8]) -> Result<usize, Self::Error> {
102		let mut write_params = WriteParams {
103			fd: self.0,
104			buf: GuestPhysAddr::new(
105				virtual_to_physical(VirtAddr::from_ptr(buf.as_ptr()))
106					.unwrap()
107					.as_u64(),
108			),
109			len: buf.len().try_into().unwrap(),
110			ret: 0i64,
111		};
112		// fd refers to a regular file
113		uhyve_hypercall(Hypercall::FileWrite(&mut write_params));
114		match write_params.ret {
115			// Assumption: fd is a regular file, a zero is only valid if the len
116			// (aka. "count") is also zero. Otherwise, however, we assume that something
117			// is wrong in Hermit<>Uhyve communication.
118			ret if ret > 0 || (ret == 0 && write_params.len == 0) => Ok(ret.try_into().unwrap()),
119			errno if errno < 0 => Err((errno as i32).abs().try_into().unwrap()),
120			_ => {
121				debug!("Uhyve write hypercall yielded a zero.");
122				Err(Errno::Inval)
123			}
124		}
125	}
126
127	fn flush(&mut self) -> Result<(), Self::Error> {
128		Ok(())
129	}
130}
131
132impl Drop for UhyveFileHandleInner {
133	fn drop(&mut self) {
134		let mut close_params = CloseParams { fd: self.0, ret: 0 };
135		uhyve_hypercall(Hypercall::FileClose(&mut close_params));
136		if close_params.ret != 0 {
137			let ret = close_params.ret; // circumvent packed field access
138			panic!("Can't close fd {} - return value {ret}", self.0);
139		}
140	}
141}
142
143pub struct UhyveFileHandle(Arc<Mutex<UhyveFileHandleInner>>);
144
145impl UhyveFileHandle {
146	pub fn new(fd: RawFd) -> Self {
147		Self(Arc::new(Mutex::new(UhyveFileHandleInner::new(fd))))
148	}
149}
150
151impl ObjectInterface for UhyveFileHandle {
152	async fn read(&self, buf: &mut [u8]) -> io::Result<usize> {
153		self.0.lock().await.read(buf)
154	}
155
156	async fn write(&self, buf: &[u8]) -> io::Result<usize> {
157		self.0.lock().await.write(buf)
158	}
159
160	async fn lseek(&self, offset: isize, whence: SeekWhence) -> io::Result<isize> {
161		self.0.lock().await.lseek(offset, whence)
162	}
163
164	async fn fstat(&self) -> io::Result<FileAttr> {
165		fstat_hypercall(self.0.lock().await.0)
166	}
167}
168
169impl Clone for UhyveFileHandle {
170	fn clone(&self) -> Self {
171		Self(self.0.clone())
172	}
173}
174
175pub struct UhyveDirectoryHandle {
176	/// Guest fd of the directory, mapped in Uhyve's fd layer.
177	fd: i32,
178}
179
180impl UhyveDirectoryHandle {
181	pub const fn new(fd: i32) -> Self {
182		Self { fd }
183	}
184}
185
186impl Drop for UhyveDirectoryHandle {
187	fn drop(&mut self) {
188		let mut close_params = CloseParams {
189			fd: self.fd,
190			ret: 0,
191		};
192		uhyve_hypercall(Hypercall::FileClose(&mut close_params));
193	}
194}
195
196impl ObjectInterface for UhyveDirectoryHandle {
197	async fn getdents(
198		&self,
199		buf: &mut [MaybeUninit<u8>],
200		format: DirentFormat,
201	) -> io::Result<usize> {
202		match format {
203			DirentFormat::PosixDent => unimplemented!("Uhyve has no POSIX dirent support (yet)"),
204			DirentFormat::Dirent64 => {
205				let mut read_dir_params = GetdentParams {
206					fd: self.fd,
207					buf: GuestPhysAddr::new(
208						virtual_to_physical(VirtAddr::from_ptr(buf.as_mut_ptr()))
209							.unwrap()
210							.as_u64(),
211					),
212					len: buf.len().try_into().unwrap(),
213					ret: GetdentResult::None,
214				};
215				uhyve_hypercall(Hypercall::Getdents(&mut read_dir_params));
216				match read_dir_params.ret {
217					GetdentResult::None => Err(Errno::Nosys),
218					GetdentResult::Success(len) => Ok(len.try_into().unwrap()),
219					GetdentResult::EndOfDirectory => Ok(0),
220					GetdentResult::Error(errno) => Err(Errno::try_from(errno).unwrap()),
221				}
222			}
223		}
224	}
225
226	async fn fstat(&self) -> io::Result<FileAttr> {
227		fstat_hypercall(self.fd)
228	}
229}
230
231#[derive(Debug)]
232pub(crate) struct UhyveDirectory {
233	/// The external path of this directory.
234	///
235	/// Before talking to virtio-fs, the relative path inside this directory is
236	/// adjoined with this prefix.
237	prefix: String,
238}
239
240impl UhyveDirectory {
241	pub const fn new(prefix: String) -> Self {
242		UhyveDirectory { prefix }
243	}
244
245	fn traversal_path(&self, path: &str) -> CString {
246		let prefix = self.prefix.as_str();
247		let prefix = prefix.strip_suffix("/").unwrap_or(prefix);
248		if path.is_empty() {
249			return CString::new(prefix).unwrap();
250		}
251		let path = [prefix, path].join("/");
252		CString::new(path).unwrap()
253	}
254
255	fn stat_hypercall(&self, path: &str, kind: StatKind) -> io::Result<FileAttr> {
256		let path = self.traversal_path(path);
257		let mut attr = FileAttr::default();
258		let mut stat_params = StatParams {
259			name: GuestPhysAddr::new(
260				virtual_to_physical(VirtAddr::from_ptr(path.as_ptr()))
261					.unwrap()
262					.as_u64(),
263			),
264			kind,
265			attr: GuestPhysAddr::new(
266				virtual_to_physical(VirtAddr::from_ptr((&raw mut attr).cast::<FileAttr>()))
267					.unwrap()
268					.as_u64(),
269			),
270			ret: StatResult::None,
271		};
272		uhyve_hypercall(Hypercall::FileStat(&mut stat_params));
273		match stat_params.ret {
274			StatResult::None => Err(Errno::Nosys),
275			StatResult::Success => Ok(attr),
276			StatResult::Error(errno) => Err(Errno::try_from(errno).unwrap()),
277		}
278	}
279}
280
281impl VfsNode for UhyveDirectory {
282	/// Returns the node type
283	fn get_kind(&self) -> NodeKind {
284		NodeKind::Directory
285	}
286
287	fn get_file_attributes(&self) -> io::Result<FileAttr> {
288		self.stat_hypercall("", StatKind::Stat)
289	}
290
291	fn traverse_stat(&self, path: &str) -> io::Result<FileAttr> {
292		self.stat_hypercall(path, StatKind::Stat)
293	}
294
295	fn traverse_lstat(&self, path: &str) -> io::Result<FileAttr> {
296		self.stat_hypercall(path, StatKind::LStat)
297	}
298
299	fn traverse_open(
300		&self,
301		path: &str,
302		opt: OpenOption,
303		mode: AccessPermission,
304	) -> io::Result<Arc<async_lock::RwLock<Fd>>> {
305		let path = self.traversal_path(path);
306
307		let mut open_params = OpenParams {
308			name: GuestPhysAddr::new(
309				virtual_to_physical(VirtAddr::from_ptr(path.as_ptr()))
310					.unwrap()
311					.as_u64(),
312			),
313			flags: opt.bits(),
314			mode: mode.bits() as i32,
315			ret: -1,
316		};
317		uhyve_hypercall(Hypercall::FileOpen(&mut open_params));
318		let ret = open_params.ret; // circumvent packed field access
319		match ret {
320			// Assumption: Uhyve will never return a standard stream.
321			ret if ret >= 0 => {
322				let obj = if opt.contains(OpenOption::O_DIRECTORY) {
323					UhyveDirectoryHandle::new(ret).into()
324				} else {
325					UhyveFileHandle::new(ret).into()
326				};
327				Ok(Arc::new(async_lock::RwLock::new(obj)))
328			}
329			_ => Err(ret.abs().try_into().unwrap()),
330		}
331	}
332
333	fn traverse_unlink(&self, path: &str) -> io::Result<()> {
334		let path = self.traversal_path(path);
335
336		let mut unlink_params = UnlinkParams {
337			name: GuestPhysAddr::new(
338				virtual_to_physical(VirtAddr::from_ptr(path.as_ptr()))
339					.unwrap()
340					.as_u64(),
341			),
342			ret: -1,
343		};
344		uhyve_hypercall(Hypercall::FileUnlink(&mut unlink_params));
345		let ret = unlink_params.ret; // circumvent packed field access
346		match ret {
347			0 => Ok(()),
348			_ => Err(unlink_params.ret.abs().try_into().unwrap()),
349		}
350	}
351
352	fn traverse_rmdir(&self, _path: &str) -> io::Result<()> {
353		Err(Errno::Nosys)
354	}
355
356	fn traverse_mkdir(&self, _path: &str, _mode: AccessPermission) -> io::Result<()> {
357		Err(Errno::Nosys)
358	}
359
360	/// Determines the syscall interface
361	fn get_object(&self) -> io::Result<Arc<async_lock::RwLock<Fd>>> {
362		let path = CString::new(self.prefix.clone()).unwrap();
363		let mut open_params = OpenParams {
364			name: GuestPhysAddr::new(
365				virtual_to_physical(VirtAddr::from_ptr(path.as_ptr()))
366					.unwrap()
367					.as_u64(),
368			),
369			flags: (OpenOption::O_RDONLY | OpenOption::O_DIRECTORY).bits(),
370			mode: 0,
371			ret: -1,
372		};
373		uhyve_hypercall(Hypercall::FileOpen(&mut open_params));
374		let fd = open_params.ret;
375		match fd {
376			fd if fd >= 0 => Ok(Arc::new(async_lock::RwLock::new(
377				UhyveDirectoryHandle::new(fd).into(),
378			))),
379			_ => Err(fd.abs().try_into().unwrap()),
380		}
381	}
382}
383
384pub(crate) fn init() {
385	info!("Try to initialize uhyve filesystem");
386
387	let mount_str = env::start_info().fdt().and_then(|fdt| {
388		fdt.find_node("/uhyve,mounts")
389			.and_then(|node| node.property("mounts"))
390			.and_then(|property| property.as_str())
391	});
392
393	let Some(mount_str) = mount_str else {
394		// No FDT -> Uhyve legacy mounting (to /root)
395		let mount_point = hermit_var_or!("UHYVE_MOUNT", "/root").to_owned();
396		info!("Mounting uhyve filesystem at {mount_point}");
397		fs::FILESYSTEM
398			.get()
399			.unwrap()
400			.mount(
401				&mount_point,
402				Box::new(UhyveDirectory::new(mount_point.clone())),
403			)
404			.expect("Mount failed. Duplicate mount_point?");
405		return;
406	};
407
408	assert_ne!(mount_str.len(), 0, "Invalid /uhyve,mounts node in FDT");
409	for mount_point in mount_str.split('\0') {
410		info!("Mounting uhyve filesystem at {mount_point}");
411
412		let obj = Box::new(UhyveDirectory::new(mount_point.to_owned()));
413		let Err(errno) = fs::FILESYSTEM.get().unwrap().mount(mount_point, obj) else {
414			continue;
415		};
416
417		assert_eq!(errno, Errno::Badf);
418		debug!("Mounting of {mount_point} failed with {errno:?}. Creating missing parent folders");
419		let (parent_path, _file_name) = mount_point.rsplit_once('/').unwrap();
420		create_dir_recursive(parent_path, AccessPermission::S_IRWXU).unwrap();
421
422		let obj = Box::new(UhyveDirectory::new(mount_point.to_owned()));
423		fs::FILESYSTEM
424			.get()
425			.unwrap()
426			.mount(mount_point, obj)
427			.unwrap();
428	}
429}
430
431/// Creates a directory and creates all missing parent directories as well.
432fn create_dir_recursive(path: &str, mode: AccessPermission) -> io::Result<()> {
433	trace!("create_dir_recursive: {path}");
434	fs::create_dir(path, mode).or_else(|errno| {
435		if errno != Errno::Badf {
436			return Err(errno);
437		}
438		let (parent_path, _file_name) = path.rsplit_once('/').unwrap();
439		create_dir_recursive(parent_path, mode)?;
440		fs::create_dir(path, mode)
441	})
442}