corevm-engine 0.1.28

CoreVM engine that drives program execution either on the builder or CoreVM service side
Documentation
use crate::{Engine, HostCallError, InnerVm, OuterVm};
use core::ffi::CStr;
use corevm_host::{fs, Outcome, OutputStream};
use jam_types::Hash;
use polkakernel::{
	libc, write_dir_entry, Environment, Error, FileSystem, Machine, MachineError, Metadata,
	SeekFrom, WriteDirEntryErr,
};

pub struct KernelContext<'a, O: OuterVm> {
	pub engine: &'a mut Engine<O>,
	pub error: Option<HostCallError>,
}

impl<O: OuterVm> KernelContext<'_, O> {
	fn append(&mut self, stream: OutputStream, data: &[u8]) -> Result<(), HostCallError> {
		self.engine.append_output_slice(stream, data)
	}
}

impl<O: OuterVm> Machine for KernelContext<'_, O> {
	fn reg(&self, name: polkakernel::Reg) -> u64 {
		self.engine.args.regs[name as usize]
	}

	fn set_reg(&mut self, name: polkakernel::Reg, value: u64) {
		self.engine.args.regs[name as usize] = value;
	}

	fn read_u64(&mut self, address: u64) -> Result<u64, MachineError> {
		let mut buf = [0_u8; 8];
		self.read_memory_into(address, &mut buf)?;
		Ok(u64::from_le_bytes(buf))
	}

	fn read_u32(&mut self, address: u64) -> Result<u32, MachineError> {
		let mut buf = [0_u8; 4];
		self.read_memory_into(address, &mut buf)?;
		Ok(u32::from_le_bytes(buf))
	}

	fn read_u16(&mut self, address: u64) -> Result<u16, MachineError> {
		let mut buf = [0_u8; 2];
		self.read_memory_into(address, &mut buf)?;
		Ok(u16::from_le_bytes(buf))
	}

	fn read_u8(&mut self, address: u64) -> Result<u8, MachineError> {
		let mut buf = [0_u8; 1];
		self.read_memory_into(address, &mut buf)?;
		Ok(u8::from_le_bytes(buf))
	}

	fn read_memory_into(&mut self, address: u64, buf: &mut [u8]) -> Result<(), MachineError> {
		use MachineError::BadAddress;
		if self.error.is_some() {
			return Err(BadAddress);
		}
		let address: u32 = address.try_into().map_err(|_| BadAddress)?;
		let length: u32 = buf.len().try_into().map_err(|_| BadAddress)?;
		self.engine.validate_guest_can_read(address, length).map_err(|_| BadAddress)?;
		match self.engine.touch_once_range(address, length) {
			Ok(()) => {},
			Err(e) => {
				self.error = Some(e.into());
				return Err(BadAddress);
			},
		};
		self.engine.inner_vm.peek_into(buf, address).map_err(|_| BadAddress)
	}

	fn write_u64(&mut self, address: u64, value: u64) -> Result<(), MachineError> {
		let bytes = value.to_le_bytes();
		self.write_memory(address, &bytes[..])
	}

	fn write_u32(&mut self, address: u64, value: u32) -> Result<(), MachineError> {
		let bytes = value.to_le_bytes();
		self.write_memory(address, &bytes[..])
	}

	fn write_u16(&mut self, address: u64, value: u16) -> Result<(), MachineError> {
		let bytes = value.to_le_bytes();
		self.write_memory(address, &bytes[..])
	}

	fn write_u8(&mut self, address: u64, value: u8) -> Result<(), MachineError> {
		let bytes = value.to_le_bytes();
		self.write_memory(address, &bytes[..])
	}

	fn write_memory(&mut self, address: u64, buf: &[u8]) -> Result<(), MachineError> {
		use MachineError::BadAddress;
		if self.error.is_some() {
			return Err(BadAddress);
		}
		let address: u32 = address.try_into().map_err(|_| BadAddress)?;
		let length: u32 = buf.len().try_into().map_err(|_| BadAddress)?;
		self.engine.validate_guest_can_write(address, length).map_err(|_| BadAddress)?;
		match self.engine.touch_once_range(address, length) {
			Ok(()) => {},
			Err(e) => {
				self.error = Some(e.into());
				return Err(BadAddress);
			},
		}
		self.engine.inner_vm.poke(buf, address).map_err(|_| BadAddress)
	}
}

impl<O: OuterVm> Environment for KernelContext<'_, O> {
	fn write_to_stdout(&mut self, data: &[u8]) -> Result<u64, Error> {
		if self.error.is_some() {
			return Err(Error(libc::EIO));
		}
		if self.append(OutputStream::Stdout, data).is_err() {
			self.error = Some(Outcome::OutputLimitReached.into());
			return Err(Error(libc::EIO));
		}
		Ok(data.len() as u64)
	}

	fn write_to_stderr(&mut self, data: &[u8]) -> Result<u64, Error> {
		if self.error.is_some() {
			return Err(Error(libc::EIO));
		}
		if self.append(OutputStream::Stderr, data).is_err() {
			self.error = Some(Outcome::OutputLimitReached.into());
			return Err(Error(libc::EIO));
		}
		Ok(data.len() as u64)
	}
}

