Class: Dewasm::Pozeiden::WasmModule::Rt::WASI

Inherits:
Object
  • Object
show all
Defined in:
lib/dewasm/pozeiden/wasm_module.rb

Defined Under Namespace

Classes: WasiDir

Constant Summary collapse

ERRNO_SUCCESS =
0
ERRNO_BADF =
8
ERRNO_INVAL =
28
ERRNO_IO =
29
ERRNO_NOSYS =
52
ERRNO_NOTSUP =
58
ERRNO_SPIPE =
70
ERRNO_NOTCAPABLE =

NOTCAPABLE lives in this always-bundled prelude, not errno_fs, because the per-fd rights model raises it from the stdio-core fd_* units too, not only from the path_* units that pull in errno_fs.

76
ERRNO_ACCES =

Filesystem-only errno codes: kept out of the always-bundled wasi/class prelude so a stdio-only WASI module (no path* / fs-only fd_* imports) doesn't carry them.

2
ERRNO_EXIST =
20
ERRNO_ISDIR =
31
ERRNO_LOOP =
32
ERRNO_NAMETOOLONG =
37
ERRNO_NOENT =
44
ERRNO_NOTDIR =
54
ERRNO_NOTEMPTY =
55
ERRNO_PERM =
63
FS_ERRNO =

One SystemCallError-to-WASI-errno table shared by every filesystem syscall, so the same host error never maps to different codes depending on which syscall raised it.

{
	Errno::EACCES => ERRNO_ACCES,
	Errno::EBADF => ERRNO_BADF,
	Errno::EEXIST => ERRNO_EXIST,
	Errno::EINVAL => ERRNO_INVAL,
	Errno::EISDIR => ERRNO_ISDIR,
	Errno::ELOOP => ERRNO_LOOP,
	Errno::ENAMETOOLONG => ERRNO_NAMETOOLONG,
	Errno::ENOENT => ERRNO_NOENT,
	Errno::ENOTDIR => ERRNO_NOTDIR,
	Errno::ENOTEMPTY => ERRNO_NOTEMPTY,
	Errno::EPERM => ERRNO_PERM,
	Errno::ESPIPE => ERRNO_SPIPE,
}.freeze
RIGHT_FD_DATASYNC =

WASI p1 rights bits and the per-filetype masks used to grant and enforce capabilities. Kept in its own unit so every rights-aware syscall can require it without pulling extra filesystem code. The @fd_meta map (fd => [base, inheriting, fdflags]) that these operate on is seeded in wasi/_class.

1 << 0
RIGHT_FD_READ =
1 << 1
RIGHT_FD_SEEK =
1 << 2
RIGHT_FD_FDSTAT_SET_FLAGS =
1 << 3
RIGHT_FD_SYNC =
1 << 4
RIGHT_FD_TELL =
1 << 5
RIGHT_FD_WRITE =
1 << 6
RIGHT_FD_ADVISE =
1 << 7
RIGHT_FD_ALLOCATE =
1 << 8
RIGHT_PATH_CREATE_DIRECTORY =
1 << 9
RIGHT_PATH_CREATE_FILE =
1 << 10
1 << 11
1 << 12
RIGHT_PATH_OPEN =
1 << 13
RIGHT_FD_READDIR =
1 << 14
1 << 15
RIGHT_PATH_RENAME_SOURCE =
1 << 16
RIGHT_PATH_RENAME_TARGET =
1 << 17
RIGHT_PATH_FILESTAT_GET =
1 << 18
RIGHT_PATH_FILESTAT_SET_SIZE =
1 << 19
RIGHT_PATH_FILESTAT_SET_TIMES =
1 << 20
RIGHT_FD_FILESTAT_GET =
1 << 21
RIGHT_FD_FILESTAT_SET_SIZE =
1 << 22
RIGHT_FD_FILESTAT_SET_TIMES =
1 << 23
1 << 24
RIGHT_PATH_REMOVE_DIRECTORY =
1 << 25
1 << 26
RIGHT_POLL_FD_READWRITE =
1 << 27
FILE_BASE_RIGHTS =

