-
Notifications
You must be signed in to change notification settings - Fork 3.4k
Implement open
/stat
/mkdir
/symlink
for standalone WASI mode
#24246
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
c606386
46b7fa4
16b1925
e12df0f
65bec88
ca3f795
52a4e03
53c309b
65e8c96
5303f12
95eab07
bd4a07c
73e982e
36e66d8
cc87f5d
67905b1
88650d8
f3a80bb
534223f
16d2ba7
eb35b6b
1ec092a
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change | ||
---|---|---|---|---|
@@ -0,0 +1,254 @@ | ||||
/* | ||||
* Copyright 2025 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. | ||||
* | ||||
* The preopen code is based on wasi-libc's `preopens.c` which is licensed | ||||
* under a MIT style license. This license can also be found in the LICENSE | ||||
* file. | ||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Please keep the copyright line from the original file, in addition to this text (which is good). |
||||
*/ | ||||
|
||||
#define _GNU_SOURCE | ||||
#include "paths.h" | ||||
|
||||
#include <assert.h> | ||||
#include <errno.h> | ||||
#include <fcntl.h> | ||||
#include <stdbool.h> | ||||
#include <stdlib.h> | ||||
#include <string.h> | ||||
#include <sysexits.h> | ||||
|
||||
// A name and file descriptor pair. | ||||
typedef struct preopen { | ||||
// The path prefix associated with the file descriptor. | ||||
const char* prefix; | ||||
|
||||
// The file descriptor. | ||||
__wasi_fd_t fd; | ||||
} preopen; | ||||
|
||||
// A simple growable array of `preopen`. | ||||
static preopen* preopens; | ||||
static size_t num_preopens; | ||||
|
||||
// Are the `prefix_len` bytes pointed to by `prefix` a prefix of `path`? | ||||
static bool | ||||
prefix_matches(const char* prefix, size_t prefix_len, const char* path) { | ||||
// Allow an empty string as a prefix of any relative path. | ||||
if (path[0] != '/' && prefix_len == 0) { | ||||
return true; | ||||
sbc100 marked this conversation as resolved.
Show resolved
Hide resolved
|
||||
} | ||||
|
||||
// Check whether any bytes of the prefix differ. | ||||
if (memcmp(path, prefix, prefix_len) != 0) { | ||||
return false; | ||||
} | ||||
|
||||
// Ignore trailing slashes in directory names. | ||||
size_t i = prefix_len; | ||||
while (i > 0 && prefix[i - 1] == '/') { | ||||
--i; | ||||
} | ||||
|
||||
// Match only complete path components. | ||||
char last = path[i]; | ||||
return last == '/' || last == '\0'; | ||||
} | ||||
|
||||
bool __paths_resolve_path(int* resolved_dirfd, const char** path_ptr) { | ||||
const char* path = *path_ptr; | ||||
|
||||
if (*resolved_dirfd != AT_FDCWD && path[0] != '/') { | ||||
return true; | ||||
} | ||||
|
||||
// Strip leading `/` characters, the prefixes we're mataching won't have | ||||
// them. | ||||
while (*path == '/') { | ||||
path++; | ||||
} | ||||
// Search through the preopens table. Iterate in reverse so that more | ||||
// recently added preopens take precedence over less recently addded ones. | ||||
size_t match_len = 0; | ||||
int fd = -1; | ||||
for (size_t i = num_preopens; i > 0; --i) { | ||||
const preopen* pre = &preopens[i - 1]; | ||||
const char* prefix = pre->prefix; | ||||
size_t len = strlen(prefix); | ||||
|
||||
// If we haven't had a match yet, or the candidate path is longer than | ||||
// our current best match's path, and the candidate path is a prefix of | ||||
// the requested path, take that as the new best path. | ||||
if ((fd == -1 || len > match_len) && prefix_matches(prefix, len, path)) { | ||||
fd = pre->fd; | ||||
match_len = len; | ||||
} | ||||
} | ||||
|
||||
if (fd == -1) { | ||||
return false; | ||||
} | ||||
|
||||
// The relative path is the substring after the portion that was matched. | ||||
const char* computed = path + match_len; | ||||
|
||||
// Omit leading slashes in the relative path. | ||||
while (*computed == '/') { | ||||
++computed; | ||||
} | ||||
|
||||
// *at syscalls don't accept empty relative paths, so use "." instead. | ||||
if (*computed == '\0') { | ||||
computed = "."; | ||||
} | ||||
|
||||
*resolved_dirfd = fd; | ||||
*path_ptr = computed; | ||||
return true; | ||||
} | ||||
|
||||
#if defined(EMSCRIPTEN_PURE_WASI) | ||||
|
||||
static size_t preopen_capacity; | ||||
|
||||
#ifdef NDEBUG | ||||
#define assert_invariants() // assertions disabled | ||||
#else | ||||
static void assert_invariants(void) { | ||||
assert(num_preopens <= preopen_capacity); | ||||
assert(preopen_capacity == 0 || preopens != NULL); | ||||
assert(preopen_capacity == 0 || | ||||
preopen_capacity * sizeof(preopen) > preopen_capacity); | ||||
|
||||
for (size_t i = 0; i < num_preopens; ++i) { | ||||
const preopen* pre = &preopens[i]; | ||||
assert(pre->prefix != NULL); | ||||
assert(pre->fd != (__wasi_fd_t)-1); | ||||
#ifdef __wasm__ | ||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why |
||||
assert((uintptr_t)pre->prefix < | ||||
(__uint128_t)__builtin_wasm_memory_size(0) * PAGESIZE); | ||||
#endif | ||||
} | ||||
} | ||||
#endif | ||||
|
||||
// Allocate space for more preopens. Returns 0 on success and -1 on failure. | ||||
static bool resize_preopens(void) { | ||||
size_t start_capacity = 4; | ||||
size_t old_capacity = preopen_capacity; | ||||
size_t new_capacity = old_capacity == 0 ? start_capacity : old_capacity * 2; | ||||
|
||||
preopen* old_preopens = preopens; | ||||
preopen* new_preopens = calloc(sizeof(preopen), new_capacity); | ||||
if (new_preopens == NULL) { | ||||
return false; | ||||
} | ||||
|
||||
memcpy(new_preopens, old_preopens, num_preopens * sizeof(preopen)); | ||||
preopens = new_preopens; | ||||
preopen_capacity = new_capacity; | ||||
free(old_preopens); | ||||
|
||||
assert_invariants(); | ||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why not just |
||||
return true; | ||||
} | ||||
|
||||
// Normalize an absolute path. Removes leading `/` and leading `./`, so the | ||||
// first character is the start of a directory name. This works because our | ||||
// process always starts with a working directory of `/`. Additionally translate | ||||
// `.` to the empty string. | ||||
static const char* strip_prefixes(const char* path) { | ||||
while (1) { | ||||
if (path[0] == '/') { | ||||
path++; | ||||
} else if (path[0] == '.' && path[1] == '/') { | ||||
path += 2; | ||||
} else if (path[0] == '.' && path[1] == 0) { | ||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. use ' This case looks like its trying to match |
||||
path++; | ||||
} else { | ||||
break; | ||||
} | ||||
} | ||||
|
||||
return path; | ||||
} | ||||
|
||||
// Register the given preopened file descriptor under the given path. | ||||
// | ||||
// This function takes ownership of `prefix`. | ||||
static bool register_preopened_fd(__wasi_fd_t fd, const char* relprefix) { | ||||
// Check preconditions. | ||||
assert_invariants(); | ||||
assert(fd != AT_FDCWD); | ||||
assert(fd != -1); | ||||
assert(relprefix != NULL); | ||||
|
||||
if (num_preopens == preopen_capacity && !resize_preopens()) { | ||||
return false; | ||||
} | ||||
|
||||
char* prefix = strdup(strip_prefixes(relprefix)); | ||||
if (prefix == NULL) { | ||||
return false; | ||||
} | ||||
preopens[num_preopens++] = (preopen){ | ||||
prefix, | ||||
fd, | ||||
}; | ||||
|
||||
assert_invariants(); | ||||
return true; | ||||
} | ||||
|
||||
// Populate WASI preopens. | ||||
__attribute__((constructor(100))) // construct this before user code | ||||
static void _standalone_populate_preopens(void) { | ||||
// Skip stdin, stdout, and stderr, and count up until we reach an invalid | ||||
// file descriptor. | ||||
for (__wasi_fd_t fd = 3; fd != 0; ++fd) { | ||||
__wasi_prestat_t prestat; | ||||
__wasi_errno_t ret = __wasi_fd_prestat_get(fd, &prestat); | ||||
if (ret == __WASI_ERRNO_BADF) { | ||||
break; | ||||
} | ||||
if (ret != __WASI_ERRNO_SUCCESS) { | ||||
goto oserr; | ||||
} | ||||
switch (prestat.pr_type) { | ||||
case __WASI_PREOPENTYPE_DIR: { | ||||
char* prefix = malloc(prestat.u.dir.pr_name_len + 1); | ||||
if (prefix == NULL) { | ||||
goto software; | ||||
} | ||||
|
||||
// TODO: Remove the cast on `path` once the witx is updated with | ||||
// char8 support. | ||||
ret = __wasi_fd_prestat_dir_name( | ||||
fd, (uint8_t*)prefix, prestat.u.dir.pr_name_len); | ||||
if (ret != __WASI_ERRNO_SUCCESS) { | ||||
goto oserr; | ||||
} | ||||
prefix[prestat.u.dir.pr_name_len] = '\0'; | ||||
|
||||
if (!register_preopened_fd(fd, prefix)) { | ||||
goto software; | ||||
} | ||||
free(prefix); | ||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Given that |
||||
|
||||
break; | ||||
} | ||||
default: | ||||
break; | ||||
} | ||||
} | ||||
|
||||
return; | ||||
oserr: | ||||
_Exit(EX_OSERR); | ||||
software: | ||||
_Exit(EX_SOFTWARE); | ||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why not just There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Good question -- this seems to be a wasi-libc thing. The code in
|
||||
} | ||||
|
||||
#endif |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,25 @@ | ||
/* | ||
* Copyright 2025 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. | ||
*/ | ||
|
||
#pragma once | ||
|
||
#include <stdbool.h> | ||
|
||
// | ||
// Resolve a (dirfd, relative/absolute path) pair. | ||
// | ||
// Arguments: | ||
// - `resolved_dirfd`: | ||
// - as input: input dirfd, may be `AT_FDCWD` | ||
// - as output: resolved dirfd (which always is a preopened fd) | ||
// - `path_ptr`: | ||
// - as input: pointer to a relative or absolute path | ||
// - as output: a path relative to `resolved_dirfd` | ||
// | ||
// Returns: `true` if resolution was successful, `false` otherwise. | ||
// | ||
bool __paths_resolve_path(int* resolved_dirfd, const char** path_ptr); |
Uh oh!
There was an error while loading. Please reload this page.