impl<O: OuterVm> FileSystem for KernelContext<'_, O> {
	type Fd = OpenedFile;

	fn open(&mut self, path: &CStr, flags: u64) -> Result<OpenedFile, Error> {
		let mut block_reader = self.engine.outer_vm.block_reader();
		let block_ref = fs::resolve_path(self.engine.exec.root_dir, c"", path, &mut block_reader)
			.map_err(|e| match e {
			fs::Error::Path => Error(libc::ENOENT),
			fs::Error::Node => Error(libc::ENOTDIR),
			fs::Error::Io | fs::Error::Block | fs::Error::Loop => Error(libc::EIO),
		})?;
		let node = fs::Node::open(&block_ref, &mut block_reader).map_err(io_error)?;
		if flags & libc::O_DIRECTORY != 0 && !matches!(node, fs::Node::Dir { .. }) {
			return Err(Error(libc::ENOTDIR));
		}
		Ok(OpenedFile { node: node.into(), block_ref })
	}

	fn seek(&mut self, fd: &mut OpenedFile, from: SeekFrom) -> Result<u64, Error> {
		use SeekFrom::*;
		match fd.node {
			FsNode::File(ref mut file) => {
				let new_position = match from {
					Start(offset) => offset,
					Current(offset) =>
						file.position().checked_add_signed(offset).ok_or(Error(libc::EINVAL))?,
					End(offset) => file
						.main_block()
						.file_size()
						.checked_add_signed(offset)
						.ok_or(Error(libc::EINVAL))?,
				};
				file.seek(new_position).map_err(io_error)?;
				Ok(file.position())
			},
			FsNode::Dir { ref dir, ref mut position } => {
				let new_position = match from {
					Start(offset) => offset,
					Current(offset) =>
						position.checked_add_signed(offset).ok_or(Error(libc::EINVAL))?,
					End(offset) => (dir.0.len() as u64)
						.checked_add_signed(offset)
						.ok_or(Error(libc::EINVAL))?,
				};
				if new_position >= dir.0.len() as u64 {
					return Err(Error(libc::EINVAL));
				}
				*position = new_position;
				Ok(new_position)
			},
		}
	}

	fn read(&mut self, fd: &mut OpenedFile, buf: &mut [u8]) -> Result<usize, Error> {
		let FsNode::File(ref mut file) = fd.node else {
			return Err(Error(libc::EBADF));
		};
		let mut block_reader = self.engine.outer_vm.block_reader();
		let num_bytes_read = file.read(buf, &mut block_reader).map_err(io_error)?;
		Ok(num_bytes_read)
	}

	fn metadata(&mut self, path: &CStr) -> Result<Metadata, Error> {
		let mut block_reader = self.engine.outer_vm.block_reader();
		let block_ref = fs::resolve_path(self.engine.exec.root_dir, c"", path, &mut block_reader)
			.map_err(|e| match e {
			fs::Error::Path => Error(libc::ENOENT),
			fs::Error::Node => Error(libc::ENOTDIR),
			fs::Error::Io | fs::Error::Block | fs::Error::Loop => Error(libc::EIO),
		})?;
		let file = fs::File::open(&block_ref, &mut block_reader).map_err(io_error)?;
		Ok(Metadata {
			id: hash_to_id(&block_ref.hash.0),
			size: file.main_block().file_size(),
			block_size: file.main_block().block_size(),
			mode: match file.main_block().kind() {
				fs::NodeKind::File => 0o100644,
				fs::NodeKind::Dir => 0o40755,
			},
		})
	}

	fn read_dir(&mut self, fd: &mut OpenedFile, buf: &mut [u8]) -> Result<usize, Error> {
		let FsNode::Dir { ref dir, ref mut position } = fd.node else {
			return Err(Error(libc::ENOTDIR));
		};
		let start = *position as usize;
		let mut offset = 0;
		for (name, block_ref) in dir.0.iter().skip(start) {
			if offset == buf.len() {
				break;
			}
			let id = hash_to_id(&block_ref.hash.0);
			match write_dir_entry(id, name.as_ref(), &mut buf[offset..]) {
				Ok(n) => {
					offset += n;
					*position += 1;
				},
				Err(WriteDirEntryErr::NameTooLong) => {
					const _: () = assert!(fs::MAX_FILE_NAME_LEN <= u16::MAX as usize);
					unreachable!()
				},
				Err(WriteDirEntryErr::BufferTooSmall) => break,
			}
		}
		Ok(offset)
	}
}

// TODO @ivan Ids (inodes) must be unique.
const fn hash_to_id(h: &Hash) -> u64 {
	u64::from_le_bytes([h[0], h[1], h[2], h[3], h[4], h[5], h[6], h[7]])
}

fn io_error<E>(_error: E) -> Error {
	Error(libc::EIO)
}

pub enum FsNode {
	File(fs::File),
	Dir { dir: fs::Dir, position: u64 },
}

impl FsNode {
	pub fn position(&self) -> u64 {
		match self {
			Self::File(file) => file.position(),
			Self::Dir { position, .. } => *position,
		}
	}
}

impl From<fs::Node> for FsNode {
	fn from(other: fs::Node) -> Self {
		match other {
			fs::Node::File(file) => Self::File(file),
			fs::Node::Dir(dir) => Self::Dir { dir, position: 0 },
		}
	}
}

pub struct OpenedFile {
	pub node: FsNode,
	pub block_ref: fs::BlockRef,
}