Rights a regular-file fd may carry, and the two directory masks. A directory fd's base is narrowed to DIR_BASE_RIGHTS (so a requested FD_SEEK or FD_WRITE is dropped rather than granted), and its inheriting rights add the file rights it may pass to children (DIR_INHERITING_RIGHTS). These mirror the fixed masks wasi-common applies at path_open.

RIGHT_FD_DATASYNC | RIGHT_FD_READ | RIGHT_FD_SEEK |
RIGHT_FD_FDSTAT_SET_FLAGS | RIGHT_FD_SYNC | RIGHT_FD_TELL |
RIGHT_FD_WRITE | RIGHT_FD_ADVISE | RIGHT_FD_ALLOCATE |
RIGHT_FD_FILESTAT_GET | RIGHT_FD_FILESTAT_SET_SIZE |
RIGHT_FD_FILESTAT_SET_TIMES | RIGHT_POLL_FD_READWRITE
DIR_BASE_RIGHTS =
RIGHT_PATH_CREATE_DIRECTORY | RIGHT_PATH_CREATE_FILE |
RIGHT_PATH_LINK_SOURCE | RIGHT_PATH_LINK_TARGET |
RIGHT_PATH_OPEN | RIGHT_FD_READDIR | RIGHT_PATH_READLINK |
RIGHT_PATH_RENAME_SOURCE | RIGHT_PATH_RENAME_TARGET |
RIGHT_PATH_FILESTAT_GET | RIGHT_PATH_FILESTAT_SET_SIZE |
RIGHT_PATH_FILESTAT_SET_TIMES | RIGHT_FD_FILESTAT_GET |
RIGHT_FD_FILESTAT_SET_TIMES | RIGHT_PATH_SYMLINK |
RIGHT_PATH_REMOVE_DIRECTORY | RIGHT_PATH_UNLINK_FILE
DIR_INHERITING_RIGHTS =
DIR_BASE_RIGHTS | FILE_BASE_RIGHTS

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(args: [], env: {}, preopens: {}) ⇒ WASI

Returns a new instance of WASI.



541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
# File 'lib/dewasm/pozeiden/wasm_module.rb', line 541

def initialize(args: [], env: {}, preopens: {})
	@args = args.map(&:to_s)
	@env = env.map { |k, v| "#{k}=#{v}" }
	@fds = { 0 => $stdin, 1 => $stdout, 2 => $stderr }
	# Per-fd capability metadata: fd => [rights_base, rights_inheriting, fdflags]. stdio is seeded all-rights (it is never rights-tested and must stay readable/writable); preopens likewise, so a real embedder keeps unrestricted access and path_open derives the narrowed rights from them.
	@fd_meta = {
		0 => [Rt::M64, Rt::M64, 0],
		1 => [Rt::M64, Rt::M64, 0],
		2 => [Rt::M64, Rt::M64, 0],
	}
	# The stdio special-cases (SPIPE on seek/tell/pread/pwrite, no close)
	# key on the objects captured here, in lockstep with the fd table, not on whatever the globals point at when a syscall runs.
	@std_ios = [$stdin, $stdout, $stderr].freeze
	next_fd = 3
	preopens.each do |guest, host|
		# The host path must resolve, but need not be a directory: a single-file preopen (e.g. "/dev/null" for the zeroperl reactor's init probe) is accepted: the guest resolves it as the preopen root itself.
		real = begin
			File.realpath(host)
		rescue SystemCallError => e
			raise ArgumentError, "preopen #{guest.inspect} => #{host.inspect}: #{e.message}"
		end
		@fds[next_fd] = WasiDir.new(real, guest, nil)
		# A preopen is a directory, so its base is the directory-rights set
		# (no FD_WRITE etc.); its inheriting rights carry the full file-rights set so guest-opened files under it get real read/write capability.
		# root_directory() in the testsuite reopens the preopen with exactly these, so seeding all-of-M64 here would wrongly hand a directory the write right and make that reopen fail EISDIR.
		@fd_meta[next_fd] = [DIR_BASE_RIGHTS, DIR_INHERITING_RIGHTS, 0]
		next_fd += 1
	end
	@next_fd = next_fd
	$stdout.binmode
	$stderr.binmode
	$stdin.binmode
