1 // Copyright 2023 The Go Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style
3 // license that can be found in the LICENSE file.
14 type uintptr32 = uint32
17 type filesize = uint64
19 type lookupflags = uint32
22 type timestamp = uint64
23 type dircookie = uint64
24 type filedelta = int64
25 type fstflags = uint32
33 LOOKUP_SYMLINK_FOLLOW = 0x00000001
38 OFLAG_DIRECTORY = 0x0002
44 FDFLAG_APPEND = 0x0001
46 FDFLAG_NONBLOCK = 0x0004
52 RIGHT_FD_DATASYNC = 1 << iota
55 RIGHT_FDSTAT_SET_FLAGS
61 RIGHT_PATH_CREATE_DIRECTORY
62 RIGHT_PATH_CREATE_FILE
63 RIGHT_PATH_LINK_SOURCE
64 RIGHT_PATH_LINK_TARGET
68 RIGHT_PATH_RENAME_SOURCE
69 RIGHT_PATH_RENAME_TARGET
70 RIGHT_PATH_FILESTAT_GET
71 RIGHT_PATH_FILESTAT_SET_SIZE
72 RIGHT_PATH_FILESTAT_SET_TIMES
74 RIGHT_FD_FILESTAT_SET_SIZE
75 RIGHT_FD_FILESTAT_SET_TIMES
77 RIGHT_PATH_REMOVE_DIRECTORY
78 RIGHT_PATH_UNLINK_FILE
79 RIGHT_POLL_FD_READWRITE
91 FILESTAT_SET_ATIM = 0x0001
92 FILESTAT_SET_ATIM_NOW = 0x0002
93 FILESTAT_SET_MTIM = 0x0004
94 FILESTAT_SET_MTIM_NOW = 0x0008
98 // Despite the rights being defined as a 64 bits integer in the spec,
99 // wasmtime crashes the program if we set any of the upper 32 bits.
100 fullRights = rights(^uint32(0))
101 readRights = rights(RIGHT_FD_READ | RIGHT_FD_READDIR)
102 writeRights = rights(RIGHT_FD_DATASYNC | RIGHT_FD_WRITE | RIGHT_FD_ALLOCATE | RIGHT_PATH_FILESTAT_SET_SIZE)
104 // Some runtimes have very strict expectations when it comes to which
105 // rights can be enabled on files opened by path_open. The fileRights
106 // constant is used as a mask to retain only bits for operations that
107 // are supported on files.
108 fileRights rights = RIGHT_FD_DATASYNC |
111 RIGHT_FDSTAT_SET_FLAGS |
117 RIGHT_PATH_CREATE_DIRECTORY |
118 RIGHT_PATH_CREATE_FILE |
119 RIGHT_PATH_LINK_SOURCE |
120 RIGHT_PATH_LINK_TARGET |
123 RIGHT_PATH_READLINK |
124 RIGHT_PATH_RENAME_SOURCE |
125 RIGHT_PATH_RENAME_TARGET |
126 RIGHT_PATH_FILESTAT_GET |
127 RIGHT_PATH_FILESTAT_SET_SIZE |
128 RIGHT_PATH_FILESTAT_SET_TIMES |
129 RIGHT_FD_FILESTAT_GET |
130 RIGHT_FD_FILESTAT_SET_SIZE |
131 RIGHT_FD_FILESTAT_SET_TIMES |
133 RIGHT_PATH_REMOVE_DIRECTORY |
134 RIGHT_PATH_UNLINK_FILE |
135 RIGHT_POLL_FD_READWRITE
137 // Runtimes like wasmtime and wasmedge will refuse to open directories
138 // if the rights requested by the application exceed the operations that
139 // can be performed on a directory.
140 dirRights rights = RIGHT_FD_SEEK |
141 RIGHT_FDSTAT_SET_FLAGS |
143 RIGHT_PATH_CREATE_DIRECTORY |
144 RIGHT_PATH_CREATE_FILE |
145 RIGHT_PATH_LINK_SOURCE |
146 RIGHT_PATH_LINK_TARGET |
149 RIGHT_PATH_READLINK |
150 RIGHT_PATH_RENAME_SOURCE |
151 RIGHT_PATH_RENAME_TARGET |
152 RIGHT_PATH_FILESTAT_GET |
153 RIGHT_PATH_FILESTAT_SET_SIZE |
154 RIGHT_PATH_FILESTAT_SET_TIMES |
155 RIGHT_FD_FILESTAT_GET |
156 RIGHT_FD_FILESTAT_SET_TIMES |
158 RIGHT_PATH_REMOVE_DIRECTORY |
159 RIGHT_PATH_UNLINK_FILE
162 // https://github.com/WebAssembly/WASI/blob/a2b96e81c0586125cc4dc79a5be0b78d9a059925/legacy/preview1/docs.md#-fd_closefd-fd---result-errno
164 //go:wasmimport wasi_snapshot_preview1 fd_close
166 func fd_close(fd int32) Errno
168 // https://github.com/WebAssembly/WASI/blob/a2b96e81c0586125cc4dc79a5be0b78d9a059925/legacy/preview1/docs.md#-fd_filestat_set_sizefd-fd-size-filesize---result-errno
170 //go:wasmimport wasi_snapshot_preview1 fd_filestat_set_size
172 func fd_filestat_set_size(fd int32, set_size filesize) Errno
174 // https://github.com/WebAssembly/WASI/blob/a2b96e81c0586125cc4dc79a5be0b78d9a059925/legacy/preview1/docs.md#-fd_preadfd-fd-iovs-iovec_array-offset-filesize---resultsize-errno
176 //go:wasmimport wasi_snapshot_preview1 fd_pread
178 func fd_pread(fd int32, iovs unsafe.Pointer, iovsLen size, offset filesize, nread unsafe.Pointer) Errno
180 //go:wasmimport wasi_snapshot_preview1 fd_pwrite
182 func fd_pwrite(fd int32, iovs unsafe.Pointer, iovsLen size, offset filesize, nwritten unsafe.Pointer) Errno
184 //go:wasmimport wasi_snapshot_preview1 fd_read
186 func fd_read(fd int32, iovs unsafe.Pointer, iovsLen size, nread unsafe.Pointer) Errno
188 //go:wasmimport wasi_snapshot_preview1 fd_readdir
190 func fd_readdir(fd int32, buf unsafe.Pointer, bufLen size, cookie dircookie, nwritten unsafe.Pointer) Errno
192 //go:wasmimport wasi_snapshot_preview1 fd_seek
194 func fd_seek(fd int32, offset filedelta, whence uint32, newoffset unsafe.Pointer) Errno
196 // https://github.com/WebAssembly/WASI/blob/a2b96e81c0586125cc4dc79a5be0b78d9a059925/legacy/preview1/docs.md#-fd_fdstat_set_rightsfd-fd-fs_rights_base-rights-fs_rights_inheriting-rights---result-errno
198 //go:wasmimport wasi_snapshot_preview1 fd_fdstat_set_rights
200 func fd_fdstat_set_rights(fd int32, rightsBase rights, rightsInheriting rights) Errno
202 //go:wasmimport wasi_snapshot_preview1 fd_filestat_get
204 func fd_filestat_get(fd int32, buf unsafe.Pointer) Errno
206 //go:wasmimport wasi_snapshot_preview1 fd_write
208 func fd_write(fd int32, iovs unsafe.Pointer, iovsLen size, nwritten unsafe.Pointer) Errno
210 //go:wasmimport wasi_snapshot_preview1 fd_sync
212 func fd_sync(fd int32) Errno
214 //go:wasmimport wasi_snapshot_preview1 path_create_directory
216 func path_create_directory(fd int32, path unsafe.Pointer, pathLen size) Errno
218 //go:wasmimport wasi_snapshot_preview1 path_filestat_get
220 func path_filestat_get(fd int32, flags lookupflags, path unsafe.Pointer, pathLen size, buf unsafe.Pointer) Errno
222 //go:wasmimport wasi_snapshot_preview1 path_filestat_set_times
224 func path_filestat_set_times(fd int32, flags lookupflags, path unsafe.Pointer, pathLen size, atim timestamp, mtim timestamp, fstflags fstflags) Errno
226 //go:wasmimport wasi_snapshot_preview1 path_link
228 func path_link(oldFd int32, oldFlags lookupflags, oldPath unsafe.Pointer, oldPathLen size, newFd int32, newPath unsafe.Pointer, newPathLen size) Errno
230 //go:wasmimport wasi_snapshot_preview1 path_readlink
232 func path_readlink(fd int32, path unsafe.Pointer, pathLen size, buf unsafe.Pointer, bufLen size, nwritten unsafe.Pointer) Errno
234 //go:wasmimport wasi_snapshot_preview1 path_remove_directory
236 func path_remove_directory(fd int32, path unsafe.Pointer, pathLen size) Errno
238 //go:wasmimport wasi_snapshot_preview1 path_rename
240 func path_rename(oldFd int32, oldPath unsafe.Pointer, oldPathLen size, newFd int32, newPath unsafe.Pointer, newPathLen size) Errno
242 //go:wasmimport wasi_snapshot_preview1 path_symlink
244 func path_symlink(oldPath unsafe.Pointer, oldPathLen size, fd int32, newPath unsafe.Pointer, newPathLen size) Errno
246 //go:wasmimport wasi_snapshot_preview1 path_unlink_file
248 func path_unlink_file(fd int32, path unsafe.Pointer, pathLen size) Errno
250 //go:wasmimport wasi_snapshot_preview1 path_open
252 func path_open(rootFD int32, dirflags lookupflags, path unsafe.Pointer, pathLen size, oflags oflags, fsRightsBase rights, fsRightsInheriting rights, fsFlags fdflags, fd unsafe.Pointer) Errno
254 //go:wasmimport wasi_snapshot_preview1 random_get
256 func random_get(buf unsafe.Pointer, bufLen size) Errno
258 // https://github.com/WebAssembly/WASI/blob/a2b96e81c0586125cc4dc79a5be0b78d9a059925/legacy/preview1/docs.md#-fdstat-record
259 // fdflags must be at offset 2, hence the uint16 type rather than the
260 // fdflags (uint32) type.
265 rightsInheriting rights
268 //go:wasmimport wasi_snapshot_preview1 fd_fdstat_get
270 func fd_fdstat_get(fd int32, buf unsafe.Pointer) Errno
272 //go:wasmimport wasi_snapshot_preview1 fd_fdstat_set_flags
274 func fd_fdstat_set_flags(fd int32, flags fdflags) Errno
276 func fd_fdstat_get_flags(fd int) (uint32, error) {
278 errno := fd_fdstat_get(int32(fd), unsafe.Pointer(&stat))
279 return uint32(stat.fdflags), errnoErr(errno)
282 type preopentype = uint8
285 preopentypeDir preopentype = iota
288 type prestatDir struct {
292 type prestat struct {
297 //go:wasmimport wasi_snapshot_preview1 fd_prestat_get
299 func fd_prestat_get(fd int32, prestat unsafe.Pointer) Errno
301 //go:wasmimport wasi_snapshot_preview1 fd_prestat_dir_name
303 func fd_prestat_dir_name(fd int32, path unsafe.Pointer, pathLen size) Errno
305 type opendir struct {
310 // List of preopen directories that were exposed by the runtime. The first one
311 // is assumed to the be root directory of the file system, and others are seen
312 // as mount points at sub paths of the root.
313 var preopens []opendir
315 // Current working directory. We maintain this as a string and resolve paths in
316 // the code because wasmtime does not allow relative path lookups outside of the
317 // scope of a directory; a previous approach we tried consisted in maintaining
318 // open a file descriptor to the current directory so we could perform relative
319 // path lookups from that location, but it resulted in breaking path resolution
320 // from the current directory to its parent.
324 dirNameBuf := make([]byte, 256)
325 // We start looking for preopens at fd=3 because 0, 1, and 2 are reserved
326 // for standard input and outputs.
327 for preopenFd := int32(3); ; preopenFd++ {
330 errno := fd_prestat_get(preopenFd, unsafe.Pointer(&prestat))
335 panic("fd_prestat: " + errno.Error())
337 if prestat.typ != preopentypeDir {
340 if int(prestat.dir.prNameLen) > len(dirNameBuf) {
341 dirNameBuf = make([]byte, prestat.dir.prNameLen)
344 errno = fd_prestat_dir_name(preopenFd, unsafe.Pointer(&dirNameBuf[0]), prestat.dir.prNameLen)
346 panic("fd_prestat_dir_name: " + errno.Error())
349 preopens = append(preopens, opendir{
351 name: string(dirNameBuf[:prestat.dir.prNameLen]),
355 if cwd, _ = Getenv("PWD"); cwd != "" {
356 cwd = joinPath("/", cwd)
357 } else if len(preopens) > 0 {
358 cwd = preopens[0].name
362 // Provided by package runtime.
363 func now() (sec int64, nsec int32)
366 func appendCleanPath(buf []byte, path string, lookupParent bool) ([]byte, bool) {
369 for i < len(path) && path[i] == '/' {
374 for j < len(path) && path[j] != '/' {
389 for k > 0 && buf[k-1] != '/' {
392 for k > 1 && buf[k-1] == '/' {
407 if len(buf) > 0 && buf[len(buf)-1] != '/' {
408 buf = append(buf, '/')
410 buf = append(buf, s...)
412 return buf, lookupParent
415 // joinPath concatenates dir and file paths, producing a cleaned path where
416 // "." and ".." have been removed, unless dir is relative and the references
417 // to parent directories in file represented a location relative to a parent
420 // This function is used for path resolution of all wasi functions expecting
421 // a path argument; the returned string is heap allocated, which we may want
422 // to optimize in the future. Instead of returning a string, the function
423 // could append the result to an output buffer that the functions in this
424 // file can manage to have allocated on the stack (e.g. initializing to a
425 // fixed capacity). Since it will significantly increase code complexity,
426 // we prefer to optimize for readability and maintainability at this time.
427 func joinPath(dir, file string) string {
428 buf := make([]byte, 0, len(dir)+len(file)+1)
430 buf = append(buf, '/')
432 buf, lookupParent := appendCleanPath(buf, dir, false)
433 buf, _ = appendCleanPath(buf, file, lookupParent)
434 // The appendCleanPath function cleans the path so it does not inject
435 // references to the current directory. If both the dir and file args
436 // were ".", this results in the output buffer being empty so we handle
437 // this condition here.
439 buf = append(buf, '.')
441 // If the file ended with a '/' we make sure that the output also ends
442 // with a '/'. This is needed to ensure that programs have a mechanism
443 // to represent dereferencing symbolic links pointing to directories.
444 if buf[len(buf)-1] != '/' && isDir(file) {
445 buf = append(buf, '/')
447 return unsafe.String(&buf[0], len(buf))
450 func isAbs(path string) bool {
451 return hasPrefix(path, "/")
454 func isDir(path string) bool {
455 return hasSuffix(path, "/")
458 func hasPrefix(s, p string) bool {
459 return len(s) >= len(p) && s[:len(p)] == p
462 func hasSuffix(s, x string) bool {
463 return len(s) >= len(x) && s[len(s)-len(x):] == x
466 // preparePath returns the preopen file descriptor of the directory to perform
467 // path resolution from, along with the pair of pointer and length for the
468 // relative expression of path from the directory.
470 // If the path argument is not absolute, it is first appended to the current
471 // working directory before resolution.
472 func preparePath(path string) (int32, unsafe.Pointer, size) {
473 var dirFd = int32(-1)
480 path = joinPath(dir, path)
482 for _, p := range preopens {
483 if len(p.name) > len(dirName) && hasPrefix(path, p.name) {
484 dirFd, dirName = p.fd, p.name
488 path = path[len(dirName):]
496 return dirFd, stringPointer(path), size(len(path))
499 func Open(path string, openmode int, perm uint32) (int, error) {
503 dirFd, pathPtr, pathLen := preparePath(path)
506 if (openmode & O_CREATE) != 0 {
507 oflags |= OFLAG_CREATE
509 if (openmode & O_TRUNC) != 0 {
510 oflags |= OFLAG_TRUNC
512 if (openmode & O_EXCL) != 0 {
517 switch openmode & (O_RDONLY | O_WRONLY | O_RDWR) {
519 rights = fileRights & ^writeRights
521 rights = fileRights & ^readRights
527 if (openmode & O_APPEND) != 0 {
528 fdflags |= FDFLAG_APPEND
530 if (openmode & O_SYNC) != 0 {
531 fdflags |= FDFLAG_SYNC
537 LOOKUP_SYMLINK_FOLLOW,
546 if errno == EISDIR && oflags == 0 && fdflags == 0 && ((rights & writeRights) == 0) {
547 // wasmtime and wasmedge will error if attempting to open a directory
548 // because we are asking for too many rights. However, we cannot
549 // determine ahread of time if the path we are about to open is a
550 // directory, so instead we fallback to a second call to path_open with
551 // a more limited set of rights.
553 // This approach is subject to a race if the file system is modified
554 // concurrently, so we also inject OFLAG_DIRECTORY to ensure that we do
555 // not accidentally open a file which is not a directory.
558 LOOKUP_SYMLINK_FOLLOW,
561 oflags|OFLAG_DIRECTORY,
568 return int(fd), errnoErr(errno)
571 func Close(fd int) error {
572 errno := fd_close(int32(fd))
573 return errnoErr(errno)
576 func CloseOnExec(fd int) {
577 // nothing to do - no exec
580 func Mkdir(path string, perm uint32) error {
584 dirFd, pathPtr, pathLen := preparePath(path)
585 errno := path_create_directory(dirFd, pathPtr, pathLen)
586 return errnoErr(errno)
589 func ReadDir(fd int, buf []byte, cookie dircookie) (int, error) {
591 errno := fd_readdir(int32(fd), unsafe.Pointer(&buf[0]), size(len(buf)), cookie, unsafe.Pointer(&nwritten))
592 return int(nwritten), errnoErr(errno)
607 // Uid and Gid are always zero on wasip1 platforms
612 func Stat(path string, st *Stat_t) error {
616 dirFd, pathPtr, pathLen := preparePath(path)
617 errno := path_filestat_get(dirFd, LOOKUP_SYMLINK_FOLLOW, pathPtr, pathLen, unsafe.Pointer(st))
619 return errnoErr(errno)
622 func Lstat(path string, st *Stat_t) error {
626 dirFd, pathPtr, pathLen := preparePath(path)
627 errno := path_filestat_get(dirFd, 0, pathPtr, pathLen, unsafe.Pointer(st))
629 return errnoErr(errno)
632 func Fstat(fd int, st *Stat_t) error {
633 errno := fd_filestat_get(int32(fd), unsafe.Pointer(st))
635 return errnoErr(errno)
638 func setDefaultMode(st *Stat_t) {
639 // WASI does not support unix-like permissions, but Go programs are likely
640 // to expect the permission bits to not be zero so we set defaults to help
641 // avoid breaking applications that are migrating to WASM.
642 if st.Filetype == FILETYPE_DIRECTORY {
649 func Unlink(path string) error {
653 dirFd, pathPtr, pathLen := preparePath(path)
654 errno := path_unlink_file(dirFd, pathPtr, pathLen)
655 return errnoErr(errno)
658 func Rmdir(path string) error {
662 dirFd, pathPtr, pathLen := preparePath(path)
663 errno := path_remove_directory(dirFd, pathPtr, pathLen)
664 return errnoErr(errno)
667 func Chmod(path string, mode uint32) error {
669 return Stat(path, &stat)
672 func Fchmod(fd int, mode uint32) error {
674 return Fstat(fd, &stat)
677 func Chown(path string, uid, gid int) error {
681 func Fchown(fd int, uid, gid int) error {
685 func Lchown(path string, uid, gid int) error {
689 func UtimesNano(path string, ts []Timespec) error {
690 // UTIME_OMIT value must match internal/syscall/unix/at_wasip1.go
691 const UTIME_OMIT = -0x2
695 dirFd, pathPtr, pathLen := preparePath(path)
696 atime := TimespecToNsec(ts[0])
697 mtime := TimespecToNsec(ts[1])
698 if ts[0].Nsec == UTIME_OMIT || ts[1].Nsec == UTIME_OMIT {
700 if err := Stat(path, &st); err != nil {
703 if ts[0].Nsec == UTIME_OMIT {
704 atime = int64(st.Atime)
706 if ts[1].Nsec == UTIME_OMIT {
707 mtime = int64(st.Mtime)
710 errno := path_filestat_set_times(
712 LOOKUP_SYMLINK_FOLLOW,
717 FILESTAT_SET_ATIM|FILESTAT_SET_MTIM,
719 return errnoErr(errno)
722 func Rename(from, to string) error {
723 if from == "" || to == "" {
726 oldDirFd, oldPathPtr, oldPathLen := preparePath(from)
727 newDirFd, newPathPtr, newPathLen := preparePath(to)
728 errno := path_rename(
736 return errnoErr(errno)
739 func Truncate(path string, length int64) error {
743 fd, err := Open(path, O_WRONLY, 0)
748 return Ftruncate(fd, length)
751 func Ftruncate(fd int, length int64) error {
752 errno := fd_filestat_set_size(int32(fd), filesize(length))
753 return errnoErr(errno)
756 const ImplementsGetwd = true
758 func Getwd() (string, error) {
762 func Chdir(path string) error {
771 path = joinPath(dir, path)
774 dirFd, pathPtr, pathLen := preparePath(path)
775 errno := path_filestat_get(dirFd, LOOKUP_SYMLINK_FOLLOW, pathPtr, pathLen, unsafe.Pointer(&stat))
777 return errnoErr(errno)
779 if stat.Filetype != FILETYPE_DIRECTORY {
786 func Readlink(path string, buf []byte) (n int, err error) {
793 dirFd, pathPtr, pathLen := preparePath(path)
795 errno := path_readlink(
799 unsafe.Pointer(&buf[0]),
801 unsafe.Pointer(&nwritten),
803 // For some reason wasmtime returns ERANGE when the output buffer is
804 // shorter than the symbolic link value. os.Readlink expects a nil
805 // error and uses the fact that n is greater or equal to the buffer
806 // length to assume that it needs to try again with a larger size.
807 // This condition is handled in os.Readlink.
808 return int(nwritten), errnoErr(errno)
811 func Link(path, link string) error {
812 if path == "" || link == "" {
815 oldDirFd, oldPathPtr, oldPathLen := preparePath(path)
816 newDirFd, newPathPtr, newPathLen := preparePath(link)
826 return errnoErr(errno)
829 func Symlink(path, link string) error {
830 if path == "" || link == "" {
833 dirFd, pathPtr, pathlen := preparePath(link)
834 errno := path_symlink(
841 return errnoErr(errno)
844 func Fsync(fd int) error {
845 errno := fd_sync(int32(fd))
846 return errnoErr(errno)
849 func bytesPointer(b []byte) unsafe.Pointer {
850 return unsafe.Pointer(unsafe.SliceData(b))
853 func stringPointer(s string) unsafe.Pointer {
854 return unsafe.Pointer(unsafe.StringData(s))
857 func makeIOVec(b []byte) unsafe.Pointer {
858 return unsafe.Pointer(&iovec{
859 buf: uintptr32(uintptr(bytesPointer(b))),
860 bufLen: size(len(b)),
864 func Read(fd int, b []byte) (int, error) {
866 errno := fd_read(int32(fd), makeIOVec(b), 1, unsafe.Pointer(&nread))
868 return int(nread), errnoErr(errno)
871 func Write(fd int, b []byte) (int, error) {
873 errno := fd_write(int32(fd), makeIOVec(b), 1, unsafe.Pointer(&nwritten))
875 return int(nwritten), errnoErr(errno)
878 func Pread(fd int, b []byte, offset int64) (int, error) {
880 errno := fd_pread(int32(fd), makeIOVec(b), 1, filesize(offset), unsafe.Pointer(&nread))
882 return int(nread), errnoErr(errno)
885 func Pwrite(fd int, b []byte, offset int64) (int, error) {
887 errno := fd_pwrite(int32(fd), makeIOVec(b), 1, filesize(offset), unsafe.Pointer(&nwritten))
889 return int(nwritten), errnoErr(errno)
892 func Seek(fd int, offset int64, whence int) (int64, error) {
893 var newoffset filesize
894 errno := fd_seek(int32(fd), filedelta(offset), uint32(whence), unsafe.Pointer(&newoffset))
895 return int64(newoffset), errnoErr(errno)
898 func Dup(fd int) (int, error) {
902 func Dup2(fd, newfd int) error {
906 func Pipe(fd []int) error {
910 func RandomGet(b []byte) error {
911 errno := random_get(bytesPointer(b), size(len(b)))
912 return errnoErr(errno)