// Copyright 2021 The Emscripten Authors. All rights reserved. // Emscripten is available under two separate licenses, the MIT license and the // University of Illinois/NCSA Open Source License. Both these licenses can be // found in the LICENSE file. // Syscall implementations. #define _LARGEFILE64_SOURCE // For F_GETLK64 etc #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "backend.h" #include "file.h" #include "file_table.h" #include "paths.h" #include "pipe_backend.h" #include "special_files.h" #include "wasmfs.h" // File permission macros for wasmfs. // Used to improve readability compared to those in stat.h #define WASMFS_PERM_READ 0444 #define WASMFS_PERM_WRITE 0222 #define WASMFS_PERM_EXECUTE 0111 // In Linux, the maximum length for a filename is 255 bytes. #define WASMFS_NAME_MAX 255 extern "C" { using namespace wasmfs; int __syscall_dup3(int oldfd, int newfd, int flags) { if (flags & ~O_CLOEXEC) { return -EINVAL; } if (oldfd == newfd) { return -EINVAL; } auto fileTable = wasmFS.getFileTable().locked(); auto oldOpenFile = fileTable.getEntry(oldfd); if (!oldOpenFile) { return -EBADF; } if (newfd < 0 || newfd >= WASMFS_FD_MAX) { return -EBADF; } // If the file descriptor newfd was previously open, it will just be // overwritten silently. (void)fileTable.setEntry(newfd, oldOpenFile); return newfd; } int __syscall_dup(int fd) { auto fileTable = wasmFS.getFileTable().locked(); // Check that an open file exists corresponding to the given fd. auto openFile = fileTable.getEntry(fd); if (!openFile) { return -EBADF; } return fileTable.addEntry(openFile); } // This enum specifies whether file offset will be provided by the open file // state or provided by argument in the case of pread or pwrite. enum class OffsetHandling { OpenFileState, Argument }; // Internal write function called by __wasi_fd_write and __wasi_fd_pwrite // Receives an open file state offset. // Optionally sets open file state offset. static __wasi_errno_t writeAtOffset(OffsetHandling setOffset, __wasi_fd_t fd, const __wasi_ciovec_t* iovs, size_t iovs_len, __wasi_size_t* nwritten, __wasi_filesize_t offset = 0) { auto openFile = wasmFS.getFileTable().locked().getEntry(fd); if (!openFile) { return __WASI_ERRNO_BADF; } if (iovs_len < 0 || offset < 0) { return __WASI_ERRNO_INVAL; } auto lockedOpenFile = openFile->locked(); auto file = lockedOpenFile.getFile()->dynCast(); if (!file) { return __WASI_ERRNO_ISDIR; } auto lockedFile = file->locked(); if (setOffset == OffsetHandling::OpenFileState) { if (lockedOpenFile.getFlags() & O_APPEND) { off_t size = lockedFile.getSize(); if (size < 0) { // Translate to WASI standard of positive return codes. return -size; } offset = size; lockedOpenFile.setPosition(offset); } else { offset = lockedOpenFile.getPosition(); } } // TODO: Check open file access mode for write permissions. size_t bytesWritten = 0; for (size_t i = 0; i < iovs_len; i++) { const uint8_t* buf = iovs[i].buf; off_t len = iovs[i].buf_len; // Check if buf_len specifies a positive length buffer but buf is a // null pointer if (!buf && len > 0) { return __WASI_ERRNO_INVAL; } // Check if the sum of the buf_len values overflows an off_t (63 bits). if (addWillOverFlow(offset, (__wasi_filesize_t)bytesWritten)) { return __WASI_ERRNO_FBIG; } auto result = lockedFile.write(buf, len, offset + bytesWritten); if (result < 0) { // This individual write failed. Report the error unless we've already // written some bytes, in which case report a successful short write. if (bytesWritten > 0) { break; } return -result; } // The write was successful. bytesWritten += result; if (result < len) { // The write was short, so stop here. break; } } *nwritten = bytesWritten; if (setOffset == OffsetHandling::OpenFileState && lockedOpenFile.getFile()->isSeekable()) { lockedOpenFile.setPosition(offset + bytesWritten); } if (bytesWritten) { lockedFile.updateMTime(); } return __WASI_ERRNO_SUCCESS; } // Internal read function called by __wasi_fd_read and __wasi_fd_pread // Receives an open file state offset. // Optionally sets open file state offset. // TODO: combine this with writeAtOffset because the code is nearly identical. static __wasi_errno_t readAtOffset(OffsetHandling setOffset, __wasi_fd_t fd, const __wasi_iovec_t* iovs, size_t iovs_len, __wasi_size_t* nread, __wasi_filesize_t offset = 0) { auto openFile = wasmFS.getFileTable().locked().getEntry(fd); if (!openFile) { return __WASI_ERRNO_BADF; } auto lockedOpenFile = openFile->locked(); if (setOffset == OffsetHandling::OpenFileState) { offset = lockedOpenFile.getPosition(); } if (iovs_len < 0 || offset < 0) { return __WASI_ERRNO_INVAL; } // TODO: Check open file access mode for read permissions. auto file = lockedOpenFile.getFile()->dynCast(); // If file is nullptr, then the file was not a DataFile. if (!file) { return __WASI_ERRNO_ISDIR; } auto lockedFile = file->locked(); size_t bytesRead = 0; for (size_t i = 0; i < iovs_len; i++) { uint8_t* buf = iovs[i].buf; size_t len = iovs[i].buf_len; if (!buf && len > 0) { return __WASI_ERRNO_INVAL; } // TODO: Check for overflow when adding offset + bytesRead. auto result = lockedFile.read(buf, len, offset + bytesRead); if (result < 0) { // This individual read failed. Report the error unless we've already read // some bytes, in which case report a successful short read. if (bytesRead > 0) { break; } return -result; } // The read was successful. // Backends must only return len or less. assert(result <= len); bytesRead += result; if (result < len) { // The read was short, so stop here. break; } } *nread = bytesRead; if (setOffset == OffsetHandling::OpenFileState && lockedOpenFile.getFile()->isSeekable()) { lockedOpenFile.setPosition(offset + bytesRead); } return __WASI_ERRNO_SUCCESS; } __wasi_errno_t __wasi_fd_write(__wasi_fd_t fd, const __wasi_ciovec_t* iovs, size_t iovs_len, __wasi_size_t* nwritten) { return writeAtOffset( OffsetHandling::OpenFileState, fd, iovs, iovs_len, nwritten); } __wasi_errno_t __wasi_fd_read(__wasi_fd_t fd, const __wasi_iovec_t* iovs, size_t iovs_len, __wasi_size_t* nread) { return readAtOffset(OffsetHandling::OpenFileState, fd, iovs, iovs_len, nread); } __wasi_errno_t __wasi_fd_pwrite(__wasi_fd_t fd, const __wasi_ciovec_t* iovs, size_t iovs_len, __wasi_filesize_t offset, __wasi_size_t* nwritten) { return writeAtOffset( OffsetHandling::Argument, fd, iovs, iovs_len, nwritten, offset); } __wasi_errno_t __wasi_fd_pread(__wasi_fd_t fd, const __wasi_iovec_t* iovs, size_t iovs_len, __wasi_filesize_t offset, __wasi_size_t* nread) { return readAtOffset( OffsetHandling::Argument, fd, iovs, iovs_len, nread, offset); } __wasi_errno_t __wasi_fd_close(__wasi_fd_t fd) { std::shared_ptr closee; { // Do not hold the file table lock while performing the close. auto fileTable = wasmFS.getFileTable().locked(); auto entry = fileTable.getEntry(fd); if (!entry) { return __WASI_ERRNO_BADF; } closee = fileTable.setEntry(fd, nullptr); } if (closee) { // Translate to WASI standard of positive return codes. int ret = -closee->locked().close(); assert(ret >= 0); return ret; } return __WASI_ERRNO_SUCCESS; } __wasi_errno_t __wasi_fd_sync(__wasi_fd_t fd) { auto openFile = wasmFS.getFileTable().locked().getEntry(fd); if (!openFile) { return __WASI_ERRNO_BADF; } // Nothing to flush for anything but a data file, but also not an error either // way. TODO: in the future we may want syncing of directories. auto dataFile = openFile->locked().getFile()->dynCast(); if (dataFile) { auto ret = dataFile->locked().flush(); assert(ret <= 0); // Translate to WASI standard of positive return codes. return -ret; } return __WASI_ERRNO_SUCCESS; } int __syscall_fdatasync(int fd) { // TODO: Optimize this to avoid unnecessarily flushing unnecessary metadata. return __wasi_fd_sync(fd); } backend_t wasmfs_get_backend_by_fd(int fd) { auto openFile = wasmFS.getFileTable().locked().getEntry(fd); if (!openFile) { return NullBackend; } return openFile->locked().getFile()->getBackend(); } // This function is exposed to users to allow them to obtain a backend_t for a // specified path. backend_t wasmfs_get_backend_by_path(const char* path) { auto parsed = path::parseFile(path); if (parsed.getError()) { // Could not find the file. return NullBackend; } return parsed.getFile()->getBackend(); } static timespec ms_to_timespec(double ms) { long long seconds = ms / 1000; timespec ts; ts.tv_sec = seconds; // seconds ts.tv_nsec = (ms - (seconds * 1000)) * 1000 * 1000; // nanoseconds return ts; } int __syscall_newfstatat(int dirfd, intptr_t path, intptr_t buf, int flags) { // Only accept valid flags. if (flags & ~(AT_EMPTY_PATH | AT_NO_AUTOMOUNT | AT_SYMLINK_NOFOLLOW)) { // TODO: Test this case. return -EINVAL; } auto parsed = path::getFileAt(dirfd, (char*)path, flags); if (auto err = parsed.getError()) { return err; } auto file = parsed.getFile(); // Extract the information from the file. auto lockedFile = file->locked(); auto buffer = (struct stat*)buf; off_t size = lockedFile.getSize(); if (size < 0) { return size; } buffer->st_size = size; // ATTN: hard-coded constant values are copied from the existing JS file // system. Specific values were chosen to match existing library_fs.js // values. // ID of device containing file: Hardcode 1 for now, no meaning at the // moment for Emscripten. buffer->st_dev = 1; buffer->st_mode = lockedFile.getMode(); buffer->st_ino = file->getIno(); // The number of hard links is 1 since they are unsupported. buffer->st_nlink = 1; buffer->st_uid = 0; buffer->st_gid = 0; // Device ID (if special file) No meaning right now for Emscripten. buffer->st_rdev = 0; // The syscall docs state this is hardcoded to # of 512 byte blocks. buffer->st_blocks = (buffer->st_size + 511) / 512; // Specifies the preferred blocksize for efficient disk I/O. buffer->st_blksize = 4096; buffer->st_atim = ms_to_timespec(lockedFile.getATime()); buffer->st_mtim = ms_to_timespec(lockedFile.getMTime()); buffer->st_ctim = ms_to_timespec(lockedFile.getCTime()); return __WASI_ERRNO_SUCCESS; } int __syscall_stat64(intptr_t path, intptr_t buf) { return __syscall_newfstatat(AT_FDCWD, path, buf, 0); } int __syscall_lstat64(intptr_t path, intptr_t buf) { return __syscall_newfstatat(AT_FDCWD, path, buf, AT_SYMLINK_NOFOLLOW); } int __syscall_fstat64(int fd, intptr_t buf) { return __syscall_newfstatat(fd, (intptr_t) "", buf, AT_EMPTY_PATH); } // When calling doOpen(), we may request an FD be returned, or we may not need // that return value (in which case no FD need be allocated, and we return 0 on // success). enum class OpenReturnMode { FD, Nothing }; static __wasi_fd_t doOpen(path::ParsedParent parsed, int flags, mode_t mode, backend_t backend = NullBackend, OpenReturnMode returnMode = OpenReturnMode::FD) { int accessMode = (flags & O_ACCMODE); if (accessMode != O_WRONLY && accessMode != O_RDONLY && accessMode != O_RDWR) { return -EINVAL; } // TODO: remove assert when all functionality is complete. assert((flags & ~(O_CREAT | O_EXCL | O_DIRECTORY | O_TRUNC | O_APPEND | O_RDWR | O_WRONLY | O_RDONLY | O_LARGEFILE | O_NOFOLLOW | O_CLOEXEC | O_NONBLOCK)) == 0); if (auto err = parsed.getError()) { return err; } auto& [parent, childName] = parsed.getParentChild(); if (childName.size() > WASMFS_NAME_MAX) { return -ENAMETOOLONG; } std::shared_ptr child; { auto lockedParent = parent->locked(); child = lockedParent.getChild(std::string(childName)); // The requested node was not found. if (!child) { // If curr is the last element and the create flag is specified // If O_DIRECTORY is also specified, still create a regular file: // https://man7.org/linux/man-pages/man2/open.2.html#BUGS if (!(flags & O_CREAT)) { return -ENOENT; } // Inserting into an unlinked directory is not allowed. if (!lockedParent.getParent()) { return -ENOENT; } // Mask out everything except the permissions bits. mode &= S_IALLUGO; mode &= ~wasmFS.getUmask(); // If there is no explicitly provided backend, use the parent's backend. if (!backend) { backend = parent->getBackend(); } // TODO: Check write permissions on the parent directory. std::shared_ptr created; if (backend == parent->getBackend()) { created = lockedParent.insertDataFile(std::string(childName), mode); if (!created) { // TODO Receive a specific error code, and report it here. For now, // report a generic error. return -EIO; } } else { created = backend->createFile(mode); if (!created) { // TODO Receive a specific error code, and report it here. For now, // report a generic error. return -EIO; } [[maybe_unused]] bool mounted = lockedParent.mountChild(std::string(childName), created); assert(mounted); } // TODO: Check that the insert actually succeeds. if (returnMode == OpenReturnMode::Nothing) { return 0; } std::shared_ptr openFile; if (auto err = OpenFileState::create(created, flags, openFile)) { assert(err < 0); return err; } return wasmFS.getFileTable().locked().addEntry(openFile); } } if (auto link = child->dynCast()) { if (flags & O_NOFOLLOW) { return -ELOOP; } // TODO: The link dereference count starts back at 0 here. We could // propagate it from the previous path parsing instead. auto target = link->getTarget(); auto parsedLink = path::getFileFrom(parent, target); if (auto err = parsedLink.getError()) { return err; } child = parsedLink.getFile(); } assert(!child->is()); // Return an error if the file exists and O_CREAT and O_EXCL are specified. if ((flags & O_EXCL) && (flags & O_CREAT)) { return -EEXIST; } if (child->is() && (accessMode != O_RDONLY || (flags & O_CREAT))) { return -EISDIR; } // Check user permissions. auto fileMode = child->locked().getMode(); if ((accessMode == O_RDONLY || accessMode == O_RDWR) && !(fileMode & WASMFS_PERM_READ)) { return -EACCES; } if ((accessMode == O_WRONLY || accessMode == O_RDWR) && !(fileMode & WASMFS_PERM_WRITE)) { return -EACCES; } // Fail if O_DIRECTORY is specified and pathname is not a directory if (flags & O_DIRECTORY && !child->is()) { return -ENOTDIR; } // Note that we open the file before truncating it because some backends may // truncate opened files more efficiently (e.g. OPFS). std::shared_ptr openFile; if (auto err = OpenFileState::create(child, flags, openFile)) { assert(err < 0); return err; } // If O_TRUNC, truncate the file if possible. if (flags & O_TRUNC) { if (!child->is()) { return -EISDIR; } if ((fileMode & WASMFS_PERM_WRITE) == 0) { return -EACCES; } // Try to truncate the file, continuing silently if we cannot. (void)child->cast()->locked().setSize(0); } return wasmFS.getFileTable().locked().addEntry(openFile); } // This function is exposed to users and allows users to create a file in a // specific backend. An fd to an open file is returned. int wasmfs_create_file(char* pathname, mode_t mode, backend_t backend) { static_assert(std::is_same_v, "unexpected conversion from result of doOpen to int"); return doOpen( path::parseParent((char*)pathname), O_CREAT | O_EXCL, mode, backend); } // TODO: Test this with non-AT_FDCWD values. int __syscall_openat(int dirfd, intptr_t path, int flags, ...) { mode_t mode = 0; va_list v1; va_start(v1, flags); mode = va_arg(v1, int); va_end(v1); return doOpen(path::parseParent((char*)path, dirfd), flags, mode); } int __syscall_mknodat(int dirfd, intptr_t path, int mode, int dev) { assert(dev == 0); // TODO: support special devices if (mode & S_IFDIR) { return -EINVAL; } if (mode & S_IFIFO) { return -EPERM; } return doOpen(path::parseParent((char*)path, dirfd), O_CREAT | O_EXCL, mode, NullBackend, OpenReturnMode::Nothing); } static int doMkdir(path::ParsedParent parsed, int mode, backend_t backend = NullBackend) { if (auto err = parsed.getError()) { return err; } auto& [parent, childNameView] = parsed.getParentChild(); std::string childName(childNameView); auto lockedParent = parent->locked(); if (childName.size() > WASMFS_NAME_MAX) { return -ENAMETOOLONG; } // Check if the requested directory already exists. if (lockedParent.getChild(childName)) { return -EEXIST; } // Mask rwx permissions for user, group and others, and the sticky bit. // This prevents users from entering S_IFREG for example. // https://www.gnu.org/software/libc/manual/html_node/Permission-Bits.html mode &= S_IRWXUGO | S_ISVTX; mode &= ~wasmFS.getUmask(); if (!(lockedParent.getMode() & WASMFS_PERM_WRITE)) { return -EACCES; } // By default, the backend that the directory is created in is the same as // the parent directory. However, if a backend is passed as a parameter, // then that backend is used. if (!backend) { backend = parent->getBackend(); } if (backend == parent->getBackend()) { if (!lockedParent.insertDirectory(childName, mode)) { // TODO Receive a specific error code, and report it here. For now, report // a generic error. return -EIO; } } else { auto created = backend->createDirectory(mode); if (!created) { // TODO Receive a specific error code, and report it here. For now, report // a generic error. return -EIO; } [[maybe_unused]] bool mounted = lockedParent.mountChild(childName, created); assert(mounted); } // TODO: Check that the insertion is successful. return 0; } // This function is exposed to users and allows users to specify a particular // backend that a directory should be created within. int wasmfs_create_directory(char* path, int mode, backend_t backend) { static_assert(std::is_same_v, "unexpected conversion from result of doMkdir to int"); return doMkdir(path::parseParent(path), mode, backend); } // TODO: Test this. int __syscall_mkdirat(int dirfd, intptr_t path, int mode) { return doMkdir(path::parseParent((char*)path, dirfd), mode); } int __syscall_umask(int mask) { mode_t old = wasmFS.getUmask(); wasmFS.setUmask(mask); return old; } __wasi_errno_t __wasi_fd_seek(__wasi_fd_t fd, __wasi_filedelta_t offset, __wasi_whence_t whence, __wasi_filesize_t* newoffset) { auto openFile = wasmFS.getFileTable().locked().getEntry(fd); if (!openFile) { return __WASI_ERRNO_BADF; } auto lockedOpenFile = openFile->locked(); if (!lockedOpenFile.getFile()->isSeekable()) { return __WASI_ERRNO_SPIPE; } off_t position; if (whence == SEEK_SET) { position = offset; } else if (whence == SEEK_CUR) { position = lockedOpenFile.getPosition() + offset; } else if (whence == SEEK_END) { // Only the open file state is altered in seek. Locking the underlying // data file here once is sufficient. off_t size = lockedOpenFile.getFile()->locked().getSize(); if (size < 0) { // Translate to WASI standard of positive return codes. return -size; } position = size + offset; } else { return __WASI_ERRNO_INVAL; } if (position < 0) { return __WASI_ERRNO_INVAL; } lockedOpenFile.setPosition(position); if (newoffset) { *newoffset = position; } return __WASI_ERRNO_SUCCESS; } static int doChdir(std::shared_ptr& file) { auto dir = file->dynCast(); if (!dir) { return -ENOTDIR; } wasmFS.setCWD(dir); return 0; } int __syscall_chdir(intptr_t path) { auto parsed = path::parseFile((char*)path); if (auto err = parsed.getError()) { return err; } return doChdir(parsed.getFile()); } int __syscall_fchdir(int fd) { auto openFile = wasmFS.getFileTable().locked().getEntry(fd); if (!openFile) { return -EBADF; } return doChdir(openFile->locked().getFile()); } int __syscall_getcwd(intptr_t buf, size_t size) { // Check if buf points to a bad address. if (!buf && size > 0) { return -EFAULT; } // Check if the size argument is zero and buf is not a null pointer. if (buf && size == 0) { return -EINVAL; } auto curr = wasmFS.getCWD(); std::string result = ""; while (curr != wasmFS.getRootDirectory()) { auto parent = curr->locked().getParent(); // Check if the parent exists. The parent may not exist if the CWD or one // of its ancestors has been unlinked. if (!parent) { return -ENOENT; } auto name = parent->locked().getName(curr); result = '/' + name + result; curr = parent; } // Check if the cwd is the root directory. if (result.empty()) { result = "/"; } int len = result.length() + 1; // Check if the size argument is less than the length of the absolute // pathname of the working directory, including null terminator. if (len > size) { return -ERANGE; } // Return value is a null-terminated c string. strcpy((char*)buf, result.c_str()); return len; } __wasi_errno_t __wasi_fd_fdstat_get(__wasi_fd_t fd, __wasi_fdstat_t* stat) { // TODO: This is only partial implementation of __wasi_fd_fdstat_get. Enough // to get __wasi_fd_is_valid working. // There are other fields in the stat structure that we should really // be filling in here. auto openFile = wasmFS.getFileTable().locked().getEntry(fd); if (!openFile) { return __WASI_ERRNO_BADF; } if (openFile->locked().getFile()->is()) { stat->fs_filetype = __WASI_FILETYPE_DIRECTORY; } else { stat->fs_filetype = __WASI_FILETYPE_REGULAR_FILE; } return __WASI_ERRNO_SUCCESS; } // TODO: Test this with non-AT_FDCWD values. int __syscall_unlinkat(int dirfd, intptr_t path, int flags) { if (flags & ~AT_REMOVEDIR) { // TODO: Test this case. return -EINVAL; } // It is invalid for rmdir paths to end in ".", but we need to distinguish // this case from the case of `parseParent` returning (root, '.') when parsing // "/", so we need to find the invalid "/." manually. if (flags == AT_REMOVEDIR) { std::string_view p((char*)path); // Ignore trailing '/'. while (!p.empty() && p.back() == '/') { p.remove_suffix(1); } if (p.size() >= 2 && p.substr(p.size() - 2) == std::string_view("/.")) { return -EINVAL; } } auto parsed = path::parseParent((char*)path, dirfd); if (auto err = parsed.getError()) { return err; } auto& [parent, childNameView] = parsed.getParentChild(); std::string childName(childNameView); auto lockedParent = parent->locked(); auto file = lockedParent.getChild(childName); if (!file) { return -ENOENT; } // Disallow removing the root directory, even if it is empty. if (file == wasmFS.getRootDirectory()) { return -EBUSY; } auto lockedFile = file->locked(); if (auto dir = file->dynCast()) { if (flags != AT_REMOVEDIR) { return -EISDIR; } // A directory can only be removed if it has no entries. if (dir->locked().getNumEntries() > 0) { return -ENOTEMPTY; } } else { // A normal file or symlink. if (flags == AT_REMOVEDIR) { return -ENOTDIR; } } // Cannot unlink/rmdir if the parent dir doesn't have write permissions. if (!(lockedParent.getMode() & WASMFS_PERM_WRITE)) { return -EACCES; } // Input is valid, perform the unlink. return lockedParent.removeChild(childName); } int __syscall_rmdir(intptr_t path) { return __syscall_unlinkat(AT_FDCWD, path, AT_REMOVEDIR); } // wasmfs_unmount is similar to __syscall_unlinkat, but assumes AT_REMOVEDIR is // true and will only unlink mountpoints (Empty and nonempty). int wasmfs_unmount(const char* path) { auto parsed = path::parseParent(path, AT_FDCWD); if (auto err = parsed.getError()) { return err; } auto& [parent, childNameView] = parsed.getParentChild(); std::string childName(childNameView); auto lockedParent = parent->locked(); auto file = lockedParent.getChild(childName); if (!file) { return -ENOENT; } // Disallow removing the root directory, even if it is empty. if (file == wasmFS.getRootDirectory()) { return -EBUSY; } if (!file->dynCast()) { // A normal file or symlink. return -ENOTDIR; } if (parent->getBackend() == file->getBackend()) { // The child is not a valid mountpoint. return -EINVAL; } // Input is valid, perform the unlink. return lockedParent.removeChild(childName); } int __syscall_getdents64(int fd, intptr_t dirp, size_t count) { dirent* result = (dirent*)dirp; // Check if the result buffer is too small. if (count / sizeof(dirent) == 0) { return -EINVAL; } auto openFile = wasmFS.getFileTable().locked().getEntry(fd); if (!openFile) { return -EBADF; } auto lockedOpenFile = openFile->locked(); auto dir = lockedOpenFile.getFile()->dynCast(); if (!dir) { return -ENOTDIR; } auto lockedDir = dir->locked(); // A directory's position corresponds to the index in its entries vector. int index = lockedOpenFile.getPosition(); // If this directory has been unlinked and has no parent, then it is // completely empty. auto parent = lockedDir.getParent(); if (!parent) { return 0; } off_t bytesRead = 0; const auto& dirents = openFile->dirents; for (; index < dirents.size() && bytesRead + sizeof(dirent) <= count; index++) { const auto& entry = dirents[index]; result->d_ino = entry.ino; result->d_off = index + 1; result->d_reclen = sizeof(dirent); switch (entry.kind) { case File::UnknownKind: result->d_type = DT_UNKNOWN; break; case File::DataFileKind: result->d_type = DT_REG; break; case File::DirectoryKind: result->d_type = DT_DIR; break; case File::SymlinkKind: result->d_type = DT_LNK; break; default: result->d_type = DT_UNKNOWN; break; } assert(entry.name.size() + 1 <= sizeof(result->d_name)); strcpy(result->d_name, entry.name.c_str()); ++result; bytesRead += sizeof(dirent); } // Update position lockedOpenFile.setPosition(index); return bytesRead; } // TODO: Test this with non-AT_FDCWD values. int __syscall_renameat(int olddirfd, intptr_t oldpath, int newdirfd, intptr_t newpath) { // Rename is the only syscall that needs to (or is allowed to) acquire locks // on two directories at once. It requires locks on both the old and new // parent directories to ensure that the moved file can be atomically removed // from the old directory and added to the new directory without something // changing that would prevent the move. // // To prevent deadlock in the case of simultaneous renames, serialize renames // with an additional global lock. static std::mutex renameMutex; std::lock_guard renameLock(renameMutex); // Get the old directory. auto parsedOld = path::parseParent((char*)oldpath, olddirfd); if (auto err = parsedOld.getError()) { return err; } auto& [oldParent, oldFileNameView] = parsedOld.getParentChild(); std::string oldFileName(oldFileNameView); // Get the new directory. auto parsedNew = path::parseParent((char*)newpath, newdirfd); if (auto err = parsedNew.getError()) { return err; } auto& [newParent, newFileNameView] = parsedNew.getParentChild(); std::string newFileName(newFileNameView); if (newFileNameView.size() > WASMFS_NAME_MAX) { return -ENAMETOOLONG; } // Lock both directories. auto lockedOldParent = oldParent->locked(); auto lockedNewParent = newParent->locked(); // Get the source and destination files. auto oldFile = lockedOldParent.getChild(oldFileName); auto newFile = lockedNewParent.getChild(newFileName); if (!oldFile) { return -ENOENT; } // If the source and destination are the same, do nothing. if (oldFile == newFile) { return 0; } // Never allow renaming or overwriting the root. auto root = wasmFS.getRootDirectory(); if (oldFile == root || newFile == root) { return -EBUSY; } // Cannot modify either directory without write permissions. if (!(lockedOldParent.getMode() & WASMFS_PERM_WRITE) || !(lockedNewParent.getMode() & WASMFS_PERM_WRITE)) { return -EACCES; } // Both parents must have the same backend. if (oldParent->getBackend() != newParent->getBackend()) { return -EXDEV; } // Check that oldDir is not an ancestor of newDir. for (auto curr = newParent; curr != root; curr = curr->locked().getParent()) { if (curr == oldFile) { return -EINVAL; } } // The new file will be removed if it already exists. if (newFile) { if (auto newDir = newFile->dynCast()) { // Cannot overwrite a directory with a non-directory. auto oldDir = oldFile->dynCast(); if (!oldDir) { return -EISDIR; } // Cannot overwrite a non-empty directory. if (newDir->locked().getNumEntries() > 0) { return -ENOTEMPTY; } } else { // Cannot overwrite a non-directory with a directory. if (oldFile->is()) { return -ENOTDIR; } } } // Perform the move. if (auto err = lockedNewParent.insertMove(newFileName, oldFile)) { assert(err < 0); return err; } return 0; } // TODO: Test this with non-AT_FDCWD values. int __syscall_symlinkat(intptr_t target, int newdirfd, intptr_t linkpath) { auto parsed = path::parseParent((char*)linkpath, newdirfd); if (auto err = parsed.getError()) { return err; } auto& [parent, childNameView] = parsed.getParentChild(); if (childNameView.size() > WASMFS_NAME_MAX) { return -ENAMETOOLONG; } auto lockedParent = parent->locked(); std::string childName(childNameView); if (lockedParent.getChild(childName)) { return -EEXIST; } if (!lockedParent.insertSymlink(childName, (char*)target)) { return -EPERM; } return 0; } // TODO: Test this with non-AT_FDCWD values. int __syscall_readlinkat(int dirfd, intptr_t path, intptr_t buf, size_t bufsize) { // TODO: Handle empty paths. auto parsed = path::parseFile((char*)path, dirfd, path::NoFollowLinks); if (auto err = parsed.getError()) { return err; } auto link = parsed.getFile()->dynCast(); if (!link) { return -EINVAL; } const auto& target = link->getTarget(); auto bytes = std::min((size_t)bufsize, target.size()); memcpy((char*)buf, target.c_str(), bytes); return bytes; } static double timespec_to_ms(timespec ts) { if (ts.tv_nsec == UTIME_OMIT) { return INFINITY; } if (ts.tv_nsec == UTIME_NOW) { return emscripten_date_now(); } return double(ts.tv_sec) * 1000 + double(ts.tv_nsec) / (1000 * 1000); } // TODO: Test this with non-AT_FDCWD values. int __syscall_utimensat(int dirFD, intptr_t path_, intptr_t times_, int flags) { const char* path = (const char*)path_; const struct timespec* times = (const struct timespec*)times_; if (flags & ~AT_SYMLINK_NOFOLLOW) { // TODO: Test this case. return -EINVAL; } // Add AT_EMPTY_PATH as Linux (and so, musl, and us) has a nonstandard // behavior in which an empty path means to operate on whatever is in dirFD // (directory or not), which is exactly the behavior of AT_EMPTY_PATH (but // without passing that in). See "C library/kernel ABI differences" in // https://man7.org/linux/man-pages/man2/utimensat.2.html // // TODO: Handle AT_SYMLINK_NOFOLLOW once we traverse symlinks correctly. auto parsed = path::getFileAt(dirFD, path, flags | AT_EMPTY_PATH); if (auto err = parsed.getError()) { return err; } // TODO: Handle tv_nsec being UTIME_NOW or UTIME_OMIT. // TODO: Check for write access to the file (see man page for specifics). double aTime, mTime; if (times == nullptr) { aTime = mTime = emscripten_date_now(); } else { aTime = timespec_to_ms(times[0]); mTime = timespec_to_ms(times[1]); } auto locked = parsed.getFile()->locked(); if (aTime != INFINITY) { locked.setATime(aTime); } if (mTime != INFINITY) { locked.setMTime(mTime); } return 0; } // TODO: Test this with non-AT_FDCWD values. int __syscall_fchmodat2(int dirfd, intptr_t path, int mode, int flags) { if (flags & ~AT_SYMLINK_NOFOLLOW) { // TODO: Test this case. return -EINVAL; } auto parsed = path::getFileAt(dirfd, (char*)path, flags); if (auto err = parsed.getError()) { return err; } auto lockedFile = parsed.getFile()->locked(); lockedFile.setMode(mode); // On POSIX, ctime is updated on metadata changes, like chmod. lockedFile.updateCTime(); return 0; } int __syscall_chmod(intptr_t path, int mode) { return __syscall_fchmodat2(AT_FDCWD, path, mode, 0); } int __syscall_fchmod(int fd, int mode) { auto openFile = wasmFS.getFileTable().locked().getEntry(fd); if (!openFile) { return -EBADF; } auto lockedFile = openFile->locked().getFile()->locked(); lockedFile.setMode(mode); lockedFile.updateCTime(); return 0; } int __syscall_fchownat( int dirfd, intptr_t path, int owner, int group, int flags) { // Only accept valid flags. if (flags & ~(AT_EMPTY_PATH | AT_SYMLINK_NOFOLLOW)) { // TODO: Test this case. return -EINVAL; } auto parsed = path::getFileAt(dirfd, (char*)path, flags); if (auto err = parsed.getError()) { return err; } // Ignore the actual owner and group because we don't track those. // TODO: Update metadata time stamp. return 0; } int __syscall_fchown32(int fd, int owner, int group) { return __syscall_fchownat(fd, (intptr_t) "", owner, group, AT_EMPTY_PATH); } // TODO: Test this with non-AT_FDCWD values. int __syscall_faccessat(int dirfd, intptr_t path, int amode, int flags) { // The input must be F_OK (check for existence) or a combination of [RWX]_OK // flags. if (amode != F_OK && (amode & ~(R_OK | W_OK | X_OK))) { return -EINVAL; } if (flags & ~(AT_EACCESS | AT_SYMLINK_NOFOLLOW)) { // TODO: Test this case. return -EINVAL; } // TODO: Handle AT_SYMLINK_NOFOLLOW once we traverse symlinks correctly. auto parsed = path::parseFile((char*)path, dirfd); if (auto err = parsed.getError()) { return err; } if (amode != F_OK) { auto mode = parsed.getFile()->locked().getMode(); if ((amode & R_OK) && !(mode & WASMFS_PERM_READ)) { return -EACCES; } if ((amode & W_OK) && !(mode & WASMFS_PERM_WRITE)) { return -EACCES; } if ((amode & X_OK) && !(mode & WASMFS_PERM_EXECUTE)) { return -EACCES; } } return 0; } static int doTruncate(std::shared_ptr& file, off_t size) { auto dataFile = file->dynCast(); if (!dataFile) { return -EISDIR; } auto locked = dataFile->locked(); if (!(locked.getMode() & WASMFS_PERM_WRITE)) { return -EACCES; } if (size < 0) { return -EINVAL; } int ret = locked.setSize(size); assert(ret <= 0); return ret; } int __syscall_truncate64(intptr_t path, off_t size) { auto parsed = path::parseFile((char*)path); if (auto err = parsed.getError()) { return err; } return doTruncate(parsed.getFile(), size); } int __syscall_ftruncate64(int fd, off_t size) { auto openFile = wasmFS.getFileTable().locked().getEntry(fd); if (!openFile) { return -EBADF; } auto ret = doTruncate(openFile->locked().getFile(), size); // XXX It is not clear from the docs why ftruncate would differ from // truncate here. However, on Linux this definitely happens, and the old // FS matches that as well, so do the same here. if (ret == -EACCES) { ret = -EINVAL; } return ret; } static bool isTTY(std::shared_ptr& file) { // TODO: Full TTY support. For now, just see stdin/out/err as terminals and // nothing else. return file == SpecialFiles::getStdin() || file == SpecialFiles::getStdout() || file == SpecialFiles::getStderr(); } int __syscall_ioctl(int fd, int request, ...) { auto openFile = wasmFS.getFileTable().locked().getEntry(fd); if (!openFile) { return -EBADF; } va_list args; va_start(args, request); void* argp = va_arg(args, void*); va_end(args); auto openHandle = openFile->locked(); auto file = openHandle.getFile(); if (request == FIONREAD) { off_t size = file->locked().getSize(); if (size < 0) { return (int)size; } if (file->isSeekable()) { size -= openHandle.getPosition(); } *static_cast(argp) = static_cast(size); return 0; } if (!isTTY(file)) { return -ENOTTY; } // TODO: Full TTY support. For now this is limited, and matches the old FS. switch (request) { case TCGETA: case TCGETS: case TCSETA: case TCSETAW: case TCSETAF: case TCSETS: case TCSETSW: case TCSETSF: case TIOCGWINSZ: case TIOCSWINSZ: { // TTY operations that we do nothing for anyhow can just be ignored. return 0; } default: { return -EINVAL; // not supported } } } int __syscall_pipe2(intptr_t fd, int flags) { auto* fds = (__wasi_fd_t*)fd; if (flags && flags != O_CLOEXEC) { return -ENOTSUP; } // Make a pipe: Two PipeFiles that share a single data source between them, so // that writing to one can be read in the other. // // No backend is needed here, so pass in nullptr for that. auto data = std::make_shared(); auto reader = std::make_shared(S_IRUGO, data); auto writer = std::make_shared(S_IWUGO, data); std::shared_ptr openReader, openWriter; (void)OpenFileState::create(reader, O_RDONLY, openReader); (void)OpenFileState::create(writer, O_WRONLY, openWriter); auto fileTable = wasmFS.getFileTable().locked(); fds[0] = fileTable.addEntry(openReader); fds[1] = fileTable.addEntry(openWriter); return 0; } // int poll(struct pollfd* fds, nfds_t nfds, int timeout); int __syscall_poll(intptr_t fds_, int nfds, int timeout) { struct pollfd* fds = (struct pollfd*)fds_; auto fileTable = wasmFS.getFileTable().locked(); // Process the list of FDs and compute their revents masks. Count the number // of nonzero such masks, which is our return value. int nonzero = 0; for (nfds_t i = 0; i < nfds; i++) { auto* pollfd = &fds[i]; auto fd = pollfd->fd; if (fd < 0) { // Negative FDs are ignored in poll(). pollfd->revents = 0; continue; } // Assume invalid, unless there is an open file. auto mask = POLLNVAL; auto openFile = fileTable.getEntry(fd); if (openFile) { mask = 0; auto flags = openFile->locked().getFlags(); auto accessMode = flags & O_ACCMODE; auto readBit = pollfd->events & POLLOUT; if (readBit && (accessMode == O_WRONLY || accessMode == O_RDWR)) { mask |= readBit; } auto writeBit = pollfd->events & POLLIN; if (writeBit && (accessMode == O_RDONLY || accessMode == O_RDWR)) { // If there is data in the file, then there is also the ability to read. // TODO: Does this need to consider the position as well? That is, if // the position is at the end, we can't read from the current position // at least. If we update this, make sure the size isn't an error! if (openFile->locked().getFile()->locked().getSize() > 0) { mask |= writeBit; } } // TODO: get mask from File dynamically using a poll() hook? } // TODO: set the state based on the state of the other end of the pipe, for // pipes (POLLERR | POLLHUP) if (mask) { nonzero++; } pollfd->revents = mask; } // TODO: This should block based on the timeout. The old FS did not do so due // to web limitations, which we should perhaps revisit (especially with // pthreads and asyncify). return nonzero; } int __syscall_fallocate(int fd, int mode, off_t offset, off_t len) { assert(mode == 0); // TODO, but other modes were never supported in the old FS auto fileTable = wasmFS.getFileTable().locked(); auto openFile = fileTable.getEntry(fd); if (!openFile) { return -EBADF; } auto dataFile = openFile->locked().getFile()->dynCast(); // TODO: support for symlinks. if (!dataFile) { return -ENODEV; } auto locked = dataFile->locked(); if (!(locked.getMode() & WASMFS_PERM_WRITE)) { return -EBADF; } if (offset < 0 || len <= 0) { return -EINVAL; } // TODO: We could only fill zeros for regions that were completely unused // before, which for a backend with sparse data storage could make a // difference. For that we'd need a new backend API. auto newNeededSize = offset + len; off_t size = locked.getSize(); if (size < 0) { return size; } if (newNeededSize > size) { if (auto err = locked.setSize(newNeededSize)) { assert(err < 0); return err; } } return 0; } int __syscall_fcntl64(int fd, int cmd, ...) { auto fileTable = wasmFS.getFileTable().locked(); auto openFile = fileTable.getEntry(fd); if (!openFile) { return -EBADF; } switch (cmd) { case F_DUPFD: { int newfd; va_list v1; va_start(v1, cmd); newfd = va_arg(v1, int); va_end(v1); if (newfd < 0) { return -EINVAL; } // Find the first available fd at arg or after. // TODO: Should we check for a limit on the max FD number, if we have one? while (1) { if (!fileTable.getEntry(newfd)) { (void)fileTable.setEntry(newfd, openFile); return newfd; } newfd++; } } case F_GETFD: case F_SETFD: // FD_CLOEXEC makes no sense for a single process. return 0; case F_GETFL: return openFile->locked().getFlags(); case F_SETFL: { int flags; va_list v1; va_start(v1, cmd); flags = va_arg(v1, int); va_end(v1); auto lockedOpenFile = openFile->locked(); auto oldFlags = lockedOpenFile.getFlags(); // This syscall should ignore most flags. int mask = O_APPEND | O_NONBLOCK | O_ASYNC | O_DIRECT | O_NOATIME; lockedOpenFile.setFlags((oldFlags & ~mask) | (flags & mask)); return 0; } case F_GETLK: { // If these constants differ then we'd need a case for both. static_assert(F_GETLK == F_GETLK64); flock* data; va_list v1; va_start(v1, cmd); data = va_arg(v1, flock*); va_end(v1); // We're always unlocked for now, until we implement byte-range locks. data->l_type = F_UNLCK; return 0; } case F_SETLK: case F_SETLKW: { static_assert(F_SETLK == F_SETLK64); static_assert(F_SETLKW == F_SETLKW64); // Pretend that the locking is successful. These are process-level locks, // and Emscripten programs are a single process. If we supported linking a // filesystem between programs, we'd need to do more here. // See https://github.com/emscripten-core/emscripten/issues/23697 return 0; } default: { // TODO: support any remaining cmds return -EINVAL; } } } static int doStatFS(std::shared_ptr& file, size_t size, struct statfs* buf) { if (size != sizeof(struct statfs)) { // We only know how to write to a standard statfs, not even a truncated one. return -EINVAL; } // NOTE: None of the constants here are true. We're just returning safe and // sane values, that match the long-existing JS FS behavior (except for // the inode number, where we can do better). buf->f_type = 0; buf->f_bsize = 4096; buf->f_frsize = 4096; buf->f_blocks = 1000000; buf->f_bfree = 500000; buf->f_bavail = 500000; buf->f_files = file->getIno(); buf->f_ffree = 1000000; buf->f_fsid = {0, 0}; buf->f_flags = ST_NOSUID; buf->f_namelen = 255; return 0; } int __syscall_statfs64(intptr_t path, size_t size, intptr_t buf) { auto parsed = path::parseFile((char*)path); if (auto err = parsed.getError()) { return err; } return doStatFS(parsed.getFile(), size, (struct statfs*)buf); } int __syscall_fstatfs64(int fd, size_t size, intptr_t buf) { auto openFile = wasmFS.getFileTable().locked().getEntry(fd); if (!openFile) { return -EBADF; } return doStatFS(openFile->locked().getFile(), size, (struct statfs*)buf); } int _mmap_js(size_t length, int prot, int flags, int fd, off_t offset, int* allocated, void** addr) { // PROT_EXEC is not supported (although we pretend to support the absence of // PROT_READ or PROT_WRITE). if ((prot & PROT_EXEC)) { return -EPERM; } if (!length) { return -EINVAL; } // One of MAP_PRIVATE, MAP_SHARED, or MAP_SHARED_VALIDATE must be used. int mapType = flags & MAP_TYPE; if (mapType != MAP_PRIVATE && mapType != MAP_SHARED && mapType != MAP_SHARED_VALIDATE) { return -EINVAL; } if (mapType == MAP_SHARED_VALIDATE) { WASMFS_UNREACHABLE("TODO: MAP_SHARED_VALIDATE"); } auto openFile = wasmFS.getFileTable().locked().getEntry(fd); if (!openFile) { return -EBADF; } std::shared_ptr file; // Keep the open file info locked only for as long as we need that. { auto lockedOpenFile = openFile->locked(); // Check permissions. We always need read permissions, since we need to read // the data in the file to map it. if ((lockedOpenFile.getFlags() & O_ACCMODE) == O_WRONLY) { return -EACCES; } // According to the POSIX spec it is possible to write to a file opened in // read-only mode with MAP_PRIVATE flag, as all modifications will be // visible only in the memory of the current process. if ((prot & PROT_WRITE) != 0 && mapType != MAP_PRIVATE && (lockedOpenFile.getFlags() & O_ACCMODE) != O_RDWR) { return -EACCES; } file = lockedOpenFile.getFile()->dynCast(); } if (!file) { return -ENODEV; } // TODO: On MAP_SHARED, install the mapping on the DataFile object itself so // that reads and writes can be redirected to the mapped region and so that // the mapping can correctly outlive the file being closed. This will require // changes to emscripten_mmap.c as well. // Align to a wasm page size, as we expect in the future to get wasm // primitives to do this work, and those would presumably be aligned to a page // size. Aligning now avoids confusion later. uint8_t* ptr = (uint8_t*)emscripten_builtin_memalign(WASM_PAGE_SIZE, length); if (!ptr) { return -ENOMEM; } auto nread = file->locked().read(ptr, length, offset); if (nread < 0) { // The read failed. Report the error, but first free the allocation. emscripten_builtin_free(ptr); return nread; } // From here on, we have succeeded, and can mark the allocation as having // occurred (which means that the caller has the responsibility to free it). *allocated = true; *addr = (void*)ptr; // The read must be of a valid amount, or we have had an internal logic error. assert(nread <= length); // mmap clears any extra bytes after the data itself. memset(ptr + nread, 0, length - nread); return 0; } int _msync_js( intptr_t addr, size_t length, int prot, int flags, int fd, off_t offset) { // TODO: This is not correct! Mappings should be associated with files, not // fds. Only need to sync if shared and writes are allowed. int mapType = flags & MAP_TYPE; if (mapType == MAP_SHARED && (prot & PROT_WRITE)) { __wasi_ciovec_t iovec; iovec.buf = (uint8_t*)addr; iovec.buf_len = length; __wasi_size_t nwritten; // Translate from WASI positive error codes to negative error codes. return -__wasi_fd_pwrite(fd, &iovec, 1, offset, &nwritten); } return 0; } int _munmap_js( intptr_t addr, size_t length, int prot, int flags, int fd, off_t offset) { // TODO: This is not correct! Mappings should be associated with files, not // fds. // TODO: Syncing should probably be handled in __syscall_munmap instead. return _msync_js(addr, length, prot, flags, fd, offset); } // Stubs (at least for now) int __syscall_accept4(int sockfd, intptr_t addr, intptr_t addrlen, int flags, int dummy1, int dummy2) { return -ENOSYS; } int __syscall_bind( int sockfd, intptr_t addr, size_t alen, int dummy, int dummy2, int dummy3) { return -ENOSYS; } int __syscall_connect( int sockfd, intptr_t addr, size_t len, int dummy, int dummy2, int dummy3) { return -ENOSYS; } int __syscall_socket( int domain, int type, int protocol, int dummy1, int dummy2, int dummy3) { return -ENOSYS; } int __syscall_listen( int sockfd, int backlog, int dummy1, int dummy2, int dummy3, int dummy4) { return -ENOSYS; } int __syscall_getsockopt(int sockfd, int level, int optname, intptr_t optval, intptr_t optlen, int dummy) { return -ENOSYS; } int __syscall_getsockname( int sockfd, intptr_t addr, intptr_t len, int dummy, int dummy2, int dummy3) { return -ENOSYS; } int __syscall_getpeername( int sockfd, intptr_t addr, intptr_t len, int dummy, int dummy2, int dummy3) { return -ENOSYS; } int __syscall_sendto( int sockfd, intptr_t msg, size_t len, int flags, intptr_t addr, size_t alen) { return -ENOSYS; } int __syscall_sendmsg( int sockfd, intptr_t msg, int flags, intptr_t addr, size_t alen, int dummy) { return -ENOSYS; } int __syscall_recvfrom(int sockfd, intptr_t msg, size_t len, int flags, intptr_t addr, intptr_t alen) { return -ENOSYS; } int __syscall_recvmsg( int sockfd, intptr_t msg, int flags, int dummy, int dummy2, int dummy3) { return -ENOSYS; } int __syscall_fadvise64(int fd, off_t offset, off_t length, int advice) { // Advice is currently ignored. TODO some backends might use it return 0; } } // extern "C"