end

Instance Attribute Details

#memoryObject (readonly)

Returns the value of attribute memory.



539
540
541
# File 'lib/dewasm/pozeiden/wasm_module.rb', line 539

def memory
  @memory
end

Instance Method Details

#attach(instance) ⇒ Object



581
582
583
# File 'lib/dewasm/pozeiden/wasm_module.rb', line 581

def attach(instance)
	@memory = instance.memory
end

#import(name) ⇒ Object

Import-provider protocol: a custom WASI runtime replaces this class wholesale by implementing these two methods.



576
577
578
579
# File 'lib/dewasm/pozeiden/wasm_module.rb', line 576

def import(name)
	meth = :"wasi_#{name}"
	respond_to?(meth) ? method(meth) : nil
end

#wasi_clock_res_get(_id, out_ptr) ⇒ Object



585
586
587
588
# File 'lib/dewasm/pozeiden/wasm_module.rb', line 585

def wasi_clock_res_get(_id, out_ptr)
	@memory.ids(out_ptr, 1)
	ERRNO_SUCCESS
end

#wasi_clock_time_get(id, _precision, out_ptr) ⇒ Object



590
591
592
593
594
595
596
597
598
599
# File 'lib/dewasm/pozeiden/wasm_module.rb', line 590

def wasi_clock_time_get(id, _precision, out_ptr)
	ns =
		case id
		when 0 then Process.clock_gettime(Process::CLOCK_REALTIME, :nanosecond)
		when 1, 2, 3 then Process.clock_gettime(Process::CLOCK_MONOTONIC, :nanosecond)
		else return ERRNO_INVAL
		end
	@memory.ids(out_ptr, Rt.m64(ns))
	ERRNO_SUCCESS
end

#wasi_environ_get(environ_ptr, buf_ptr) ⇒ Object



601
602
603
# File 'lib/dewasm/pozeiden/wasm_module.rb', line 601

def wasi_environ_get(environ_ptr, buf_ptr)
	write_string_list(@env, environ_ptr, buf_ptr)
end

#wasi_environ_sizes_get(count_ptr, buf_size_ptr) ⇒ Object



605
606
607
608
609
# File 'lib/dewasm/pozeiden/wasm_module.rb', line 605

def wasi_environ_sizes_get(count_ptr, buf_size_ptr)
	@memory.iws(count_ptr, @env.size)
	@memory.iws(buf_size_ptr, @env.sum { |e| e.bytesize + 1 })
	ERRNO_SUCCESS
end

#wasi_fd_close(fd) ⇒ Object



644
645
646
647
648
649
650
# File 'lib/dewasm/pozeiden/wasm_module.rb', line 644

def wasi_fd_close(fd)
	io = @fds.delete(fd)
	return ERRNO_BADF unless io
	return ERRNO_SUCCESS if io.is_a?(WasiDir) # no real OS handle to close
	io.close unless @std_ios.include?(io)
	ERRNO_SUCCESS
end

#wasi_fd_fdstat_get(fd, out_ptr) ⇒ Object



652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
# File 'lib/dewasm/pozeiden/wasm_module.rb', line 652

def wasi_fd_fdstat_get(fd, out_ptr)
	io = @fds[fd]
	return ERRNO_BADF unless io
	filetype =
		if io.is_a?(WasiDir)
			3 # directory
		else
			io.respond_to?(:tty?) && io.tty? ? 2 : 4 # char device / regular file
		end
	base, inheriting, fdflags = @fd_meta[fd] || [Rt::M64, Rt::M64, 0]
	@memory.fill(out_ptr, 0, 24)
	@memory.iwsb(out_ptr, filetype)
	@memory.iwsh(out_ptr + 2, fdflags)
	@memory.ids(out_ptr + 8, base)
	@memory.ids(out_ptr + 16, inheriting)
	ERRNO_SUCCESS
end

#wasi_fd_filestat_get(fd, buf_ptr) ⇒ Object



