Skip to main content

hermit/fs/
mod.rs

1pub(crate) mod dev_directory;
2pub(crate) mod mem;
3#[cfg(feature = "uhyve")]
4pub(crate) mod uhyve;
5#[cfg(feature = "virtio-fs")]
6pub(crate) mod virtio_fs;
7
8use alloc::borrow::ToOwned;
9use alloc::boxed::Box;
10use alloc::string::String;
11use alloc::sync::Arc;
12use alloc::vec::Vec;
13use core::fmt;
14use core::mem::MaybeUninit;
15use core::ops::BitAnd;
16
17use embedded_io::{Read, Write};
18use hermit_sync::{InterruptSpinMutex, OnceCell};
19use mem::MemDirectory;
20use num_enum::{IntoPrimitive, TryFromPrimitive};
21
22#[cfg(any(feature = "uhyve", feature = "initramfs"))]
23use crate::env::{self, StartInfo as _};
24use crate::errno::Errno;
25use crate::executor::block_on;
26use crate::fd::{AccessPermission, Fd, ObjectInterface, OpenOption, insert_object, remove_object};
27use crate::io;
28use crate::syscalls::DirentFormat;
29use crate::time::{SystemTime, timespec};
30
31static FILESYSTEM: OnceCell<Filesystem> = OnceCell::new();
32
33static WORKING_DIRECTORY: InterruptSpinMutex<Option<String>> = InterruptSpinMutex::new(None);
34
35static UMASK: InterruptSpinMutex<AccessPermission> =
36	InterruptSpinMutex::new(AccessPermission::from_bits_retain(0o777));
37
38#[derive(Debug, Clone)]
39pub struct DirectoryEntry {
40	pub name: String,
41}
42
43impl DirectoryEntry {
44	pub fn new(name: String) -> Self {
45		Self { name }
46	}
47}
48
49/// Type of the VNode
50#[derive(Copy, Clone, Debug, PartialEq, Eq)]
51pub(crate) enum NodeKind {
52	/// Node represent a file
53	File,
54	/// Node represent a directory
55	Directory,
56}
57
58/// VfsNode represents an internal node of the ramdisk.
59pub(crate) trait VfsNode: Send + Sync + fmt::Debug {
60	/// Determines the current node type
61	fn get_kind(&self) -> NodeKind;
62
63	/// Determines the current file attribute
64	fn get_file_attributes(&self) -> io::Result<FileAttr> {
65		Err(Errno::Nosys)
66	}
67
68	/// Determines the syscall interface
69	fn get_object(&self) -> io::Result<Arc<async_lock::RwLock<Fd>>> {
70		Err(Errno::Nosys)
71	}
72
73	/// Creates a new directory node
74	fn traverse_mkdir(&self, _path: &str, _mode: AccessPermission) -> io::Result<()> {
75		Err(Errno::Nosys)
76	}
77
78	/// Deletes a directory node
79	fn traverse_rmdir(&self, _path: &str) -> io::Result<()> {
80		Err(Errno::Nosys)
81	}
82
83	/// Removes the specified file
84	fn traverse_unlink(&self, _path: &str) -> io::Result<()> {
85		Err(Errno::Nosys)
86	}
87
88	/// Opens a directory
89	fn traverse_readdir(&self, _path: &str) -> io::Result<Vec<DirectoryEntry>> {
90		Err(Errno::Nosys)
91	}
92
93	/// Gets file status
94	fn traverse_lstat(&self, _path: &str) -> io::Result<FileAttr> {
95		Err(Errno::Nosys)
96	}
97
98	/// Gets file status
99	fn traverse_stat(&self, _path: &str) -> io::Result<FileAttr> {
100		Err(Errno::Nosys)
101	}
102
103	/// Mounts a file system
104	fn traverse_mount(&self, _path: &str, _obj: Box<dyn VfsNode>) -> io::Result<()> {
105		Err(Errno::Nosys)
106	}
107
108	/// Opens a file
109	fn traverse_open(
110		&self,
111		_path: &str,
112		_option: OpenOption,
113		_mode: AccessPermission,
114	) -> io::Result<Arc<async_lock::RwLock<Fd>>> {
115		Err(Errno::Nosys)
116	}
117
118	/// Creates a read-only file
119	fn traverse_create_file(
120		&self,
121		_path: &str,
122		_data: &'static [u8],
123		_mode: AccessPermission,
124	) -> io::Result<()> {
125		Err(Errno::Nosys)
126	}
127}
128
129pub(crate) struct DirectoryReader {
130	entries: Vec<DirectoryEntry>,
131	read_idx: async_lock::Mutex<usize>,
132}
133
134impl DirectoryReader {
135	pub fn new(entries: Vec<DirectoryEntry>) -> Self {
136		Self {
137			entries,
138			read_idx: async_lock::Mutex::new(0),
139		}
140	}
141}
142
143impl ObjectInterface for DirectoryReader {
144	async fn getdents(
145		&self,
146		buf: &mut [MaybeUninit<u8>],
147		format: DirentFormat,
148	) -> io::Result<usize> {
149		let mut buf_offset: usize = 0;
150		let mut read_idx = self.read_idx.lock().await;
151		for entry in self.entries.iter().skip(*read_idx) {
152			let Some(next_dirent) =
153				format.write_entry(buf, buf_offset, 1, FileType::Unknown, entry.name.as_bytes())
154			else {
155				if buf_offset == 0 {
156					// Buffer too small to hold even one entry
157					return Err(Errno::Inval);
158				}
159				// Buffer full -> return bytes written so far; caller retries from read_idx
160				break;
161			};
162
163			*read_idx += 1;
164			buf_offset = next_dirent;
165		}
166		Ok(buf_offset)
167	}
168
169	async fn lseek(&self, offset: isize, whence: SeekWhence) -> io::Result<isize> {
170		if whence != SeekWhence::Set && offset != 0 {
171			error!("Invalid offset for directory lseek ({offset})");
172			return Err(Errno::Inval);
173		}
174		*self.read_idx.lock().await = offset as usize;
175		Ok(offset)
176	}
177}
178
179#[derive(Debug)]
180pub(crate) struct Filesystem {
181	root: MemDirectory,
182}
183
184impl Filesystem {
185	pub fn new() -> Self {
186		Self {
187			root: MemDirectory::new(AccessPermission::from_bits(0o777).unwrap()),
188		}
189	}
190
191	/// Tries to open file at given path.
192	pub fn open(
193		&self,
194		path: &str,
195		opt: OpenOption,
196		mode: AccessPermission,
197	) -> io::Result<Arc<async_lock::RwLock<Fd>>> {
198		debug!("Open file {path} with {opt:?}");
199
200		let path = path.strip_prefix('/').unwrap_or(path);
201
202		self.root.traverse_open(path, opt, mode)
203	}
204
205	/// Unlinks a file given by path
206	pub fn unlink(&self, path: &str) -> io::Result<()> {
207		debug!("Unlinking file {path}");
208
209		let path = path.strip_prefix('/').unwrap_or(path);
210
211		self.root.traverse_unlink(path)
212	}
213
214	/// Remove directory given by path
215	pub fn rmdir(&self, path: &str) -> io::Result<()> {
216		debug!("Removing directory {path}");
217
218		let path = path.strip_prefix('/').unwrap_or(path);
219
220		self.root.traverse_rmdir(path)
221	}
222
223	/// Create directory given by path
224	pub fn mkdir(&self, path: &str, mode: AccessPermission) -> io::Result<()> {
225		debug!("Create directory {path}");
226
227		let path = path.strip_prefix('/').unwrap_or(path);
228
229		self.root.traverse_mkdir(path, mode)
230	}
231
232	pub fn opendir(&self, path: &str) -> io::Result<Arc<async_lock::RwLock<Fd>>> {
233		debug!("Open directory {path}");
234		Ok(Arc::new(async_lock::RwLock::new(
235			DirectoryReader::new(self.readdir(path)?).into(),
236		)))
237	}
238
239	/// List given directory
240	pub fn readdir(&self, path: &str) -> io::Result<Vec<DirectoryEntry>> {
241		debug!("Readdir {path}");
242
243		let path = path.strip_prefix('/').unwrap_or(path);
244
245		self.root.traverse_readdir(path)
246	}
247
248	/// stat
249	pub fn stat(&self, path: &str) -> io::Result<FileAttr> {
250		debug!("Getting stats {path}");
251
252		let path = path.strip_prefix('/').unwrap_or(path);
253
254		self.root.traverse_stat(path)
255	}
256
257	/// lstat
258	pub fn lstat(&self, path: &str) -> io::Result<FileAttr> {
259		debug!("Getting lstats {path}");
260
261		let path = path.strip_prefix('/').unwrap_or(path);
262
263		self.root.traverse_lstat(path)
264	}
265
266	/// Create new backing-fs at mountpoint mntpath
267	pub fn mount(&self, path: &str, obj: Box<dyn VfsNode>) -> io::Result<()> {
268		debug!("Mounting {path}");
269
270		let path = path.strip_prefix('/').unwrap_or(path);
271
272		self.root.traverse_mount(path, obj)
273	}
274
275	/// Create read-only file
276	pub fn create_file(
277		&self,
278		path: &str,
279		data: &'static [u8],
280		mode: AccessPermission,
281	) -> io::Result<()> {
282		debug!("Create read-only file {path}");
283
284		let path = path.strip_prefix('/').unwrap_or(path);
285
286		self.root.traverse_create_file(path, data, mode)
287	}
288}
289
290#[repr(C)]
291#[derive(Debug, Default, Copy, Clone)]
292pub struct FileAttr {
293	pub st_dev: u64,
294	pub st_ino: u64,
295	pub st_nlink: u64,
296	/// Access permissions
297	pub st_mode: AccessPermission,
298	/// User id
299	pub st_uid: u32,
300	/// Group id
301	pub st_gid: u32,
302	/// Device id
303	pub st_rdev: u64,
304	/// Size in bytes
305	pub st_size: i64,
306	/// Block size
307	pub st_blksize: i64,
308	/// Size in blocks
309	pub st_blocks: i64,
310	/// Time of last access
311	pub st_atim: timespec,
312	/// Time of last modification
313	pub st_mtim: timespec,
314	/// Time of last status change
315	pub st_ctim: timespec,
316}
317
318#[derive(TryFromPrimitive, IntoPrimitive, PartialEq, Eq, Clone, Copy, Debug)]
319#[repr(u8)]
320pub enum FileType {
321	Unknown = 0,         // DT_UNKNOWN
322	Fifo = 1,            // DT_FIFO
323	CharacterDevice = 2, // DT_CHR
324	Directory = 4,       // DT_DIR
325	BlockDevice = 6,     // DT_BLK
326	RegularFile = 8,     // DT_REG
327	SymbolicLink = 10,   // DT_LNK
328	Socket = 12,         // DT_SOCK
329	Whiteout = 14,       // DT_WHT
330}
331
332#[derive(TryFromPrimitive, IntoPrimitive, PartialEq, Eq, Clone, Copy, Debug)]
333#[repr(u8)]
334pub enum SeekWhence {
335	Set = 0,
336	Cur = 1,
337	End = 2,
338	Data = 3,
339	Hole = 4,
340}
341
342pub(crate) fn init() {
343	const VERSION: &str = env!("CARGO_PKG_VERSION");
344	const UTC_BUILT_TIME: &str = build_time::build_time_utc!();
345
346	let root_filesystem = Filesystem::new();
347
348	// Handle optional Initramfs specified in start info modules.
349	#[cfg(feature = "initramfs")]
350	for i in env::start_info().modules() {
351		let i = i.as_slice();
352
353		// Skip the kernel itself, or similar binaries passed as modules
354		if i.starts_with(goblin::elf64::header::ELFMAG) {
355			continue;
356		}
357
358		root_filesystem
359			.root
360			.try_extend_from_image(i)
361			.expect("Unable to parse initramfs");
362	}
363
364	#[cfg(all(feature = "uhyve", not(feature = "initramfs")))]
365	if env::start_info().is_uhyve() && env::start_info().modules().next().is_some() {
366		error!(
367			"Kernel built without Hermit image support, but a Hermit image was supplied: ignoring"
368		);
369	}
370
371	root_filesystem
372		.mkdir("/tmp", AccessPermission::from_bits(0o777).unwrap())
373		.expect("Unable to create /tmp");
374	root_filesystem
375		.mkdir("/proc", AccessPermission::from_bits(0o777).unwrap())
376		.expect("Unable to create /proc");
377
378	FILESYSTEM.set(root_filesystem).unwrap();
379
380	if let Ok(mut file) = File::create("/proc/version") {
381		if write!(file, "Hermit version {VERSION} # UTC {UTC_BUILT_TIME}").is_err() {
382			error!("Unable to write in /proc/version");
383		}
384	} else {
385		error!("Unable to create /proc/version");
386	}
387
388	*WORKING_DIRECTORY.lock() = Some("/tmp".to_owned());
389
390	#[cfg(feature = "virtio-fs")]
391	virtio_fs::init();
392
393	#[cfg(feature = "uhyve")]
394	use crate::env::UhyveStartInfo;
395
396	#[cfg(feature = "uhyve")]
397	if env::start_info().is_uhyve() {
398		uhyve::init();
399	}
400
401	dev_directory::init();
402}
403
404pub fn create_file(name: &str, data: &'static [u8], mode: AccessPermission) -> io::Result<()> {
405	with_relative_filename(name, |name| {
406		FILESYSTEM
407			.get()
408			.ok_or(Errno::Inval)?
409			.create_file(name, data, mode)
410	})
411}
412
413/// Removes an empty directory.
414pub fn remove_dir(path: &str) -> io::Result<()> {
415	with_relative_filename(path, |path| {
416		FILESYSTEM.get().ok_or(Errno::Inval)?.rmdir(path)
417	})
418}
419
420pub fn unlink(path: &str) -> io::Result<()> {
421	with_relative_filename(path, |path| {
422		FILESYSTEM.get().ok_or(Errno::Inval)?.unlink(path)
423	})
424}
425
426/// Creates a new, empty directory at the provided path
427pub fn create_dir(path: &str, mode: AccessPermission) -> io::Result<()> {
428	let mask = *UMASK.lock();
429
430	with_relative_filename(path, |path| {
431		FILESYSTEM
432			.get()
433			.ok_or(Errno::Inval)?
434			.mkdir(path, mode.bitand(mask))
435	})
436}
437
438/// Returns an vector with all the entries within a directory.
439pub fn readdir(name: &str) -> io::Result<Vec<DirectoryEntry>> {
440	debug!("Read directory {name}");
441
442	with_relative_filename(name, |name| {
443		FILESYSTEM.get().ok_or(Errno::Inval)?.readdir(name)
444	})
445}
446
447pub fn read_stat(name: &str) -> io::Result<FileAttr> {
448	with_relative_filename(name, |name| {
449		FILESYSTEM.get().ok_or(Errno::Inval)?.stat(name)
450	})
451}
452
453pub fn read_lstat(name: &str) -> io::Result<FileAttr> {
454	with_relative_filename(name, |name| {
455		FILESYSTEM.get().ok_or(Errno::Inval)?.lstat(name)
456	})
457}
458
459fn with_relative_filename<F, T>(name: &str, callback: F) -> io::Result<T>
460where
461	F: FnOnce(&str) -> io::Result<T>,
462{
463	if name.starts_with("/") {
464		return callback(name);
465	}
466
467	let path = {
468		let cwd = WORKING_DIRECTORY.lock();
469
470		let Some(cwd) = cwd.as_ref() else {
471			// Relative path with no CWD, this is weird/impossible
472			return Err(Errno::Badf);
473		};
474
475		let mut path = String::with_capacity(cwd.len() + name.len() + 1);
476		path.push_str(cwd);
477		path.push('/');
478		path.push_str(name);
479		path
480	};
481
482	callback(&path)
483}
484
485pub fn truncate(name: &str, size: usize) -> io::Result<()> {
486	with_relative_filename(name, |name| {
487		let fs = FILESYSTEM.get().ok_or(Errno::Inval)?;
488		let file = fs
489			.open(name, OpenOption::O_TRUNC, AccessPermission::empty())
490			.map_err(|_| Errno::Badf)?;
491
492		block_on(async { file.read().await.truncate(size).await }, None)
493	})
494}
495
496pub fn open(name: &str, flags: OpenOption, mode: AccessPermission) -> io::Result<RawFd> {
497	// mode is 0x777 (0b0111_0111_0111), when flags | O_CREAT, else 0
498	// flags is bitmask of O_DEC_* defined above.
499	// (taken from rust stdlib/sys hermit target )
500	let mask = *UMASK.lock();
501
502	with_relative_filename(name, |name| {
503		debug!("Open {name}, {flags:?}, {mode:?}");
504
505		let fs = FILESYSTEM.get().ok_or(Errno::Inval)?;
506		let file = fs.open(name, flags, mode.bitand(mask))?;
507		let fd = insert_object(file)?;
508		Ok(fd)
509	})
510}
511
512pub fn get_cwd() -> io::Result<String> {
513	let cwd = WORKING_DIRECTORY.lock();
514	let cwd = cwd.as_ref().ok_or(Errno::Noent)?;
515	Ok(cwd.clone())
516}
517
518pub fn set_cwd(cwd: &str) -> io::Result<()> {
519	// TODO: check that the directory exists and that permission flags are correct
520
521	let mut working_dir = WORKING_DIRECTORY.lock();
522	if cwd.starts_with("/") {
523		*working_dir = Some(cwd.to_owned());
524	} else {
525		let working_dir = working_dir.as_mut().ok_or(Errno::Badf)?;
526		working_dir.push('/');
527		working_dir.push_str(cwd);
528	}
529
530	Ok(())
531}
532
533pub fn umask(new_mask: AccessPermission) -> AccessPermission {
534	let mut lock = UMASK.lock();
535	let old = *lock;
536	*lock = new_mask;
537	old
538}
539
540/// Open a directory to read the directory entries
541pub(crate) fn opendir(name: &str) -> io::Result<RawFd> {
542	let obj = FILESYSTEM.get().ok_or(Errno::Inval)?.opendir(name)?;
543	insert_object(obj)
544}
545
546use crate::fd::{self, RawFd};
547
548pub fn file_attributes(path: &str) -> io::Result<FileAttr> {
549	FILESYSTEM.get().ok_or(Errno::Inval)?.lstat(path)
550}
551
552#[allow(clippy::len_without_is_empty)]
553#[derive(Debug, Copy, Clone)]
554pub struct Metadata(FileAttr);
555
556impl Metadata {
557	/// Returns the size of the file, in bytes
558	pub fn len(&self) -> usize {
559		self.0.st_size.try_into().unwrap()
560	}
561
562	/// Returns true if this metadata is for a file.
563	pub fn is_file(&self) -> bool {
564		self.0.st_mode.contains(AccessPermission::S_IFREG)
565	}
566
567	/// Returns true if this metadata is for a directory.
568	pub fn is_dir(&self) -> bool {
569		self.0.st_mode.contains(AccessPermission::S_IFDIR)
570	}
571
572	/// Returns the last modification time listed in this metadata.
573	pub fn modified(&self) -> io::Result<SystemTime> {
574		Ok(SystemTime::from(self.0.st_mtim))
575	}
576
577	/// Returns the last modification time listed in this metadata.
578	pub fn accessed(&self) -> io::Result<SystemTime> {
579		Ok(SystemTime::from(self.0.st_atim))
580	}
581}
582
583/// Given a path, query the file system to get information about a file, directory, etc.
584pub fn metadata(path: &str) -> io::Result<Metadata> {
585	Ok(Metadata(file_attributes(path)?))
586}
587
588#[derive(Debug)]
589pub struct File {
590	fd: RawFd,
591	path: String,
592}
593
594impl File {
595	/// Opens a file in write-only mode.
596	///
597	/// This function will create a file if it does not exist, and will truncate it if it does.
598	fn create(path: &str) -> io::Result<File> {
599		let fd = open(
600			path,
601			OpenOption::O_CREAT | OpenOption::O_TRUNC | OpenOption::O_WRONLY,
602			AccessPermission::from_bits(0o666).unwrap(),
603		)?;
604
605		Ok(File {
606			fd,
607			path: path.to_owned(),
608		})
609	}
610
611	/// Creates a new file in read-write mode; error if the file exists.
612	///
613	/// This function will create a file if it does not exist, or return
614	/// an error if it does. This way, if the call succeeds, the file
615	/// returned is guaranteed to be new.
616	pub fn create_new(path: &str) -> io::Result<Self> {
617		let fd = open(
618			path,
619			OpenOption::O_CREAT | OpenOption::O_EXCL | OpenOption::O_RDWR,
620			AccessPermission::from_bits(0o666).unwrap(),
621		)?;
622
623		Ok(File {
624			fd,
625			path: path.to_owned(),
626		})
627	}
628
629	/// Attempts to open a file in read-write mode.
630	pub fn open(path: &str) -> io::Result<Self> {
631		let fd = open(
632			path,
633			OpenOption::O_RDWR,
634			AccessPermission::from_bits(0o666).unwrap(),
635		)?;
636
637		Ok(File {
638			fd,
639			path: path.to_owned(),
640		})
641	}
642
643	pub fn metadata(&self) -> io::Result<Metadata> {
644		metadata(&self.path)
645	}
646}
647
648impl embedded_io::ErrorType for File {
649	type Error = Errno;
650}
651
652impl Read for File {
653	fn read(&mut self, buf: &mut [u8]) -> Result<usize, Self::Error> {
654		fd::read(self.fd, buf)
655	}
656}
657
658impl Write for File {
659	fn write(&mut self, buf: &[u8]) -> Result<usize, Self::Error> {
660		fd::write(self.fd, buf)
661	}
662
663	fn flush(&mut self) -> Result<(), Self::Error> {
664		Ok(())
665	}
666}
667
668impl Drop for File {
669	fn drop(&mut self) {
670		if let Err(err) = remove_object(self.fd) {
671			error!("File::drop failed: {err}");
672		}
673	}
674}