github.com/tetratelabs/wazero@v1.2.1/internal/sysfs/rootfs.go (about) 1 package sysfs 2 3 import ( 4 "fmt" 5 "io" 6 "io/fs" 7 "strings" 8 "syscall" 9 10 "github.com/tetratelabs/wazero/internal/fsapi" 11 ) 12 13 func NewRootFS(fs []fsapi.FS, guestPaths []string) (fsapi.FS, error) { 14 switch len(fs) { 15 case 0: 16 return fsapi.UnimplementedFS{}, nil 17 case 1: 18 if StripPrefixesAndTrailingSlash(guestPaths[0]) == "" { 19 return fs[0], nil 20 } 21 } 22 23 ret := &CompositeFS{ 24 string: stringFS(fs, guestPaths), 25 fs: make([]fsapi.FS, len(fs)), 26 guestPaths: make([]string, len(fs)), 27 cleanedGuestPaths: make([]string, len(fs)), 28 rootGuestPaths: map[string]int{}, 29 rootIndex: -1, 30 } 31 32 copy(ret.guestPaths, guestPaths) 33 copy(ret.fs, fs) 34 35 for i, guestPath := range guestPaths { 36 // Clean the prefix in the same way path matches will. 37 cleaned := StripPrefixesAndTrailingSlash(guestPath) 38 if cleaned == "" { 39 if ret.rootIndex != -1 { 40 return nil, fmt.Errorf("multiple root filesystems are invalid: %s", ret.string) 41 } 42 ret.rootIndex = i 43 } else if strings.HasPrefix(cleaned, "..") { 44 // ../ mounts are special cased and aren't returned in a directory 45 // listing, so we can ignore them for now. 46 } else if strings.Contains(cleaned, "/") { 47 return nil, fmt.Errorf("only single-level guest paths allowed: %s", ret.string) 48 } else { 49 ret.rootGuestPaths[cleaned] = i 50 } 51 ret.cleanedGuestPaths[i] = cleaned 52 } 53 54 // Ensure there is always a root match to keep runtime logic simpler. 55 if ret.rootIndex == -1 { 56 ret.rootIndex = len(fs) 57 ret.cleanedGuestPaths = append(ret.cleanedGuestPaths, "") 58 ret.fs = append(ret.fs, &fakeRootFS{}) 59 } 60 return ret, nil 61 } 62 63 type CompositeFS struct { 64 fsapi.UnimplementedFS 65 // string is cached for convenience. 66 string string 67 // fs is index-correlated with cleanedGuestPaths 68 fs []fsapi.FS 69 // guestPaths are the original paths supplied by the end user, cleaned as 70 // cleanedGuestPaths. 71 guestPaths []string 72 // cleanedGuestPaths to match in precedence order, ascending. 73 cleanedGuestPaths []string 74 // rootGuestPaths are cleanedGuestPaths that exist directly under root, such as 75 // "tmp". 76 rootGuestPaths map[string]int 77 // rootIndex is the index in fs that is the root filesystem 78 rootIndex int 79 } 80 81 // String implements fmt.Stringer 82 func (c *CompositeFS) String() string { 83 return c.string 84 } 85 86 func stringFS(fs []fsapi.FS, guestPaths []string) string { 87 var ret strings.Builder 88 ret.WriteString("[") 89 writeMount(&ret, fs[0], guestPaths[0]) 90 for i, f := range fs[1:] { 91 ret.WriteString(" ") 92 writeMount(&ret, f, guestPaths[i+1]) 93 } 94 ret.WriteString("]") 95 return ret.String() 96 } 97 98 func writeMount(ret *strings.Builder, f fsapi.FS, guestPath string) { 99 ret.WriteString(f.String()) 100 ret.WriteString(":") 101 ret.WriteString(guestPath) 102 if _, ok := f.(*readFS); ok { 103 ret.WriteString(":ro") 104 } 105 } 106 107 // GuestPaths returns the underlying pre-open paths in original order. 108 func (c *CompositeFS) GuestPaths() (guestPaths []string) { 109 return c.guestPaths 110 } 111 112 // FS returns the underlying filesystems in original order. 113 func (c *CompositeFS) FS() (fs []fsapi.FS) { 114 fs = make([]fsapi.FS, len(c.guestPaths)) 115 copy(fs, c.fs) 116 return 117 } 118 119 // OpenFile implements the same method as documented on api.FS 120 func (c *CompositeFS) OpenFile(path string, flag int, perm fs.FileMode) (f fsapi.File, err syscall.Errno) { 121 matchIndex, relativePath := c.chooseFS(path) 122 123 f, err = c.fs[matchIndex].OpenFile(relativePath, flag, perm) 124 if err != 0 { 125 return 126 } 127 128 // Ensure the root directory listing includes any prefix mounts. 129 if matchIndex == c.rootIndex { 130 switch path { 131 case ".", "/", "": 132 if len(c.rootGuestPaths) > 0 { 133 f = &openRootDir{path: path, c: c, f: f} 134 } 135 } 136 } 137 return 138 } 139 140 // An openRootDir is a root directory open for reading, which has mounts inside 141 // of it. 142 type openRootDir struct { 143 fsapi.DirFile 144 145 path string 146 c *CompositeFS 147 f fsapi.File // the directory file itself 148 dirents []fsapi.Dirent // the directory contents 149 direntsI int // the read offset, an index into the files slice 150 } 151 152 // Ino implements the same method as documented on fsapi.File 153 func (d *openRootDir) Ino() (uint64, syscall.Errno) { 154 return d.f.Ino() 155 } 156 157 // Stat implements the same method as documented on fsapi.File 158 func (d *openRootDir) Stat() (fsapi.Stat_t, syscall.Errno) { 159 return d.f.Stat() 160 } 161 162 // Seek implements the same method as documented on fsapi.File 163 func (d *openRootDir) Seek(offset int64, whence int) (newOffset int64, errno syscall.Errno) { 164 if offset != 0 || whence != io.SeekStart { 165 errno = syscall.ENOSYS 166 return 167 } 168 d.dirents = nil 169 d.direntsI = 0 170 return d.f.Seek(offset, whence) 171 } 172 173 // Readdir implements the same method as documented on fsapi.File 174 func (d *openRootDir) Readdir(count int) (dirents []fsapi.Dirent, errno syscall.Errno) { 175 if d.dirents == nil { 176 if errno = d.readdir(); errno != 0 { 177 return 178 } 179 } 180 181 // logic similar to go:embed 182 n := len(d.dirents) - d.direntsI 183 if n == 0 { 184 return 185 } 186 if count > 0 && n > count { 187 n = count 188 } 189 dirents = make([]fsapi.Dirent, n) 190 for i := range dirents { 191 dirents[i] = d.dirents[d.direntsI+i] 192 } 193 d.direntsI += n 194 return 195 } 196 197 func (d *openRootDir) readdir() (errno syscall.Errno) { 198 // readDir reads the directory fully into d.dirents, replacing any entries that 199 // correspond to prefix matches or appending them to the end. 200 if d.dirents, errno = d.f.Readdir(-1); errno != 0 { 201 return 202 } 203 204 remaining := make(map[string]int, len(d.c.rootGuestPaths)) 205 for k, v := range d.c.rootGuestPaths { 206 remaining[k] = v 207 } 208 209 for i := range d.dirents { 210 e := d.dirents[i] 211 if fsI, ok := remaining[e.Name]; ok { 212 if d.dirents[i], errno = d.rootEntry(e.Name, fsI); errno != 0 { 213 return 214 } 215 delete(remaining, e.Name) 216 } 217 } 218 219 var di fsapi.Dirent 220 for n, fsI := range remaining { 221 if di, errno = d.rootEntry(n, fsI); errno != 0 { 222 return 223 } 224 d.dirents = append(d.dirents, di) 225 } 226 return 227 } 228 229 // Sync implements the same method as documented on fsapi.File 230 func (d *openRootDir) Sync() syscall.Errno { 231 return d.f.Sync() 232 } 233 234 // Datasync implements the same method as documented on fsapi.File 235 func (d *openRootDir) Datasync() syscall.Errno { 236 return d.f.Datasync() 237 } 238 239 // Chmod implements the same method as documented on fsapi.File 240 func (d *openRootDir) Chmod(fs.FileMode) syscall.Errno { 241 return syscall.ENOSYS 242 } 243 244 // Chown implements the same method as documented on fsapi.File 245 func (d *openRootDir) Chown(int, int) syscall.Errno { 246 return syscall.ENOSYS 247 } 248 249 // Utimens implements the same method as documented on fsapi.File 250 func (d *openRootDir) Utimens(*[2]syscall.Timespec) syscall.Errno { 251 return syscall.ENOSYS 252 } 253 254 // Close implements fs.File 255 func (d *openRootDir) Close() syscall.Errno { 256 return d.f.Close() 257 } 258 259 func (d *openRootDir) rootEntry(name string, fsI int) (fsapi.Dirent, syscall.Errno) { 260 if st, errno := d.c.fs[fsI].Stat("."); errno != 0 { 261 return fsapi.Dirent{}, errno 262 } else { 263 return fsapi.Dirent{Name: name, Ino: st.Ino, Type: st.Mode.Type()}, 0 264 } 265 } 266 267 // Lstat implements the same method as documented on api.FS 268 func (c *CompositeFS) Lstat(path string) (fsapi.Stat_t, syscall.Errno) { 269 matchIndex, relativePath := c.chooseFS(path) 270 return c.fs[matchIndex].Lstat(relativePath) 271 } 272 273 // Stat implements the same method as documented on api.FS 274 func (c *CompositeFS) Stat(path string) (fsapi.Stat_t, syscall.Errno) { 275 matchIndex, relativePath := c.chooseFS(path) 276 return c.fs[matchIndex].Stat(relativePath) 277 } 278 279 // Mkdir implements the same method as documented on api.FS 280 func (c *CompositeFS) Mkdir(path string, perm fs.FileMode) syscall.Errno { 281 matchIndex, relativePath := c.chooseFS(path) 282 return c.fs[matchIndex].Mkdir(relativePath, perm) 283 } 284 285 // Chmod implements the same method as documented on api.FS 286 func (c *CompositeFS) Chmod(path string, perm fs.FileMode) syscall.Errno { 287 matchIndex, relativePath := c.chooseFS(path) 288 return c.fs[matchIndex].Chmod(relativePath, perm) 289 } 290 291 // Chown implements the same method as documented on api.FS 292 func (c *CompositeFS) Chown(path string, uid, gid int) syscall.Errno { 293 matchIndex, relativePath := c.chooseFS(path) 294 return c.fs[matchIndex].Chown(relativePath, uid, gid) 295 } 296 297 // Lchown implements the same method as documented on api.FS 298 func (c *CompositeFS) Lchown(path string, uid, gid int) syscall.Errno { 299 matchIndex, relativePath := c.chooseFS(path) 300 return c.fs[matchIndex].Lchown(relativePath, uid, gid) 301 } 302 303 // Rename implements the same method as documented on api.FS 304 func (c *CompositeFS) Rename(from, to string) syscall.Errno { 305 fromFS, fromPath := c.chooseFS(from) 306 toFS, toPath := c.chooseFS(to) 307 if fromFS != toFS { 308 return syscall.ENOSYS // not yet anyway 309 } 310 return c.fs[fromFS].Rename(fromPath, toPath) 311 } 312 313 // Readlink implements the same method as documented on api.FS 314 func (c *CompositeFS) Readlink(path string) (string, syscall.Errno) { 315 matchIndex, relativePath := c.chooseFS(path) 316 return c.fs[matchIndex].Readlink(relativePath) 317 } 318 319 // Link implements the same method as documented on api.FS 320 func (c *CompositeFS) Link(oldName, newName string) syscall.Errno { 321 fromFS, oldNamePath := c.chooseFS(oldName) 322 toFS, newNamePath := c.chooseFS(newName) 323 if fromFS != toFS { 324 return syscall.ENOSYS // not yet anyway 325 } 326 return c.fs[fromFS].Link(oldNamePath, newNamePath) 327 } 328 329 // Utimens implements the same method as documented on api.FS 330 func (c *CompositeFS) Utimens(path string, times *[2]syscall.Timespec, symlinkFollow bool) syscall.Errno { 331 matchIndex, relativePath := c.chooseFS(path) 332 return c.fs[matchIndex].Utimens(relativePath, times, symlinkFollow) 333 } 334 335 // Symlink implements the same method as documented on api.FS 336 func (c *CompositeFS) Symlink(oldName, link string) (err syscall.Errno) { 337 fromFS, oldNamePath := c.chooseFS(oldName) 338 toFS, linkPath := c.chooseFS(link) 339 if fromFS != toFS { 340 return syscall.ENOSYS // not yet anyway 341 } 342 return c.fs[fromFS].Symlink(oldNamePath, linkPath) 343 } 344 345 // Truncate implements the same method as documented on api.FS 346 func (c *CompositeFS) Truncate(path string, size int64) syscall.Errno { 347 matchIndex, relativePath := c.chooseFS(path) 348 return c.fs[matchIndex].Truncate(relativePath, size) 349 } 350 351 // Rmdir implements the same method as documented on api.FS 352 func (c *CompositeFS) Rmdir(path string) syscall.Errno { 353 matchIndex, relativePath := c.chooseFS(path) 354 return c.fs[matchIndex].Rmdir(relativePath) 355 } 356 357 // Unlink implements the same method as documented on api.FS 358 func (c *CompositeFS) Unlink(path string) syscall.Errno { 359 matchIndex, relativePath := c.chooseFS(path) 360 return c.fs[matchIndex].Unlink(relativePath) 361 } 362 363 // chooseFS chooses the best fs and the relative path to use for the input. 364 func (c *CompositeFS) chooseFS(path string) (matchIndex int, relativePath string) { 365 matchIndex = -1 366 matchPrefixLen := 0 367 pathI, pathLen := stripPrefixesAndTrailingSlash(path) 368 369 // Last is the highest precedence, so we iterate backwards. The last longest 370 // match wins. e.g. the pre-open "tmp" wins vs "" regardless of order. 371 for i := len(c.fs) - 1; i >= 0; i-- { 372 prefix := c.cleanedGuestPaths[i] 373 if eq, match := hasPathPrefix(path, pathI, pathLen, prefix); eq { 374 // When the input equals the prefix, there cannot be a longer match 375 // later. The relative path is the fsapi.FS root, so return empty 376 // string. 377 matchIndex = i 378 relativePath = "" 379 return 380 } else if match { 381 // Check to see if this is a longer match 382 prefixLen := len(prefix) 383 if prefixLen > matchPrefixLen || matchIndex == -1 { 384 matchIndex = i 385 matchPrefixLen = prefixLen 386 } 387 } // Otherwise, keep looking for a match 388 } 389 390 // Now, we know the path != prefix, but it matched an existing fs, because 391 // setup ensures there's always a root filesystem. 392 393 // If this was a root path match the cleaned path is the relative one to 394 // pass to the underlying filesystem. 395 if matchPrefixLen == 0 { 396 // Avoid re-slicing when the input is already clean 397 if pathI == 0 && len(path) == pathLen { 398 relativePath = path 399 } else { 400 relativePath = path[pathI:pathLen] 401 } 402 return 403 } 404 405 // Otherwise, it is non-root match: the relative path is past "$prefix/" 406 pathI += matchPrefixLen + 1 // e.g. prefix=foo, path=foo/bar -> bar 407 relativePath = path[pathI:pathLen] 408 return 409 } 410 411 // hasPathPrefix compares an input path against a prefix, both cleaned by 412 // stripPrefixesAndTrailingSlash. This returns a pair of eq, match to allow an 413 // early short circuit on match. 414 // 415 // Note: This is case-sensitive because POSIX paths are compared case 416 // sensitively. 417 func hasPathPrefix(path string, pathI, pathLen int, prefix string) (eq, match bool) { 418 matchLen := pathLen - pathI 419 if prefix == "" { 420 return matchLen == 0, true // e.g. prefix=, path=foo 421 } 422 423 prefixLen := len(prefix) 424 // reset pathLen temporarily to represent the length to match as opposed to 425 // the length of the string (that may contain leading slashes). 426 if matchLen == prefixLen { 427 if pathContainsPrefix(path, pathI, prefixLen, prefix) { 428 return true, true // e.g. prefix=bar, path=bar 429 } 430 return false, false 431 } else if matchLen < prefixLen { 432 return false, false // e.g. prefix=fooo, path=foo 433 } 434 435 if path[pathI+prefixLen] != '/' { 436 return false, false // e.g. prefix=foo, path=fooo 437 } 438 439 // Not equal, but maybe a match. e.g. prefix=foo, path=foo/bar 440 return false, pathContainsPrefix(path, pathI, prefixLen, prefix) 441 } 442 443 // pathContainsPrefix is faster than strings.HasPrefix even if we didn't cache 444 // the index,len. See benchmarks. 445 func pathContainsPrefix(path string, pathI, prefixLen int, prefix string) bool { 446 for i := 0; i < prefixLen; i++ { 447 if path[pathI] != prefix[i] { 448 return false // e.g. prefix=bar, path=foo or foo/bar 449 } 450 pathI++ 451 } 452 return true // e.g. prefix=foo, path=foo or foo/bar 453 } 454 455 func StripPrefixesAndTrailingSlash(path string) string { 456 pathI, pathLen := stripPrefixesAndTrailingSlash(path) 457 return path[pathI:pathLen] 458 } 459 460 // stripPrefixesAndTrailingSlash skips any leading "./" or "/" such that the 461 // result index begins with another string. A result of "." coerces to the 462 // empty string "" because the current directory is handled by the guest. 463 // 464 // Results are the offset/len pair which is an optimization to avoid re-slicing 465 // overhead, as this function is called for every path operation. 466 // 467 // Note: Relative paths should be handled by the guest, as that's what knows 468 // what the current directory is. However, paths that escape the current 469 // directory e.g. "../.." have been found in `tinygo test` and this 470 // implementation takes care to avoid it. 471 func stripPrefixesAndTrailingSlash(path string) (pathI, pathLen int) { 472 // strip trailing slashes 473 pathLen = len(path) 474 for ; pathLen > 0 && path[pathLen-1] == '/'; pathLen-- { 475 } 476 477 pathI = 0 478 loop: 479 for pathI < pathLen { 480 switch path[pathI] { 481 case '/': 482 pathI++ 483 case '.': 484 nextI := pathI + 1 485 if nextI < pathLen && path[nextI] == '/' { 486 pathI = nextI + 1 487 } else if nextI == pathLen { 488 pathI = nextI 489 } else { 490 break loop 491 } 492 default: 493 break loop 494 } 495 } 496 return 497 } 498 499 type fakeRootFS struct { 500 fsapi.UnimplementedFS 501 } 502 503 // OpenFile implements the same method as documented on api.FS 504 func (fakeRootFS) OpenFile(path string, flag int, perm fs.FileMode) (fsapi.File, syscall.Errno) { 505 switch path { 506 case ".", "/", "": 507 return fakeRootDir{}, 0 508 } 509 return nil, syscall.ENOENT 510 } 511 512 type fakeRootDir struct { 513 fsapi.DirFile 514 } 515 516 // Ino implements the same method as documented on fsapi.File 517 func (fakeRootDir) Ino() (uint64, syscall.Errno) { 518 return 0, 0 519 } 520 521 // Stat implements the same method as documented on fsapi.File 522 func (fakeRootDir) Stat() (fsapi.Stat_t, syscall.Errno) { 523 return fsapi.Stat_t{Mode: fs.ModeDir, Nlink: 1}, 0 524 } 525 526 // Readdir implements the same method as documented on fsapi.File 527 func (fakeRootDir) Readdir(int) (dirents []fsapi.Dirent, errno syscall.Errno) { 528 return // empty 529 } 530 531 // Sync implements the same method as documented on fsapi.File 532 func (fakeRootDir) Sync() syscall.Errno { 533 return 0 534 } 535 536 // Datasync implements the same method as documented on fsapi.File 537 func (fakeRootDir) Datasync() syscall.Errno { 538 return 0 539 } 540 541 // Chmod implements the same method as documented on fsapi.File 542 func (fakeRootDir) Chmod(fs.FileMode) syscall.Errno { 543 return syscall.ENOSYS 544 } 545 546 // Chown implements the same method as documented on fsapi.File 547 func (fakeRootDir) Chown(int, int) syscall.Errno { 548 return syscall.ENOSYS 549 } 550 551 // Utimens implements the same method as documented on fsapi.File 552 func (fakeRootDir) Utimens(*[2]syscall.Timespec) syscall.Errno { 553 return syscall.ENOSYS 554 } 555 556 // Close implements the same method as documented on fsapi.File 557 func (fakeRootDir) Close() syscall.Errno { 558 return 0 559 }