github.com/bananabytelabs/wazero@v0.0.0-20240105073314-54b22a776da8/internal/sys/fs.go (about) 1 package sys 2 3 import ( 4 "io" 5 "io/fs" 6 "net" 7 8 "github.com/bananabytelabs/wazero/experimental/sys" 9 "github.com/bananabytelabs/wazero/internal/descriptor" 10 "github.com/bananabytelabs/wazero/internal/fsapi" 11 socketapi "github.com/bananabytelabs/wazero/internal/sock" 12 "github.com/bananabytelabs/wazero/internal/sysfs" 13 ) 14 15 const ( 16 FdStdin int32 = iota 17 FdStdout 18 FdStderr 19 // FdPreopen is the file descriptor of the first pre-opened directory. 20 // 21 // # Why file descriptor 3? 22 // 23 // While not specified, the most common WASI implementation, wasi-libc, 24 // expects POSIX style file descriptor allocation, where the lowest 25 // available number is used to open the next file. Since 1 and 2 are taken 26 // by stdout and stderr, the next is 3. 27 // - https://github.com/WebAssembly/WASI/issues/122 28 // - https://pubs.opengroup.org/onlinepubs/9699919799/functions/V2_chap02.html#tag_15_14 29 // - https://github.com/WebAssembly/wasi-libc/blob/wasi-sdk-16/libc-bottom-half/sources/preopens.c#L215 30 FdPreopen 31 ) 32 33 const modeDevice = fs.ModeDevice | 0o640 34 35 // FileEntry maps a path to an open file in a file system. 36 type FileEntry struct { 37 // Name is the name of the directory up to its pre-open, or the pre-open 38 // name itself when IsPreopen. 39 // 40 // # Notes 41 // 42 // - This can drift on rename. 43 // - This relates to the guest path, which is not the real file path 44 // except if the entire host filesystem was made available. 45 Name string 46 47 // IsPreopen is a directory that is lazily opened. 48 IsPreopen bool 49 50 // FS is the filesystem associated with the pre-open. 51 FS sys.FS 52 53 // File is always non-nil. 54 File fsapi.File 55 56 // direntCache is nil until DirentCache was called. 57 direntCache *DirentCache 58 } 59 60 // DirentCache gets or creates a DirentCache for this file or returns an error. 61 // 62 // # Errors 63 // 64 // A zero sys.Errno is success. The below are expected otherwise: 65 // - sys.ENOSYS: the implementation does not support this function. 66 // - sys.EBADF: the dir was closed or not readable. 67 // - sys.ENOTDIR: the file was not a directory. 68 // 69 // # Notes 70 // 71 // - See /RATIONALE.md for design notes. 72 func (f *FileEntry) DirentCache() (*DirentCache, sys.Errno) { 73 if dir := f.direntCache; dir != nil { 74 return dir, 0 75 } 76 77 // Require the file to be a directory vs a late error on the same. 78 if isDir, errno := f.File.IsDir(); errno != 0 { 79 return nil, errno 80 } else if !isDir { 81 return nil, sys.ENOTDIR 82 } 83 84 // Generate the dotEntries only once. 85 if dotEntries, errno := synthesizeDotEntries(f); errno != 0 { 86 return nil, errno 87 } else { 88 f.direntCache = &DirentCache{f: f.File, dotEntries: dotEntries} 89 } 90 91 return f.direntCache, 0 92 } 93 94 // DirentCache is a caching abstraction of sys.File Readdir. 95 // 96 // This is special-cased for "wasi_snapshot_preview1.fd_readdir", and may be 97 // unneeded, or require changes, to support preview1 or preview2. 98 // - The position of the dirents are serialized as `d_next`. For reasons 99 // described below, any may need to be re-read. This accepts any positions 100 // in the cache, rather than track the position of the last dirent. 101 // - dot entries ("." and "..") must be returned. See /RATIONALE.md for why. 102 // - An sys.Dirent Name is variable length, it could exceed memory size and 103 // need to be re-read. 104 // - Multiple dirents may be returned. It is more efficient to read from the 105 // underlying file in bulk vs one-at-a-time. 106 // 107 // The last results returned by Read are cached, but entries before that 108 // position are not. This support re-reading entries that couldn't fit into 109 // memory without accidentally caching all entries in a large directory. This 110 // approach is sometimes called a sliding window. 111 type DirentCache struct { 112 // f is the underlying file 113 f sys.File 114 115 // dotEntries are the "." and ".." entries added when the directory is 116 // initialized. 117 dotEntries []sys.Dirent 118 119 // dirents are the potentially unread directory entries. 120 // 121 // Internal detail: nil is different from zero length. Zero length is an 122 // exhausted directory (eof). nil means the re-read. 123 dirents []sys.Dirent 124 125 // countRead is the total count of dirents read since last rewind. 126 countRead uint64 127 128 // eof is true when the underlying file is at EOF. This avoids re-reading 129 // the directory when it is exhausted. Entires in an exhausted directory 130 // are not visible until it is rewound via calling Read with `pos==0`. 131 eof bool 132 } 133 134 // synthesizeDotEntries generates a slice of the two elements "." and "..". 135 func synthesizeDotEntries(f *FileEntry) ([]sys.Dirent, sys.Errno) { 136 dotIno, errno := f.File.Ino() 137 if errno != 0 { 138 return nil, errno 139 } 140 result := [2]sys.Dirent{} 141 result[0] = sys.Dirent{Name: ".", Ino: dotIno, Type: fs.ModeDir} 142 // See /RATIONALE.md for why we don't attempt to get an inode for ".." and 143 // why in wasi-libc this won't fan-out either. 144 result[1] = sys.Dirent{Name: "..", Ino: 0, Type: fs.ModeDir} 145 return result[:], 0 146 } 147 148 // exhaustedDirents avoids allocating empty slices. 149 var exhaustedDirents = [0]sys.Dirent{} 150 151 // Read is similar to and returns the same errors as `Readdir` on sys.File. 152 // The main difference is this caches entries returned, resulting in multiple 153 // valid positions to read from. 154 // 155 // When zero, `pos` means rewind to the beginning of this directory. This 156 // implies a rewind (Seek to zero on the underlying sys.File), unless the 157 // initial entries are still cached. 158 // 159 // When non-zero, `pos` is the zero based index of all dirents returned since 160 // last rewind. Only entries beginning at `pos` are cached for subsequent 161 // calls. A non-zero `pos` before the cache returns sys.ENOENT for reasons 162 // described on DirentCache documentation. 163 // 164 // Up to `n` entries are cached and returned. When `n` exceeds the cache, the 165 // difference are read from the underlying sys.File via `Readdir`. EOF is 166 // when `len(dirents)` returned are less than `n`. 167 func (d *DirentCache) Read(pos uint64, n uint32) (dirents []sys.Dirent, errno sys.Errno) { 168 switch { 169 case pos > d.countRead: // farther than read or negative coerced to uint64. 170 return nil, sys.ENOENT 171 case pos == 0 && d.dirents != nil: 172 // Rewind if we have already read entries. This allows us to see new 173 // entries added after the directory was opened. 174 if _, errno = d.f.Seek(0, io.SeekStart); errno != 0 { 175 return 176 } 177 d.dirents = nil // dump cache 178 d.countRead = 0 179 } 180 181 if n == 0 { 182 return // special case no entries. 183 } 184 185 if d.dirents == nil { 186 // Always populate dot entries, which makes min len(dirents) == 2. 187 d.dirents = d.dotEntries 188 d.countRead = 2 189 d.eof = false 190 191 if countToRead := int(n - 2); countToRead <= 0 { 192 return 193 } else if dirents, errno = d.f.Readdir(countToRead); errno != 0 { 194 return 195 } else if countRead := len(dirents); countRead > 0 { 196 d.eof = countRead < countToRead 197 d.dirents = append(d.dotEntries, dirents...) 198 d.countRead += uint64(countRead) 199 } 200 201 return d.cachedDirents(n), 0 202 } 203 204 // Reset our cache to the first entry being read. 205 cacheStart := d.countRead - uint64(len(d.dirents)) 206 if pos < cacheStart { 207 // We don't currently allow reads before our cache because Seek(0) is 208 // the only portable way. Doing otherwise requires skipping, which we 209 // won't do unless wasi-testsuite starts requiring it. Implementing 210 // this would allow re-reading a large directory, so care would be 211 // needed to not buffer the entire directory in memory while skipping. 212 errno = sys.ENOENT 213 return 214 } else if posInCache := pos - cacheStart; posInCache != 0 { 215 if uint64(len(d.dirents)) == posInCache { 216 // Avoid allocation re-slicing to zero length. 217 d.dirents = exhaustedDirents[:] 218 } else { 219 d.dirents = d.dirents[posInCache:] 220 } 221 } 222 223 // See if we need more entries. 224 if countToRead := int(n) - len(d.dirents); countToRead > 0 && !d.eof { 225 // Try to read more, which could fail. 226 if dirents, errno = d.f.Readdir(countToRead); errno != 0 { 227 return 228 } 229 230 // Append the next read entries if we weren't at EOF. 231 if countRead := len(dirents); countRead > 0 { 232 d.eof = countRead < countToRead 233 d.dirents = append(d.dirents, dirents...) 234 d.countRead += uint64(countRead) 235 } 236 } 237 238 return d.cachedDirents(n), 0 239 } 240 241 // cachedDirents returns up to `n` dirents from the cache. 242 func (d *DirentCache) cachedDirents(n uint32) []sys.Dirent { 243 direntCount := uint32(len(d.dirents)) 244 switch { 245 case direntCount == 0: 246 return nil 247 case direntCount > n: 248 return d.dirents[:n] 249 } 250 return d.dirents 251 } 252 253 type FSContext struct { 254 // rootFS is the root ("/") mount. 255 rootFS sys.FS 256 257 // openedFiles is a map of file descriptor numbers (>=FdPreopen) to open files 258 // (or directories) and defaults to empty. 259 // TODO: This is unguarded, so not goroutine-safe! 260 openedFiles FileTable 261 } 262 263 // FileTable is a specialization of the descriptor.Table type used to map file 264 // descriptors to file entries. 265 type FileTable = descriptor.Table[int32, *FileEntry] 266 267 // RootFS returns a possibly unimplemented root filesystem. Any files that 268 // should be added to the table should be inserted via InsertFile. 269 // 270 // TODO: This is only used by GOOS=js and tests: Remove when we remove GOOS=js 271 // (after Go 1.22 is released). 272 func (c *FSContext) RootFS() sys.FS { 273 if rootFS := c.rootFS; rootFS == nil { 274 return sys.UnimplementedFS{} 275 } else { 276 return rootFS 277 } 278 } 279 280 // LookupFile returns a file if it is in the table. 281 func (c *FSContext) LookupFile(fd int32) (*FileEntry, bool) { 282 return c.openedFiles.Lookup(fd) 283 } 284 285 // OpenFile opens the file into the table and returns its file descriptor. 286 // The result must be closed by CloseFile or Close. 287 func (c *FSContext) OpenFile(fs sys.FS, path string, flag sys.Oflag, perm fs.FileMode) (int32, sys.Errno) { 288 if f, errno := fs.OpenFile(path, flag, perm); errno != 0 { 289 return 0, errno 290 } else { 291 fe := &FileEntry{FS: fs, File: fsapi.Adapt(f)} 292 if path == "/" || path == "." { 293 fe.Name = "" 294 } else { 295 fe.Name = path 296 } 297 if newFD, ok := c.openedFiles.Insert(fe); !ok { 298 return 0, sys.EBADF 299 } else { 300 return newFD, 0 301 } 302 } 303 } 304 305 // Renumber assigns the file pointed by the descriptor `from` to `to`. 306 func (c *FSContext) Renumber(from, to int32) sys.Errno { 307 fromFile, ok := c.openedFiles.Lookup(from) 308 if !ok || to < 0 { 309 return sys.EBADF 310 } else if fromFile.IsPreopen { 311 return sys.ENOTSUP 312 } 313 314 // If toFile is already open, we close it to prevent windows lock issues. 315 // 316 // The doc is unclear and other implementations do nothing for already-opened To FDs. 317 // https://github.com/WebAssembly/WASI/blob/snapshot-01/phases/snapshot/docs.md#-fd_renumberfd-fd-to-fd---errno 318 // https://github.com/bytecodealliance/wasmtime/blob/main/crates/wasi-common/src/snapshots/preview_1.rs#L531-L546 319 if toFile, ok := c.openedFiles.Lookup(to); ok { 320 if toFile.IsPreopen { 321 return sys.ENOTSUP 322 } 323 _ = toFile.File.Close() 324 } 325 326 c.openedFiles.Delete(from) 327 if !c.openedFiles.InsertAt(fromFile, to) { 328 return sys.EBADF 329 } 330 return 0 331 } 332 333 // SockAccept accepts a sock.TCPConn into the file table and returns its file 334 // descriptor. 335 func (c *FSContext) SockAccept(sockFD int32, nonblock bool) (int32, sys.Errno) { 336 var sock socketapi.TCPSock 337 if e, ok := c.LookupFile(sockFD); !ok || !e.IsPreopen { 338 return 0, sys.EBADF // Not a preopen 339 } else if sock, ok = e.File.(socketapi.TCPSock); !ok { 340 return 0, sys.EBADF // Not a sock 341 } 342 343 conn, errno := sock.Accept() 344 if errno != 0 { 345 return 0, errno 346 } 347 348 fe := &FileEntry{File: fsapi.Adapt(conn)} 349 350 if nonblock { 351 if errno = fe.File.SetNonblock(true); errno != 0 { 352 _ = conn.Close() 353 return 0, errno 354 } 355 } 356 357 if newFD, ok := c.openedFiles.Insert(fe); !ok { 358 return 0, sys.EBADF 359 } else { 360 return newFD, 0 361 } 362 } 363 364 // CloseFile returns any error closing the existing file. 365 func (c *FSContext) CloseFile(fd int32) (errno sys.Errno) { 366 f, ok := c.openedFiles.Lookup(fd) 367 if !ok { 368 return sys.EBADF 369 } 370 if errno = f.File.Close(); errno != 0 { 371 return errno 372 } 373 c.openedFiles.Delete(fd) 374 return errno 375 } 376 377 // Close implements io.Closer 378 func (c *FSContext) Close() (err error) { 379 // Close any files opened in this context 380 c.openedFiles.Range(func(fd int32, entry *FileEntry) bool { 381 if errno := entry.File.Close(); errno != 0 { 382 err = errno // This means err returned == the last non-nil error. 383 } 384 return true 385 }) 386 // A closed FSContext cannot be reused so clear the state. 387 c.openedFiles = FileTable{} 388 return 389 } 390 391 // InitFSContext initializes a FSContext with stdio streams and optional 392 // pre-opened filesystems and TCP listeners. 393 func (c *Context) InitFSContext( 394 stdin io.Reader, 395 stdout, stderr io.Writer, 396 fs []sys.FS, guestPaths []string, 397 tcpListeners []*net.TCPListener, 398 ) (err error) { 399 inFile, err := stdinFileEntry(stdin) 400 if err != nil { 401 return err 402 } 403 c.fsc.openedFiles.Insert(inFile) 404 outWriter, err := stdioWriterFileEntry("stdout", stdout) 405 if err != nil { 406 return err 407 } 408 c.fsc.openedFiles.Insert(outWriter) 409 errWriter, err := stdioWriterFileEntry("stderr", stderr) 410 if err != nil { 411 return err 412 } 413 c.fsc.openedFiles.Insert(errWriter) 414 415 for i, fs := range fs { 416 guestPath := guestPaths[i] 417 418 if StripPrefixesAndTrailingSlash(guestPath) == "" { 419 // Default to bind to '/' when guestPath is effectively empty. 420 guestPath = "/" 421 c.fsc.rootFS = fs 422 } 423 c.fsc.openedFiles.Insert(&FileEntry{ 424 FS: fs, 425 Name: guestPath, 426 IsPreopen: true, 427 File: &lazyDir{fs: fs}, 428 }) 429 } 430 431 for _, tl := range tcpListeners { 432 c.fsc.openedFiles.Insert(&FileEntry{IsPreopen: true, File: fsapi.Adapt(sysfs.NewTCPListenerFile(tl))}) 433 } 434 return nil 435 } 436 437 // StripPrefixesAndTrailingSlash skips any leading "./" or "/" such that the 438 // result index begins with another string. A result of "." coerces to the 439 // empty string "" because the current directory is handled by the guest. 440 // 441 // Results are the offset/len pair which is an optimization to avoid re-slicing 442 // overhead, as this function is called for every path operation. 443 // 444 // Note: Relative paths should be handled by the guest, as that's what knows 445 // what the current directory is. However, paths that escape the current 446 // directory e.g. "../.." have been found in `tinygo test` and this 447 // implementation takes care to avoid it. 448 func StripPrefixesAndTrailingSlash(path string) string { 449 // strip trailing slashes 450 pathLen := len(path) 451 for ; pathLen > 0 && path[pathLen-1] == '/'; pathLen-- { 452 } 453 454 pathI := 0 455 loop: 456 for pathI < pathLen { 457 switch path[pathI] { 458 case '/': 459 pathI++ 460 case '.': 461 nextI := pathI + 1 462 if nextI < pathLen && path[nextI] == '/' { 463 pathI = nextI + 1 464 } else if nextI == pathLen { 465 pathI = nextI 466 } else { 467 break loop 468 } 469 default: 470 break loop 471 } 472 } 473 return path[pathI:pathLen] 474 }