670
671
672
673
674
675
676
677
678
# File 'lib/dewasm/pozeiden/wasm_module.rb', line 670

def wasi_fd_filestat_get(fd, buf_ptr)
	entry = @fds[fd]
	return ERRNO_BADF unless entry
	stat = entry.is_a?(WasiDir) ? File.stat(entry.host_path) : entry.stat
	@memory.init(buf_ptr, pack_filestat(stat), 0, 64)
	ERRNO_SUCCESS
rescue SystemCallError
	ERRNO_IO
end

#wasi_fd_filestat_set_size(fd, size) ⇒ Object



680
681
682
683
684
685
686
687
688
# File 'lib/dewasm/pozeiden/wasm_module.rb', line 680

def wasi_fd_filestat_set_size(fd, size)
	io = @fds[fd]
	return ERRNO_BADF if io.nil? || io.is_a?(WasiDir)
	return ERRNO_NOTCAPABLE unless fd_has_right?(fd, RIGHT_FD_FILESTAT_SET_SIZE)
	io.truncate(size)
	ERRNO_SUCCESS
rescue SystemCallError
	ERRNO_IO
end

#wasi_fd_pwrite(fd, iovs_ptr, iovs_len, offset, nwritten_ptr) ⇒ Object



690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
# File 'lib/dewasm/pozeiden/wasm_module.rb', line 690

def wasi_fd_pwrite(fd, iovs_ptr, iovs_len, offset, nwritten_ptr)
	io = @fds[fd]
	return ERRNO_BADF if io.nil? || io.is_a?(WasiDir)
	return ERRNO_SPIPE if @std_ios.include?(io)
	return ERRNO_NOTCAPABLE unless fd_has_right?(fd, RIGHT_FD_WRITE)
	written = 0
	iovs_len.times do |i|
		ptr = @memory.iwl(iovs_ptr + i * 8)
		len = @memory.iwl(iovs_ptr + i * 8 + 4)
		chunk = @memory.read_string(ptr, len)
		n = io.pwrite(chunk, offset + written)
		written += n
		# IO#pwrite is a single pwrite(2), which may write short; stop so the reported nwritten stays contiguous.
		break if n < chunk.bytesize
	end
	@memory.iws(nwritten_ptr, written)
	ERRNO_SUCCESS
rescue SystemCallError
	ERRNO_IO
end

#wasi_fd_read(fd, iovs_ptr, iovs_len, nread_ptr) ⇒ Object



711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
# File 'lib/dewasm/pozeiden/wasm_module.rb', line 711

def wasi_fd_read(fd, iovs_ptr, iovs_len, nread_ptr)
	io = @fds[fd]
	return ERRNO_BADF if io.nil? || io.is_a?(WasiDir)
	return ERRNO_NOTCAPABLE unless fd_has_right?(fd, RIGHT_FD_READ)
	stdin = @std_ios[0].equal?(io)
	nread = 0
	iovs_len.times do |i|
		ptr = @memory.iwl(iovs_ptr + i * 8)
		len = @memory.iwl(iovs_ptr + i * 8 + 4)
		next if len == 0
		# stdin may be an interactive tty (the QuickJS REPL under a pty): a buffered
		# IO#read(len) blocks until the full len arrives or EOF, deadlocking a line-buffered terminal that never sends EOF. readpartial returns as soon as any bytes are available (the WASI short-read semantics wasmtime uses), while poll_oneoff's IO.select already did the waiting.
		# Files keep #read.
		if stdin
			begin
				chunk = io.readpartial(len)
			rescue EOFError
				break
			end
		else
			chunk = io.read(len)
			break if chunk.nil?
		end
		@memory.init(ptr, chunk, 0, chunk.bytesize)
		nread += chunk.bytesize
		break if chunk.bytesize < len
	end
	@memory.iws(nread_ptr, nread)
	ERRNO_SUCCESS
rescue SystemCallError
	ERRNO_IO
end

#wasi_fd_readdir(fd, buf_ptr, buf_len, cookie, bufused_ptr) ⇒ Object



744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
# File 'lib/dewasm/pozeiden/wasm_module.rb', line 744

