Repository navigation
wasi:filesystem alternative - force guests to do all path resolution #933
Description
This issue is an extension of my comment here with a more precise design in a location better suiting something that's actually entirely orthogonal to the proposed direction of #742. To recap slightly, there are a number of motivations that this issue is designed to resolve:
- Most hosts (to my knowledge) do not attempt to sandbox
wasi:filesystemAPIs. For example Node's wasip1 shim explicitly says it doesn't sandbox. My personal understanding of the situation is that it's just extremely difficult to write a sandboxed implementation with the current APIs thatwasi:filesystemrequires hosts to provide. To me, subjectively again, this is not a great property ofwasi:filesystemif the goal is to provide a sandbox and most hosts just don't do that. - Embedders generally expect
wasi:filesystemto be sandboxed, however. Additionally embedders expect the ability to weave together multiple host directories into the guest at various paths. Embedders also generally expect the ability to make host directories either read-only or entirely read/write. Embedders expect all modifications to be neatly contained within these directories, however. - No guest, to my knowledge, handles path resolution correctly with multiple preopens. For example with
/aand/bpreopens the guest path/a/../b/foo.txtdoes not resolve on any guest -- again, as far as I know. - No host nor guest, to my knowledge handles overlapping preopens in a standard or compatible way. If a guest has preopens
/fooand/foo/bar, then behavior is probably quite host specific, embedder-specific, and guest-specific. - To my knowledge there is no existing portable OS API that can be used to exposed a woven-together filesystem to a guest as a single directory. Basically I do not think that these problems all exist for lack of a "oh we can just use that" solution. These, I believe, are all genuinely very hard problems to solve and with no preexisting solution suitable for hosts. (someone please correct me if I'm wrong!)
In my comment I also outlined why I don't think that #742 is a viable solution to at least a subset of these concerns. Notably implement #742 would involve putting even more complexity into hosts for path resolution and such, and if many hosts already aren't sandboxing anything today they'll almost certainly not provide another VFS-style layer on top of all of that.
One thing I've articulated to myself at least is that to have reasonable semantics for a filesystem there needs to be a single "thing" that does path resolution. Right now this is spread across the guest (picking a preopen) and the host (operation on a sub-path from a preopen). The basic idea behind this issue is to do all path resolution in the guest. What I mean by this is that wasi:filesystem interface surface area would never perform path resolution. Instead it would only have extremely primitive operations like "open this directory in this other directory" or "open this file in a directory" or "remove this file within this directory" -- things like that. Notably the name of the path being operated on would still be passed to APIs, but notably the path could not contain ., .., or /. This means that names passed to APIs are required to effectively be file names themselves.
Another aspect of this hypothetical new wasi:filesystem is that while symlinks would remain they would be followed in-guest as opposed to the host. Hosts would implement everything with the O_NOFOLLOW equivalent, for example. If you tried to open a file which is a symlink, that'd just return an error saying "nope, you gotta readlink that".
As-is, this sort of design I believe would neatly solve (1) and (2). Hosts would be required to validate paths, but the validation is trivial. Hosts would be required to pass O_NOFOLLOW everywhere and not operate on symlinks, but it should be quite easy to write conformance tests for this. If directories have no "parent" operation, then everything is trivially sandboxed because by definition guests can only recurse within the directories they are given. Hosts wanting to provide read-only views can also quite easily do so by rejecting any write-operations on such preopens. Overall, the theory at least, is that an API like this is significantly easier for hosts to implement and also a significantly easier-to-sandbox implementation as well.
As-is, this would additionally solve (3) and most (4). If guests do path resolution then they get to handle everything and correctly operating on multiple preopens is expected to fall out naturally. For example a guest-relative parent of /a exists (it's /, and it's just that if you try to open that it'll be a guest-level error). Additionally guests would be able to handle overlapping directories in at least a way that's consistent across hosts, albeit probably still a bit odd between guest languages.
API-wise what I'm thinking would more-or-less look the same as today's wasi:filesystem. Minor differences could include returning a descriptor from create-directory-at, returning resources for child directories in the stream returned by read-directory, removing all path-flags usage (nofollow is now the only option), adjusting relative APIs like link-at to perhaps say "put this descriptor at this path" instead of "put this path at this other path", etc. Largely things would look the same, but semantically they'd be totally different because the guest would have to do a lot more work.
Guests, by and large, expect a POSIX-like API to operate on files. Things like open, read, write, mkdir, etc. This proposal would place a significant burden on guests to implement this POSIX-like API on top of this hypothetical idea. Guests would have to do all the path resolution themselves, they'd have to maintain in-guest parent pointers for directories (if the host doesn't do that), and they'd have to handle symlinks by using readlink-at and then interpreting the result.
I do not want to paint this approach as a silver bullet, as it's far from it. Open questions in my mind include:
- How difficult would it be to actually build an API like this on existing hosts today? This
wasi:filesystemis a significant departure from the POSIX-like interfaces most hosts support today, for example. One counter-argument to this concern, though, is that a fully sandboxed host is already a significant implementation and my hunch is that this design ofwasi:filesystemwould be much easier to implement than that. - What would the performance of this API look like? My expectation is that naively this would be significantly slower than native hosts for traversing files. Opening
/a/b/c/d.txt, for example, would mean openingbwithin/a, then using that to openc, then using that to opend.txt, and if any of those is a symlink guests would have toreadlink-at-it and then handle the result. This is a significant departure from "probably one syscall on the host in fast paths today". For example Wasmtime will useO_RESOLVE_BENEATHwithopenattoday, but that's fundamentally incompatible with thiswasi:filesystemdesign. - Naively I expect that the implemetation in at least wasi-libc to be quite complicated of how to do all this. I don't know if it would be reasonable to expect that implementation complexity to be reflected into other guest languages not based on wasi-libc.
- I've no idea how we would transition from today's
wasi:filesystemto this hypothetical design. Best I can think of is "just deprecate everything" and add a bunch of parallel APIs. Not great, though.
Anyway, lots of possible upsides for this (every host can sandbox now!) and also lots of downsides (complicated for guests, still leaves lots of open questions for wasi:filesystem like "are paths case sensitive"). I wanted to write all of this down in an official issue at least as having this buried in #742 is probably not a good spot for this.
Activity
[-]wasi:filesytem alternative - force guests to do all path resolution[/-][+]wasi:filesystem alternative - force guests to do all path resolution[/+]alexcrichton commentedon Jun 20, 2026
To elaborate a bit on differences from today's WITs, here's how I would envision behavior/API changes for today's functions on
descriptorand such:pathor*-patharguments are required to be a "segment". notably cannot contain/or cannot contain../.as path elements. For examplefoo.txtis allowed, but./foo.txtis not. Nor is/foo.txt. The one exception to this is theold-pathargument to thesymlink-atAPI. As the content of the symlink this is not validated at all.path-flagsarguments are removed and hardcoded as nofollow-always-enabled.If it's possible to get away with it, I wouldn't propose adding a sort of
descriptor.parent() -> option<descriptor>API to find the parent directory (returningnonefor preopens). I say this because I don't know how it would be implemented on the host, but the lack of this API forces the guest to maintain a stack of descriptors for all open directories so they can resolve to..appropriately.For preopens I think something needs to change about the structure of the names. If a guest gets preopens for
.and/that's sort of nonsensical from a guest trying to view preopens as "just a single filesystem" as there's no relation to.and/. Without having thought about this too-too-hard, what I'd propose is something like:Notably this (a) imposes a tree structure, (b) forces guests to think of "one root fs", (c) enables overlap as is allowed today, and (d) keeps path conventions purely in the guest by only operating on segments. Hosts would have to do a little work to modify the "list of unstructured name plus dir" that they manage today, but modeling it as a something like this I would hope wouldn't be too bad.
lukewagner commentedon Jun 22, 2026
Moving path-resolution logic into the guest makes a bunch of sense, especially for these cases of assembling a VFS from multiple native directories. For this, what you sketched above sgtm!
To your performance-concern bullet though: I am also somewhat worried that, for cases where the host does just want to expose the whole filesystem or just one chroot-style directory (which both seem like valid and not-uncommon use cases) unconditionally doing all the path resolution in the guest with host-calls (and, I assume, syscalls) for each path segment might be a pretty significant performance penalty that in these cases doesn't buy the host or guest much value in exchange for the overhead.
One assumption I believe we all have is that most guest source code only wants to think about a single rooted filesystem; I think it's only in rather advanced/specialized virtualization scenarios where the guest code wants to "see below the hood" and think about preopens, etc. Thus, even if we want the path resolution to happen in the guest for the above reasons, it seems like the host's choice of how it wants to assemble this single rooted (possibly-virtual) filesystem.
Based on this, I wonder if we can reconcile these goals by defining two interfaces in
wasi:filesystem:preopensas you've sketched aboveroot(or some TBD better name), which also containsuse types.{descriptor, error-code}but has aopen: async func(path: string) -> result<descriptor, error-code>that does do path resolutionThen we say that the
wasi:cli/commandworld imports onlyroot, notpreopens, so that, by default our producer tools are spitting out components that assume path resolution is handled "on the other side" of the import call. But we also define a new worldwasi:cli/preopen-vfs(or some TBD better name) that exportsrootand importspreopens(and the other usual CLI stuff). And then WASI or wasi-sdk would publish a canonical component.wasmthat targetswasi:cli/preopen-vfs, implementingroot's path resolution in terms ofpreopensin guest code.So then if the host wants to expose the OS filesystem in a basic way, it provides a native
wasi:filesystem/rootimpl, or if the host wants to do something more advanced, it natively implementswasi:filesystem/preopensand automatically links in the canonicalpreopen-vfsimplementation to virtualizationwasi:filesystem/root. If we're doing host-/sys-call-per-segment anyways, the additional overhead of thepreopen-vfscomponent would hopefully be amortized.A few nice consequences fall out of this hybrid approach:
preopens, such as, e.g., "mounting" various future WASI stores as directories (analogous to what Emscripten's VFS does in the browser). I expect trying to address all these use cases in wasi-libc would feel like a bridge too far (b/c it adds overhead to all wasi-libc users), whereas if it's in a separate opt-in component (of which there can be several, targeting different worlds), it seems fair game.(I mentioned this here b/c it could help address the perf downside mentioned in the OP, but if it's too much of a tangent, happy to move discussion to a separate issue.)
badeend commentedon Jun 22, 2026
I'm slowly starting to like this idea. Though I have two main concerns:
Correctness
Path resolution logic is non-trivial. It needs to:
.&..segments (logically, not syntactically),..actually refers to,On the one hand, you could argue that this complexity is exactly why moving it into the guest is a good idea.
On the other hand, all guests need to implement this logic near-identically in order to stay interoperable with each other. Otherwise we can end up in a situation where symlinks created by guest A are unresolvable by guest B or vice-versa, despite running on the same host.
Whichever route we take, we need to make sure that the path resolution logic is fully specified as part of the WASI itself.
Performance
Naively resolving paths in user space takes at least
(N-1)*2 + 1syscalls, whereNis the total number of path segments:open+closepair for each intermediate segment, andopenfor the basename.Example: resolving a relatively modest path
/a/b/c/d/e/ftakes 11 syscalls. If symlinks are involved, that number could easily multiply.Requiring the resolution logic to live inside the guest strips away all possibilities for the host to accelerate this logic. It forces the worst-possible performance characteristics onto the wasm application, even when the host doesn't care about the additional security.
A valid use case (IMHO) for not needing the additional sandboxing is: running the WASI workload inside a Docker container, where the filesystem virtualization is already handled "one level up".
Split interface
I think we can solve these problems by splitting the interface into two aspects:
Abbreviated example:
Notice the layering:
paths::open-atis defined in terms ofcore::descriptor::open.paths::stat-atis in turn defined in terms ofpaths::open-atandcore::descriptor::stat.The POSIX
statfamily boils down to various incantations of the single WASIstatmethod:fstat(fd, ..)isfd.stat()fstatat(fd, path, ..)ispaths::stat-at(fd, path)(and in turnpaths::open-at(fd, path)?.stat())stat(path, ..)ispaths::stat-at(core::get-root(), path)Single root preopen
The example above also exposes the root preopen as a regular descriptor. Now that the
descriptorAPI has been drastically simplified and they are truly are "just" simple nodes in a tree, I don't think it adds significant implementation complexity for hosts to implement it this way. Realistically speaking, the guest just wants a single tree and doesn't care how it came to be there. This also resolves the ambiguity of having overlapping preopens.alexcrichton commentedon Jun 24, 2026
You both bring up excellent points! I want to write down a few thoughts of mine on perf/correctness before responding to the dual-interface ideas as well.
Performance
One thing I can mention is that for performance the big-O number of syscalls I think is how everything works today in Wasmtime when
RESOLVE_BENEATHisn't available. I believe cap-std (which Wasmtime uses) already does the whole resolve-symlinks-manually algorithm which @badeend linked. In that sense the per-segment interpretation is just simply required on macOS, Windows, other non-Linux OS-es, and Linux whenRESOLVE_BENEATHisn't available.With this information, a per-segment interface performs worse in three situations AFAIK:
RESOLVE_BENEATHis available (as the kernel can "do the sandboxing").chroot.Subjectively I'd say that these situations might be rare enough that it might not be worth bending over backwards for them. For (1) IMO we still need to move away from list-of-preopens to one-preopen-thingie meaning that to use
RESOLVE_BENEATHrequires the full filesystem defined on the host, aka no VFS. For (2) that clashes with multi-tenancy becausechrootis a process-wide change and additionally apparently requires super-user privledges according toman(although not on Linux I think). For (3) I'm not sure how much a completely un-sandboxed view will come up, but I'll note that we cannot provide an un-sandboxed view of Windows filesystems because we don't currently have any way to map all the drives (e.g.C:/,D:/, ...) into the guest.One possibly-saving-grace is that performance of actual operations on files (e.g. reads/writes/etc) are unaffected by this proposal. It's just path traversal that'd be affected, and even the naive benchmark of "process an entire directory recursively" wouldn't be affected too much since everything's already operating on a segment-at-a-time anyway. I don't mean to say performance does matter, but there might be a plausible case that this performance becomes a bit niche over time. I could be totally wrong on this though.
Correctness
I've no doubt implementing path resolution in a guest is going to be non-trivial, but I'll go back to my point (1) in the OP and point out that many hosts do not even attempt sandboxing today due to the complexity. To me that seems like one of the highest-priority motivations for a proposal like this -- enabling all hosts to reasonably implement a sandboxed filesystem.
Subjectively we also have a lot more code sharing in guests right now with wasi-libc than we do with hosts (which AFAIK there's pretty much 0 sharing of code between many pairwise hosts). The end state might be more hosts in the world than guest languages/libraries implementing
wasi:filesystem, but this is again an un-founded assertion that could be entirely wrong.For multiple interfaces, I like the idea of enabling the host to accelerate things where possible to ensure that we haven't hamstrung ourselves with this permanent performance penalty. I also like the idea of this being a separate interface to sort of "get the best of both worlds". That being said precisely how this interface is expected to be integrated and used is something that I'd want to clarify.
@lukewagner with your approach I'd be worried that this wouldn't actually move the needle much on guest/host complexity. Wasmtime, for example, can't provide the
rootinterface you'd like to be the default most of the time. Any non-Linux platform with more-than-one-preopen means that the OS can't be relied on for path resolution. Additionally wasi-libc, as an example, is such a core dependency that if it hardcodes usage ofrootas opposed topreopen-vfsI don't think there's going to be any reasonable way to change that. Basically as such a low-level implementation detail I'd be dubious of the viability of being able to swap out one implementation vs the other.I really like @badeend's idea though of a
core/pathssplit (naming TBD with a bikeshed) wherepathsis exclusively defined in terms ofcore. This still has the same question, though, of what does a core library like wasi-libc actually use? This also in theory has the same downsides of trying to swap one-vs-the-other with @lukewagner your idea.One way to thread that needle though would be optional imports, IMO. For example wasi-libc would unconditionally import
coreand assume it's only thing that can be assumed. Or, well, ok maybe this is optional too but that's for hosts that provide no filesystem whatsoever. For wasi-libc, though, thepathsinterface (or "thing that does path resolution externally") would be an optional import. Operations in wasi-libc would then conditionally use thepathsinterface if applicable, and otherwise it would fall back tocore. This only works for POSIX guests wherepathswould be defined to have POSIX semantics, but that covers all guests today anyway really.What would y'all think of that? Wasmtime, for example, would pretty rarely provide that
pathsinterface (if ever?) but it could readily provide thecoreinterface. Other hosts could (hopefully easily) provide thecoreinterface and never providepaths, and guests would work seamlessly (in theory) across all these hosts. Now for this to be viable I do think we need something to providepathssuch that it can actually be tested, and my best idea there would be achroot-ing Wasmtime which we'd have to build out -- but basically something that's only applicable in some specific situations as opposed to trying to move all historical usage to this.Orthogonally from all of this, in terms of actual API design of this proposal (the
coreinterface @badeend has dubbed), there's a few other clarifications/modifications I'd want to make:resource preopenidea is unnecessary. Thecore.descriptortype is so simple it is reasonable to expect hosts to implement that for VFS-style dirs. Given that I think this issue is actually still in concert with Drop explicit preopens #742 ("just have a single preopen") and it's mostly all about the path resolution side of things.get-initial-cwdI think will want to return anoption<descriptor>, not a path. Withcoretrying to defer all path resolution to the guest this would close another hole where an externally-specified "full path" makes its way to the guest.I also haven't though too too hard about Windows here in terms of open-handles-prevent-deletion. I don't know how difficult it will be to ensure that the guest can close all relevant handles when trying to delete a directory or file. My hunch is not the hardest thing in the world, but if things like
get-initial-cwdreturn a descriptor and the host tries to delete the cwd, things'll probably get a bit weird. (e.g. a handle on the host preventing the guest from deleting it)programmerjake commentedon Jun 24, 2026
one option to reduce the number of syscalls necessary is to have a userspace cache of recently accessed files/dirs so, ignoring stuff that gets dropped from the cache, you only need the syscalls the first time you resolve any particular path segment and can use the cached result the rest of the time. you can either assume your wasm engine has exclusive access to the files or use something like inotify to track creation/deletion/renames to know when the cache is out-of-date and/or update it.
programmerjake commentedon Jun 24, 2026
how would libc implement
getcwdwith that API? doesdescriptorhave a method to get its absolute path?sbc100 commentedon Jun 24, 2026
But we cannot make that assumption in the general case.. and there is no inotify mechanism just yet is there in WASI?
programmerjake commentedon Jun 24, 2026
I was referring to host-side caching.
sbc100 commentedon Jun 24, 2026
Ah yes, I assumed that that in above discussion of "userspace" we were talking about the guest wasm code and "syscalls" meant wasi syscalls.
Ericson2314 commentedon Jun 25, 2026
I don't do much with WASI (yet!) but I wanted to chime in that the basic idea here is how our VFS layer in Nix works.
We (independently) decided a few years ago that trying to allow the Host OS implementation to "accelerate" symlink following just wasn't worth the attendant complexity. and doing it ourselves with a little bit of caching and
opentat2(on Linux) just isn't so bad. We do however do support multi-segment operations forRESOLVE_BENEATHacceleration.Good luck!
CC @xokdvium
lukewagner commentedon Jun 28, 2026
The
core/pathssplit idea does seem like an interesting alternative worth considering. Ifpathsis made optional though and guests have to always implement their own path resolution anyways, it does reopen some of the concerns @badeend mentioned above.I do think that perhaps I didn't clearly explain what I was suggesting with the
root/preopenssplit though and it could help reconcile these tensions. In particular, in this part of @alexcrichton's concern:In what I was suggesting,
preopen-vfsisn't an interface that wasi-libc would have to know about, it's a component that implements therootinterface. Thus, wasi-libc would only ever callrootand the ability to transparently plug in apreopen-vfscomponent (that implementsrootby performing VFS path resolution in terms of the lower-levelpreopensinterface) is just component virtualizability as usual. Thus, regarding:I think, with the above clarification (
preopen-vfsis a component, not an interface), the net host complexity of the approach I'm suggesting is equivalent to thecore-with-optional-pathsapproach (in particular, a host would natively implementrootin the same scenarios that it would natively implementpaths). The main difference is that wasi-libc (and other filesystem abstractions) wouldn't ever need to do their own VFS path resolution; they could simply use theroot'sopenfunction. Given that we have N hosts and M wasi-libc-like-guest-libraries, thepreopen-vfscomponent basically gets to sit in the middle, avoiding duplication in either. There are still tradeoffs to discuss (like the complexity of introducing an extra component), but I think it's an interesting candidate.alexcrichton commentedon Jun 29, 2026
Oh sorry about that I definitely conflated yours/@badeend's comments together. I agree that's definitely a candidate for how to make this more viable, and it's nice to be able to have a single canonical component do "hard stuff" as opposed to trying to replicate that. Runtimes supporting
--dir-like flags (e.g. Wasmtime) would need to bake in this canonical component by default to handle--dirtransparently, where the CLI would compose-on-the-fly according to--dir, which would be kind of complicated, but also not the end of the world.A consequence though of this approach is:
The consequence here being that implementing sandboxed path-resolution on the host in a cross-platform manner is extremely difficult, and most runtimes don't even try today. While this scheme would simplify guests like wasi-libc a bit it'd still leave hosts in pretty much the same complicated spot they're in today (minus the VFS-related bits hosts are expected to implement).
Personally I think I'd still favor the split @badeend mentioned where wasi-libc optional-imports path resolution and then otherwise requires segment-only APIs with internal logic for path resolution. Effectively I'd personally rank the "make it more reasonable for hosts to implement the sandbox" as a higher priority.
alexcrichton commentedon Jul 13, 2026
Time has passed, I've thought about this some more, I've discussed this with some folks in meetings, and I want to provide some more thoughts on this.
Overall it seems clear to me that we're not in a position to pursue a design such as this. The open questions are sort of hard blockers for something like this and no one's really in a position to evaluate them. For example it's not clear what the performance impact of this proposal is, and the default otherwise thinking is "probably bad" as it'd do more syscalls. It's also not clear how difficult it would be to implement this inside of wasi-libc, for example. The difficulty in implementing this wasi-libc would be a key driver in answering the question of "what should wasi-libc import by default," but without this it's hard to answer that. Effectively while (to me at least) it seems laudable to try to make host implementations easier the general community is just not staffed and/or populated with folks with enough interest to carry the burden to push a design like this forward.
If a strategy like this cannot be done, then my thinking is that #742 is the way forward. Right now hosts and guests are all performing incorrect path resolution. Guests think they can pick a preopen and resolve a path within that, but they can't. Hosts think they can provide directories to a guest and the guest will do the path resolution correctly, but they don't. With #742 we'll at least move to a world where guests can't be wrong, they always resolve relative to the root. In this situation the onus is on hosts to implement path resolution correctly.
This issue, and my conclusion at this time, is that hosts will continue to not implement path resolution correctly, however. WIth #742 most hosts will probably continue to not even attempt to provide sandboxing. Additionally hosts like Wasmtime which provide a VFS-lookalike (e.g. multiple
--diroptions) will also not implement path resolution correctly, much for the same reasons that pursuing this issue is not viable (no one is interested in pursing this and/or has a need for it).In my opinion, however, that's at least still a little bit better than it is today. A host can in theory do correct path resolution and sandboxing, even if none currently do. This to me is demonstrably better than today's situation which is effectively un-salvagable -- doing path resolution correctly requires doing the slow thing we don't want to do (aka per-segment file/dir resolution) in guests. Perhaps one day in the future someone can write a component that imports this issue's proposed API (per-segment resolution + root) and provides the API of #742 and today (root + guest imports ability to resolve within this root). That could in theory be used to determine performance/etc, and maybe at that point it'll be much easier to distribute such a component. For example Wasmtime might be able to bundle this component and then automatically compose it in when the host has multiple directory mounts for a VFS or something like that.
Basically, tl;dr, I think the only viable option at this time is to close this issue and pursue #742. Notably deprecate
preopensas-is with a new interface that just provides a single root descriptor. Hosts will be left to fend for themselves to retain preexisting functionality, and that's the tradeoff to make.