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.



454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
# File 'lib/dewasm/pozeiden/wasm_module.rb', line 454

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.



452
453
454
# File 'lib/dewasm/pozeiden/wasm_module.rb', line 452

def memory
  @memory
end

Instance Method Details

#attach(instance) ⇒ Object



494
495
496
# File 'lib/dewasm/pozeiden/wasm_module.rb', line 494

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.



489
490
491
492
# File 'lib/dewasm/pozeiden/wasm_module.rb', line 489

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

#wasi_clock_res_get(_id, out_ptr) ⇒ Object



498
499
500
501
# File 'lib/dewasm/pozeiden/wasm_module.rb', line 498

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

#wasi_clock_time_get(id, _precision, out_ptr) ⇒ Object



503
504
505
506
507
508
509
510
511
512
# File 'lib/dewasm/pozeiden/wasm_module.rb', line 503

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.i64_store(out_ptr, Rt.m64(ns))
	ERRNO_SUCCESS
end

#wasi_environ_get(environ_ptr, buf_ptr) ⇒ Object



514
515
516
# File 'lib/dewasm/pozeiden/wasm_module.rb', line 514

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



518
519
520
521
522
# File 'lib/dewasm/pozeiden/wasm_module.rb', line 518

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

#wasi_fd_close(fd) ⇒ Object



557
558
559
560
561
562
563
# File 'lib/dewasm/pozeiden/wasm_module.rb', line 557

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



565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
# File 'lib/dewasm/pozeiden/wasm_module.rb', line 565

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.i32_store8(out_ptr, filetype)
	@memory.i32_store16(out_ptr + 2, fdflags)
	@memory.i64_store(out_ptr + 8, base)
	@memory.i64_store(out_ptr + 16, inheriting)
	ERRNO_SUCCESS
end

#wasi_fd_filestat_get(fd, buf_ptr) ⇒ Object



583
584
585
586
587
588
589
590
591
# File 'lib/dewasm/pozeiden/wasm_module.rb', line 583

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



593
594
595
596
597
598
599
600
601
# File 'lib/dewasm/pozeiden/wasm_module.rb', line 593

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



603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
# File 'lib/dewasm/pozeiden/wasm_module.rb', line 603

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.i32_load(iovs_ptr + i * 8)
		len = @memory.i32_load(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.i32_store(nwritten_ptr, written)
	ERRNO_SUCCESS
rescue SystemCallError
	ERRNO_IO
end

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



624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
# File 'lib/dewasm/pozeiden/wasm_module.rb', line 624

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.i32_load(iovs_ptr + i * 8)
		len = @memory.i32_load(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.i32_store(nread_ptr, nread)
	ERRNO_SUCCESS
rescue SystemCallError
	ERRNO_IO
end

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



657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
# File 'lib/dewasm/pozeiden/wasm_module.rb', line 657

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.i32_store(bufused_ptr, out.bytesize)
	ERRNO_SUCCESS
rescue SystemCallError
	ERRNO_IO
end

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



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

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.i64_store(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



712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
# File 'lib/dewasm/pozeiden/wasm_module.rb', line 712

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.i32_load(iovs_ptr + i * 8)
		len = @memory.i32_load(iovs_ptr + i * 8 + 4)
		written += io.write(@memory.read_string(ptr, len))
	end
	io.flush
	@memory.i32_store(nwritten_ptr, written)
	ERRNO_SUCCESS
rescue SystemCallError
	ERRNO_IO
end

#wasi_path_create_directory(dirfd, path_ptr, path_len) ⇒ Object



743
744
745
746
747
748
749
750
751
752
753
754
# File 'lib/dewasm/pozeiden/wasm_module.rb', line 743

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



756
757
758
759
760
761
762
763
764
765
766
# File 'lib/dewasm/pozeiden/wasm_module.rb', line 756

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



768
769
770
771
772
773
774
775
776
777
778
779
780
781
# File 'lib/dewasm/pozeiden/wasm_module.rb', line 768

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


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

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



804
805
806
807
# File 'lib/dewasm/pozeiden/wasm_module.rb', line 804

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



956
957
958
959
960
961
962
963
964
# File 'lib/dewasm/pozeiden/wasm_module.rb', line 956

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