github.com/tetratelabs/wazero@v1.2.1/internal/sys/fs.go (about) 1 package sys 2 3 import ( 4 "io" 5 "io/fs" 6 "net" 7 "path" 8 "syscall" 9 10 "github.com/tetratelabs/wazero/internal/descriptor" 11 "github.com/tetratelabs/wazero/internal/fsapi" 12 "github.com/tetratelabs/wazero/internal/platform" 13 socketapi "github.com/tetratelabs/wazero/internal/sock" 14 "github.com/tetratelabs/wazero/internal/sysfs" 15 ) 16 17 const ( 18 FdStdin int32 = iota 19 FdStdout 20 FdStderr 21 // FdPreopen is the file descriptor of the first pre-opened directory. 22 // 23 // # Why file descriptor 3? 24 // 25 // While not specified, the most common WASI implementation, wasi-libc, 26 // expects POSIX style file descriptor allocation, where the lowest 27 // available number is used to open the next file. Since 1 and 2 are taken 28 // by stdout and stderr, the next is 3. 29 // - https://github.com/WebAssembly/WASI/issues/122 30 // - https://pubs.opengroup.org/onlinepubs/9699919799/functions/V2_chap02.html#tag_15_14 31 // - https://github.com/WebAssembly/wasi-libc/blob/wasi-sdk-16/libc-bottom-half/sources/preopens.c#L215 32 FdPreopen 33 ) 34 35 const modeDevice = fs.ModeDevice | 0o640 36 37 // FileEntry maps a path to an open file in a file system. 38 type FileEntry struct { 39 // Name is the name of the directory up to its pre-open, or the pre-open 40 // name itself when IsPreopen. 41 // 42 // # Notes 43 // 44 // - This can drift on rename. 45 // - This relates to the guest path, which is not the real file path 46 // except if the entire host filesystem was made available. 47 Name string 48 49 // IsPreopen is a directory that is lazily opened. 50 IsPreopen bool 51 52 // FS is the filesystem associated with the pre-open. 53 FS fsapi.FS 54 55 // File is always non-nil. 56 File fsapi.File 57 } 58 59 const direntBufSize = 16 60 61 // Readdir is the status of a prior fs.ReadDirFile call. 62 type Readdir struct { 63 // cursor is the current position in the buffer. 64 cursor uint64 65 66 // countRead is the total count of files read including Dirents. 67 // 68 // Notes: 69 // 70 // * countRead is the index of the next file in the list. This is 71 // also the value that Cookie returns, so it should always be 72 // higher or equal than the cookie given in Rewind. 73 // 74 // * this can overflow to negative, which means our implementation 75 // doesn't support writing greater than max int64 entries. 76 // countRead uint64 77 countRead uint64 78 79 // dirents is a fixed buffer of size direntBufSize. Notably, 80 // directory listing are not rewindable, so we keep entries around in case 81 // the caller mis-estimated their buffer and needs a few still cached. 82 // 83 // Note: This is wasi-specific and needs to be refactored. 84 // In wasi preview1, dot and dot-dot entries are required to exist, but the 85 // reverse is true for preview2. More importantly, preview2 holds separate 86 // stateful dir-entry-streams per file. 87 dirents []fsapi.Dirent 88 89 // dirInit seeks and reset the provider for dirents to the beginning 90 // and returns an initial batch (e.g. dot directories). 91 dirInit func() ([]fsapi.Dirent, syscall.Errno) 92 93 // dirReader fetches a new batch of direntBufSize elements. 94 dirReader func(n uint64) ([]fsapi.Dirent, syscall.Errno) 95 } 96 97 // NewReaddir is a constructor for Readdir. It takes a dirInit 98 func NewReaddir( 99 dirInit func() ([]fsapi.Dirent, syscall.Errno), 100 dirReader func(n uint64) ([]fsapi.Dirent, syscall.Errno), 101 ) (*Readdir, syscall.Errno) { 102 d := &Readdir{dirReader: dirReader, dirInit: dirInit} 103 return d, d.init() 104 } 105 106 // init resets the cursor and invokes the dirInit, dirReader 107 // methods to reset the internal state of the Readdir struct. 108 // 109 // Note: this is different from Reset, because it will not short-circuit 110 // when cursor is already 0, but it will force an unconditional reload. 111 func (d *Readdir) init() syscall.Errno { 112 d.cursor = 0 113 d.countRead = 0 114 // Reset the buffer to the initial state. 115 initialDirents, errno := d.dirInit() 116 if errno != 0 { 117 return errno 118 } 119 if len(initialDirents) > direntBufSize { 120 return syscall.EINVAL 121 } 122 d.dirents = initialDirents 123 // Fill the buffer with more data. 124 count := direntBufSize - len(initialDirents) 125 if count == 0 { 126 // No need to fill up the buffer further. 127 return 0 128 } 129 dirents, errno := d.dirReader(uint64(count)) 130 if errno != 0 { 131 return errno 132 } 133 d.dirents = append(d.dirents, dirents...) 134 return 0 135 } 136 137 // newReaddirFromFileEntry is a constructor for Readdir that takes a FileEntry to initialize. 138 func newReaddirFromFileEntry(f *FileEntry) (*Readdir, syscall.Errno) { 139 // Generate the dotEntries only once and return it many times in the dirInit closure. 140 dotEntries, errno := synthesizeDotEntries(f) 141 if errno != 0 { 142 return nil, errno 143 } 144 dirInit := func() ([]fsapi.Dirent, syscall.Errno) { 145 // Ensure we always rewind to the beginning when we re-init. 146 if _, errno := f.File.Seek(0, io.SeekStart); errno != 0 { 147 return nil, errno 148 } 149 // Return the dotEntries that we have already generated outside the closure. 150 return dotEntries, 0 151 } 152 dirReader := func(n uint64) ([]fsapi.Dirent, syscall.Errno) { return f.File.Readdir(int(n)) } 153 return NewReaddir(dirInit, dirReader) 154 } 155 156 // synthesizeDotEntries generates a slice of the two elements "." and "..". 157 func synthesizeDotEntries(f *FileEntry) (result []fsapi.Dirent, errno syscall.Errno) { 158 dotIno, errno := f.File.Ino() 159 if errno != 0 { 160 return nil, errno 161 } 162 result = append(result, fsapi.Dirent{Name: ".", Ino: dotIno, Type: fs.ModeDir}) 163 dotDotIno := uint64(0) 164 if !f.IsPreopen && f.Name != "." { 165 if st, errno := f.FS.Stat(path.Dir(f.Name)); errno != 0 { 166 return nil, errno 167 } else { 168 dotDotIno = st.Ino 169 } 170 } 171 result = append(result, fsapi.Dirent{Name: "..", Ino: dotDotIno, Type: fs.ModeDir}) 172 return result, 0 173 } 174 175 // Reset seeks the internal cursor to 0 and refills the buffer. 176 func (d *Readdir) Reset() syscall.Errno { 177 if d.countRead == 0 { 178 return 0 179 } 180 return d.init() 181 } 182 183 // Skip is equivalent to calling n times Advance. 184 func (d *Readdir) Skip(n uint64) { 185 end := d.countRead + n 186 var err syscall.Errno = 0 187 for d.countRead < end && err == 0 { 188 err = d.Advance() 189 } 190 } 191 192 // Cookie returns a cookie representing the current state of the ReadDir struct. 193 // 194 // Note: this returns the countRead field, but it is an implementation detail. 195 func (d *Readdir) Cookie() uint64 { 196 return d.countRead 197 } 198 199 // Rewind seeks the internal cursor to the state represented by the cookie. 200 // It returns a syscall.Errno if the cursor was reset and an I/O error occurred while trying to re-init. 201 func (d *Readdir) Rewind(cookie int64) syscall.Errno { 202 unsignedCookie := uint64(cookie) 203 switch { 204 case cookie < 0 || unsignedCookie > d.countRead: 205 // the cookie can neither be negative nor can it be larger than countRead. 206 return syscall.EINVAL 207 case cookie == 0 && d.countRead == 0: 208 return 0 209 case cookie == 0 && d.countRead != 0: 210 // This means that there was a previous call to the dir, but cookie is reset. 211 // This happens when the program calls rewinddir, for example: 212 // https://github.com/WebAssembly/wasi-libc/blob/659ff414560721b1660a19685110e484a081c3d4/libc-bottom-half/cloudlibc/src/libc/dirent/rewinddir.c#L10-L12 213 return d.Reset() 214 case unsignedCookie < d.countRead: 215 if cookie/direntBufSize != int64(d.countRead)/direntBufSize { 216 // The cookie is not 0, but it points into a window before the current one. 217 return syscall.ENOSYS 218 } 219 // We are allowed to rewind back to a previous offset within the current window. 220 d.countRead = unsignedCookie 221 d.cursor = d.countRead % direntBufSize 222 return 0 223 default: 224 // The cookie is valid. 225 return 0 226 } 227 } 228 229 // Peek emits the current value. 230 // It returns syscall.ENOENT when there are no entries left in the directory. 231 func (d *Readdir) Peek() (*fsapi.Dirent, syscall.Errno) { 232 switch { 233 case d.cursor == uint64(len(d.dirents)): 234 // We're past the buf size, fill it up again. 235 dirents, errno := d.dirReader(direntBufSize) 236 if errno != 0 { 237 return nil, errno 238 } 239 d.dirents = append(d.dirents, dirents...) 240 fallthrough 241 default: // d.cursor < direntBufSize FIXME 242 if d.cursor == uint64(len(d.dirents)) { 243 return nil, syscall.ENOENT 244 } 245 dirent := &d.dirents[d.cursor] 246 return dirent, 0 247 } 248 } 249 250 // Advance advances the internal counters and indices to the next value. 251 // It also empties and refill the buffer with the next set of values when the internal cursor 252 // reaches the end of it. 253 func (d *Readdir) Advance() syscall.Errno { 254 if d.cursor == uint64(len(d.dirents)) { 255 return syscall.ENOENT 256 } 257 d.cursor++ 258 d.countRead++ 259 return 0 260 } 261 262 type FSContext struct { 263 // rootFS is the root ("/") mount. 264 rootFS fsapi.FS 265 266 // openedFiles is a map of file descriptor numbers (>=FdPreopen) to open files 267 // (or directories) and defaults to empty. 268 // TODO: This is unguarded, so not goroutine-safe! 269 openedFiles FileTable 270 271 // readdirs is a map of numeric identifiers to Readdir structs 272 // and defaults to empty. 273 // TODO: This is unguarded, so not goroutine-safe! 274 readdirs ReaddirTable 275 } 276 277 // FileTable is a specialization of the descriptor.Table type used to map file 278 // descriptors to file entries. 279 type FileTable = descriptor.Table[int32, *FileEntry] 280 281 // ReaddirTable is a specialization of the descriptor.Table type used to map file 282 // descriptors to Readdir structs. 283 type ReaddirTable = descriptor.Table[int32, *Readdir] 284 285 // RootFS returns the underlying filesystem. Any files that should be added to 286 // the table should be inserted via InsertFile. 287 func (c *FSContext) RootFS() fsapi.FS { 288 return c.rootFS 289 } 290 291 // OpenFile opens the file into the table and returns its file descriptor. 292 // The result must be closed by CloseFile or Close. 293 func (c *FSContext) OpenFile(fs fsapi.FS, path string, flag int, perm fs.FileMode) (int32, syscall.Errno) { 294 if f, errno := fs.OpenFile(path, flag, perm); errno != 0 { 295 return 0, errno 296 } else { 297 fe := &FileEntry{FS: fs, File: f} 298 if path == "/" || path == "." { 299 fe.Name = "" 300 } else { 301 fe.Name = path 302 } 303 if newFD, ok := c.openedFiles.Insert(fe); !ok { 304 return 0, syscall.EBADF 305 } else { 306 return newFD, 0 307 } 308 } 309 } 310 311 // SockAccept accepts a socketapi.TCPConn into the file table and returns 312 // its file descriptor. 313 func (c *FSContext) SockAccept(sockFD int32, nonblock bool) (int32, syscall.Errno) { 314 var sock socketapi.TCPSock 315 if e, ok := c.LookupFile(sockFD); !ok || !e.IsPreopen { 316 return 0, syscall.EBADF // Not a preopen 317 } else if sock, ok = e.File.(socketapi.TCPSock); !ok { 318 return 0, syscall.EBADF // Not a sock 319 } 320 321 var conn socketapi.TCPConn 322 var errno syscall.Errno 323 if conn, errno = sock.Accept(); errno != 0 { 324 return 0, errno 325 } else if nonblock { 326 if errno = conn.SetNonblock(true); errno != 0 { 327 _ = conn.Close() 328 return 0, errno 329 } 330 } 331 332 fe := &FileEntry{File: conn} 333 if newFD, ok := c.openedFiles.Insert(fe); !ok { 334 return 0, syscall.EBADF 335 } else { 336 return newFD, 0 337 } 338 } 339 340 // LookupFile returns a file if it is in the table. 341 func (c *FSContext) LookupFile(fd int32) (*FileEntry, bool) { 342 return c.openedFiles.Lookup(fd) 343 } 344 345 // LookupReaddir returns a Readdir struct or creates an empty one if it was not present. 346 // 347 // Note: this currently assumes that idx == fd, where fd is the file descriptor of the directory. 348 // CloseFile will delete this idx from the internal store. In the future, idx may be independent 349 // of a file fd, and the idx may have to be disposed with an explicit CloseReaddir. 350 func (c *FSContext) LookupReaddir(idx int32, f *FileEntry) (*Readdir, syscall.Errno) { 351 if item, _ := c.readdirs.Lookup(idx); item != nil { 352 return item, 0 353 } else { 354 item, err := newReaddirFromFileEntry(f) 355 if err != 0 { 356 return nil, err 357 } 358 ok := c.readdirs.InsertAt(item, idx) 359 if !ok { 360 return nil, syscall.EINVAL 361 } 362 return item, 0 363 } 364 } 365 366 // CloseReaddir delete the Readdir struct at the given index 367 // 368 // Note: Currently only necessary in tests. In the future, the idx will have to be disposed explicitly, 369 // unless we maintain a map fd -> []idx, and we let CloseFile close all the idx in []idx. 370 func (c *FSContext) CloseReaddir(idx int32) { 371 c.readdirs.Delete(idx) 372 } 373 374 // Renumber assigns the file pointed by the descriptor `from` to `to`. 375 func (c *FSContext) Renumber(from, to int32) syscall.Errno { 376 fromFile, ok := c.openedFiles.Lookup(from) 377 if !ok || to < 0 { 378 return syscall.EBADF 379 } else if fromFile.IsPreopen { 380 return syscall.ENOTSUP 381 } 382 383 // If toFile is already open, we close it to prevent windows lock issues. 384 // 385 // The doc is unclear and other implementations do nothing for already-opened To FDs. 386 // https://github.com/WebAssembly/WASI/blob/snapshot-01/phases/snapshot/docs.md#-fd_renumberfd-fd-to-fd---errno 387 // https://github.com/bytecodealliance/wasmtime/blob/main/crates/wasi-common/src/snapshots/preview_1.rs#L531-L546 388 if toFile, ok := c.openedFiles.Lookup(to); ok { 389 if toFile.IsPreopen { 390 return syscall.ENOTSUP 391 } 392 _ = toFile.File.Close() 393 } 394 395 c.openedFiles.Delete(from) 396 if !c.openedFiles.InsertAt(fromFile, to) { 397 return syscall.EBADF 398 } 399 return 0 400 } 401 402 // CloseFile returns any error closing the existing file. 403 func (c *FSContext) CloseFile(fd int32) syscall.Errno { 404 f, ok := c.openedFiles.Lookup(fd) 405 if !ok { 406 return syscall.EBADF 407 } 408 c.openedFiles.Delete(fd) 409 c.readdirs.Delete(fd) 410 return platform.UnwrapOSError(f.File.Close()) 411 } 412 413 // Close implements io.Closer 414 func (c *FSContext) Close() (err error) { 415 // Close any files opened in this context 416 c.openedFiles.Range(func(fd int32, entry *FileEntry) bool { 417 if errno := entry.File.Close(); errno != 0 { 418 err = errno // This means err returned == the last non-nil error. 419 } 420 return true 421 }) 422 // A closed FSContext cannot be reused so clear the state instead of 423 // using Reset. 424 c.openedFiles = FileTable{} 425 c.readdirs = ReaddirTable{} 426 return 427 } 428 429 // NewFSContext creates a FSContext with stdio streams and an optional 430 // pre-opened filesystem. 431 // 432 // If `preopened` is not UnimplementedFS, it is inserted into 433 // the file descriptor table as FdPreopen. 434 func (c *Context) NewFSContext( 435 stdin io.Reader, 436 stdout, stderr io.Writer, 437 rootFS fsapi.FS, 438 tcpListeners []*net.TCPListener, 439 ) (err error) { 440 c.fsc.rootFS = rootFS 441 inFile, err := stdinFileEntry(stdin) 442 if err != nil { 443 return err 444 } 445 c.fsc.openedFiles.Insert(inFile) 446 outWriter, err := stdioWriterFileEntry("stdout", stdout) 447 if err != nil { 448 return err 449 } 450 c.fsc.openedFiles.Insert(outWriter) 451 errWriter, err := stdioWriterFileEntry("stderr", stderr) 452 if err != nil { 453 return err 454 } 455 c.fsc.openedFiles.Insert(errWriter) 456 457 if _, ok := rootFS.(fsapi.UnimplementedFS); ok { 458 // don't add to the pre-opens 459 } else if comp, ok := rootFS.(*sysfs.CompositeFS); ok { 460 preopens := comp.FS() 461 for i, p := range comp.GuestPaths() { 462 c.fsc.openedFiles.Insert(&FileEntry{ 463 FS: preopens[i], 464 Name: p, 465 IsPreopen: true, 466 File: &lazyDir{fs: rootFS}, 467 }) 468 } 469 } else { 470 c.fsc.openedFiles.Insert(&FileEntry{ 471 FS: rootFS, 472 Name: "/", 473 IsPreopen: true, 474 File: &lazyDir{fs: rootFS}, 475 }) 476 } 477 478 for _, tl := range tcpListeners { 479 c.fsc.openedFiles.Insert(&FileEntry{IsPreopen: true, File: sysfs.NewTCPListenerFile(tl)}) 480 } 481 return nil 482 }