def wasi_fd_readdir(fd, buf_ptr, buf_len, cookie, bufused_ptr)
	entry = @fds[fd]
	return ERRNO_BADF unless entry.is_a?(WasiDir)
	return ERRNO_NOTCAPABLE unless fd_has_right?(fd, RIGHT_FD_READDIR)
	# cookie 0 starts a fresh scan, so re-snapshot the listing (a directory mutated between two full readdirs must show the new state); cookie > 0 continues the snapshot taken when this scan began.
	entry.entries = readdir_entries(entry.host_path) if cookie.zero? || entry.entries.nil?
	entries = entry.entries

	out = +"".b
	i = cookie
	while i < entries.size && out.bytesize < buf_len
		name, filetype, ino = entries[i]
		# dirent: d_next (u64, resume cookie) + d_ino (u64) + d_namlen (u32) + d_type (u8) + 3 pad, followed immediately by the (unpadded) name.
		out << [i + 1, ino, name.bytesize, filetype].pack("Q<Q<L<Cx3") << name.b
		i += 1
	end
	# A dirent may be legally truncated at the tail once buf_len runs out.
	out = out.byteslice(0, buf_len) if out.bytesize > buf_len
	@memory.init(buf_ptr, out, 0, out.bytesize)
	@memory.iws(bufused_ptr, out.bytesize)
	ERRNO_SUCCESS
rescue SystemCallError
	ERRNO_IO
end

#wasi_fd_seek(fd, offset, whence, out_ptr) ⇒ Object



782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
# File 'lib/dewasm/pozeiden/wasm_module.rb', line 782

def wasi_fd_seek(fd, offset, whence, out_ptr)
	io = @fds[fd]
	return ERRNO_BADF if io.nil? || io.is_a?(WasiDir)
	return ERRNO_SPIPE if @std_ios.include?(io)
	return ERRNO_NOTCAPABLE unless fd_has_right?(fd, RIGHT_FD_SEEK)
	mode = [IO::SEEK_SET, IO::SEEK_CUR, IO::SEEK_END][whence]
	return ERRNO_INVAL unless mode
	io.seek(Rt.s64(offset), mode)
	@memory.ids(out_ptr, Rt.m64(io.tell))
	ERRNO_SUCCESS
rescue Errno::EINVAL
	# Seeking before byte 0 raises EINVAL; surface it precisely rather than folding it into the generic ERRNO_IO.
	ERRNO_INVAL
rescue SystemCallError
	ERRNO_IO
end

#wasi_fd_write(fd, iovs_ptr, iovs_len, nwritten_ptr) ⇒ Object



799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
# File 'lib/dewasm/pozeiden/wasm_module.rb', line 799

def wasi_fd_write(fd, iovs_ptr, iovs_len, nwritten_ptr)
	io = @fds[fd]
	return ERRNO_BADF if io.nil? || io.is_a?(WasiDir)
	return ERRNO_NOTCAPABLE unless fd_has_right?(fd, RIGHT_FD_WRITE)
	# APPEND is implemented here rather than via O_APPEND (so fd_fdstat_set_flags can turn it off): seek to end before writing.
	io.seek(0, IO::SEEK_END) if (@fd_meta[fd][2] & 0x1) != 0
	written = 0
	iovs_len.times do |i|
		ptr = @memory.iwl(iovs_ptr + i * 8)
		len = @memory.iwl(iovs_ptr + i * 8 + 4)
		written += io.write(@memory.read_string(ptr, len))
	end
	io.flush
	@memory.iws(nwritten_ptr, written)
	ERRNO_SUCCESS
rescue SystemCallError
	ERRNO_IO
end

#wasi_path_create_directory(dirfd, path_ptr, path_len) ⇒ Object



830
831
832
833
834
835
836
837
838
839
840
841
# File 'lib/dewasm/pozeiden/wasm_module.rb', line 830

