feat(stdlib): route os/require file IO through a virtual filesystem#302
feat(stdlib): route os/require file IO through a virtual filesystem#302davydog187 wants to merge 6 commits into
Conversation
Filesystem-touching stdlib functions now operate against an in-memory virtual filesystem carried on Lua.VM.State instead of refusing or reaching the host disk. Integrates the ivarvong/vfs VFS.Mountable protocol with the in-memory VFS.Memory backend as the default. - State.new/0 seeds an empty in-memory VFS; vfs_read/write/rm/exists?/ mount helpers thread the returned backend struct forward. - os.tmpname returns a virtual path; new os.remove and os.rename operate against state.vfs and never touch the host. - The require searcher consults the VFS first (the resolved path plus a /lua/deps-anchored path), then falls back to host File.read so existing set_lua_paths/package.path workflows keep working. - Lua.write_file/3, Lua.put_dep/3, and Lua.mount/3 let embedding hosts seed files and mount backends. Plan: A48 Closes #297
| # Run "mix help deps" to learn about dependencies. | ||
| defp deps do | ||
| [ | ||
| {:vfs, github: "ivarvong/vfs", ref: "32d2ab618ec12c16fe4f675b5ee8b563c660dd69"}, |
There was a problem hiding this comment.
Ivar will be cutting a release for me this week
The require searcher silently fell back to File.read/1, letting any VM that un-sandboxes require reach the host disk and bypass the virtual filesystem. Default the VFS to be the only backing store; an embedding host now opts into the host fallback explicitly via set_lua_paths/2, and the VFS is always consulted first so seeded modules win. Plan: A48
Automated review — round 1Solid, well-documented slice: the VFS threading discipline through Blocker
Because Major
Real Lua 5.3 returns Declared Minor
Automated round-1 review; a human will make the final call. |
os.remove/os.rename and the require searcher funnel paths through the
VFS, which raises ArgumentError on non-absolute paths. Guard the
State.vfs_read/vfs_write/vfs_rm boundary so a relative path surfaces as
a standard {:error, %VFS.Error{}} (enoent) instead of aborting the
evaluation, matching real Lua's (nil, "<path>: No such file or
directory") contract.
Also drop the unreachable message: nil clause in vfs_error_message/2,
short-circuit absolute require patterns to a single VFS read, and bump
the declared elixir floor to ~> 1.18 to match the vfs dependency.
Plan: A48
Automated review — round 2Round-1's two correctness fixes landed cleanly: relative paths now go through Two things remain. Blocker (carryover, unresolved)
Major
Minor
Automated round-2 review; a human makes the final call. |
Treat os.rename(x, x) as a successful no-op so a self-rename no longer reads/writes/removes the same path and leaves nothing behind, matching POSIX rename(2). Attribute rename failures to the path that actually failed (the destination on a failed write) rather than always the source, and append the conventional POSIX errno as the third return value of os.remove/os.rename on failure to match the Lua 5.3 contract. Plan: A48
Automated review — round 3Round-2's items all landed: self-rename is now a guarded no-op ( Two release-path blockers remain. Blocker (carryover, still unresolved)
Blocker (new)
Minor
Nit
The functional slice (State.vfs threading, os ops, require searcher, populate/mount API) is correct and well-tested. The gating issues are both about the release pipeline, not runtime behavior. Automated round-3 review; a human makes the final call. |
VFS sandbox — route os/require file IO through a virtual filesystem
Plan: .agents/plans/A48-vfs-sandbox.md
Closes #297
Goal
Make filesystem-touching
os/requireoperations safe by default by running them against an in-memory virtual filesystem instead of refusing or reaching the host disk. Integratesivarvong/vfs(aVFS.Mountableprotocol with pluggable backends), defaulting toVFS.Memory. Gives embedding hosts an API to seed files and mount backends, and uses/lua/depsas the dependency root forrequire.This PR ships the smallest coherent green slice (dep + State.vfs + os ops + require searcher + populate/mount API). The
io.*rewire is deferred — see Out of scope.Success criteria
vfsadded tomix.exsas a git dep pinned toref: 32d2ab618ec12c16fe4f675b5ee8b563c660dd69, matchingmix.lockentry,mix deps.getsucceeds.Lua.VM.Statehas avfsfield defaulting to an in-memoryVFS(VFS.new/0+VFS.mount/3withVFS.Memory.new(%{})),@type tupdated, seeded byState.new/0. (verified:test/lua/vfs_test.exs"the default VM has an empty in-memory VFS")LuaAPI to write a file (write_file/3,put_dep/3) and mount a backend (mount/3), each returning an updated%Lua{}. (verified: doctests +test/lua/vfs_test.exs)os.tmpname,os.remove,os.renameoperate againststate.vfsand thread the updated struct back; none touch the host. (verified:test/lua/vm/stdlib/os_test.exs)requireresolves modules from the VFS (resolved path +/lua/deps-anchored), with hostFile.readfallback so existingset_lua_paths/package.pathworkflows keep working; a module seeded via the new API is loadable. (verified:test/lua/vfs_test.exs+ existing require/luassert tests stay green)mix formatclean;mix compile --warnings-as-errorspasses.mix testgreen (2105 passed, 19 skipped, 1 excluded; 0 failed).mix test --only lua53shows no regression (17 passed, 12 skipped, 0 failed).Changes
```
lib/lua.ex | 55 +++++++++++++++++++++++++++
lib/lua/vm/state.ex | 81 ++++++++++++++++++++++++++++++++++++++--
lib/lua/vm/stdlib.ex | 61 ++++++++++++++++++++++++++-----
lib/lua/vm/stdlib/os.ex | 65 ++++++++++++++++++++++++++++----
mix.exs | 1 +
mix.lock | 1 +
test/lua/vm/stdlib/os_test.exs | 45 ++++++++++++++++++++++
test/lua/vfs_test.exs | new
```
Verification output (tail)
```
mix test -> Result: 2105 passed (59 doctests, 51 properties, 1995 tests), 19 skipped, 1 excluded
mix test --only lua53 -> Result: 17 passed, 12 skipped, 2096 excluded
```
Out of scope
io.*library (still a stub) — deferred follow-up.@default_sandboxdeny-list (os.remove/rename/tmpname/require/packagestay sandboxed by default; hosts opt out viasandboxed:/exclude:).files.lua,attrib.lua,verybig.lua,main.lua) green.🤖 Generated with Claude Code