def wasi_path_create_directory(dirfd, path_ptr, path_len)
	rel = @memory.read_string(path_ptr, path_len)
	# Strip a trailing slash: mkdir names a directory anyway, and EEXIST is wasmtime's answer for mkdir("file/") where the hosts split.
	rel = rel.sub(%r{(.)/+\z}, '\1')
	# mkdir(2) never follows a trailing symlink (an existing one is EEXIST).
	host_path, err = resolve_path(dirfd, rel, follow_last: false)
	return err if err
	Dir.mkdir(host_path)
	ERRNO_SUCCESS
rescue SystemCallError => e
	fs_errno(e)
end

#wasi_path_filestat_get(dirfd, flags, path_ptr, path_len, buf_ptr) ⇒ Object



843
844
845
846
847
848
849
850
851
852
853
# File 'lib/dewasm/pozeiden/wasm_module.rb', line 843

def wasi_path_filestat_get(dirfd, flags, path_ptr, path_len, buf_ptr)
	rel = @memory.read_string(path_ptr, path_len)
	symlink_follow = flags & 0x1 != 0 # lookupflags::SYMLINK_FOLLOW
	host_path, err = resolve_path(dirfd, rel, follow_last: symlink_follow)
	return err if err
	stat = symlink_follow ? File.stat(host_path) : File.lstat(host_path)
	@memory.init(buf_ptr, pack_filestat(stat), 0, 64)
	ERRNO_SUCCESS
rescue SystemCallError => e
	fs_errno(e)
end

#wasi_path_remove_directory(dirfd, path_ptr, path_len) ⇒ Object



855
856
857
858
859
860
861
862
863
864
865
866
867
868
# File 'lib/dewasm/pozeiden/wasm_module.rb', line 855

def wasi_path_remove_directory(dirfd, path_ptr, path_len)
	rel = @memory.read_string(path_ptr, path_len)
	# rmdir(2) never follows a trailing symlink.
	host_path, err = resolve_path(dirfd, rel, follow_last: false)
	return err if err
	# rmdir through a trailing slash on an existing directory is EINVAL per wasmtime; other shapes come from the host call.
	if host_path.end_with?("/") && File.directory?(host_path.delete_suffix("/"))
		return ERRNO_INVAL
	end
	Dir.rmdir(host_path)
	ERRNO_SUCCESS
rescue SystemCallError => e
	fs_errno(e)
end


870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
# File 'lib/dewasm/pozeiden/wasm_module.rb', line 870

def wasi_path_unlink_file(dirfd, path_ptr, path_len)
	rel = @memory.read_string(path_ptr, path_len)
	# unlink(2) never follows a trailing symlink: it removes the link.
	host_path, err = resolve_path(dirfd, rel, follow_last: false)
	return err if err
	# A slash-suffixed non-directory is ENOTDIR, missing is ENOENT; a real directory falls through to File.unlink (host EPERM/EISDIR).
	# Probe the slash-stripped path: stat on "file/" fails ENOTDIR, reads as missing.
	if host_path.end_with?("/")
		bare = host_path.delete_suffix("/")
		unless File.directory?(bare)
			return File.exist?(bare) || File.symlink?(bare) ? ERRNO_NOTDIR : ERRNO_NOENT
		end
	end
	File.unlink(host_path)
	ERRNO_SUCCESS
rescue SystemCallError => e
	fs_errno(e)
end

#wasi_random_get(buf_ptr, len) ⇒ Object



891
892
893
894
# File 'lib/dewasm/pozeiden/wasm_module.rb', line 891

def wasi_random_get(buf_ptr, len)
	@memory.init(buf_ptr, SecureRandom.bytes(len), 0, len)
	ERRNO_SUCCESS
end

#write_string_list(strings, list_ptr, buf_ptr) ⇒ Object



1043
1044
1045
1046
1047
1048
1049
1050
1051
# File 'lib/dewasm/pozeiden/wasm_module.rb', line 1043

def write_string_list(strings, list_ptr, buf_ptr)
	strings.each_with_index do |s, i|
		@memory.iws(list_ptr + i * 4, buf_ptr)
		@memory.init(buf_ptr, s, 0, s.bytesize)
		@memory.iwsb(buf_ptr + s.bytesize, 0)
		buf_ptr += s.bytesize + 1
	end
	ERRNO_SUCCESS
end