Skip to content

Commit b24cc97

Browse files
committed
Merge pull request #16469 from ayushr2:gofer-ino-passthrough
PiperOrigin-RevId: 1009305671
2 parents 8ab9b43 + 8ead67b commit b24cc97

21 files changed

Lines changed: 474 additions & 67 deletions

File tree

‎g3doc/user_guide/filesystem.md‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,16 @@ operations. It communicates with the Gofer process (via RPCs) to perform
9393
filesystem operations on its behalf. This increases security but comes with a
9494
performance trade-off.
9595

96+
## Inode numbers
97+
98+
Files on gofer mounts (the rootfs and bind mounts) report their host inode
99+
numbers. Host device numbers are mapped to sandbox-private device numbers, so
100+
files on different host filesystems under one mount report different `st_dev`
101+
values, as with Linux overlayfs. With `--inode-numbers=virtual`, the sandbox
102+
instead assigns sequential inode numbers and a single device number per mount,
103+
which costs memory for every file the sandbox has ever seen and a `stat(2)` call
104+
for every entry returned by `getdents(2)`.
105+
96106
## Shared root filesystem
97107

98108
The root filesystem is where the image is extracted and is not generally

‎pkg/lisafs/message.go‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1695,7 +1695,9 @@ func (g *Getdents64Req) String() string {
16951695
return fmt.Sprintf("Getdents64Req{DirFD: %d, Count: %d}", g.DirFD, g.Count)
16961696
}
16971697

1698-
// Dirent64 is analogous to struct linux_dirent64.
1698+
// Dirent64 is analogous to struct linux_dirent64. DevMinor and DevMajor are
1699+
// the device numbers of the entry's file, or zero if the server does not
1700+
// report them.
16991701
type Dirent64 struct {
17001702
Ino primitive.Uint64
17011703
DevMinor primitive.Uint32

‎pkg/lisafs/testsuite/testsuite.go‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -703,6 +703,10 @@ func testGetdents(ctx context.Context, t *testing.T, tester Tester, root lisafs.
703703
if uint64(dirent.Ino) != stat.Ino {
704704
t.Errorf("dirent ino of %s is incorrect: want %d, got %d", dirent.Name, stat.Ino, dirent.Ino)
705705
}
706+
// Servers may omit device numbers.
707+
if dirent.DevMinor == 0 && dirent.DevMajor == 0 {
708+
continue
709+
}
706710
if uint32(dirent.DevMinor) != stat.DevMinor {
707711
t.Errorf("dirent dev minor of %s is incorrect: want %d, got %d", dirent.Name, stat.DevMinor, dirent.DevMinor)
708712
}

‎pkg/sentry/fsimpl/gofer/directfs_inode.go‎

Lines changed: 14 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,11 @@ func (fs *filesystem) newDirectfsDentry(controlFD int) (*dentry, error) {
134134
}
135135
isDir := uint32(stat.Mode)&linux.FileTypeMask == linux.ModeDirectory
136136
inoKey := inoKeyFromUnixStatx(&stat)
137+
devMinor, err := fs.devMinorFromKey(inoKey)
138+
if err != nil {
139+
_ = unix.Close(controlFD)
140+
return nil, err
141+
}
137142

138143
// Common case. Performance hack which is used to allocate the dentry
139144
// and its inode together in the heap. This will help reduce allocations and memory
@@ -160,6 +165,7 @@ func (fs *filesystem) newDirectfsDentry(controlFD int) (*dentry, error) {
160165
fs: fs,
161166
inoKey: inoKey,
162167
ino: fs.inoFromKey(inoKey),
168+
devMinor: devMinor,
163169
rdevMajor: stat.Rdev_major,
164170
rdevMinor: stat.Rdev_minor,
165171
mode: atomicbitops.FromUint32(uint32(stat.Mode)),
@@ -854,9 +860,12 @@ func (i *directfsInode) getDirentsLocked(recordDirent func(name string, key inoK
854860
}
855861

856862
return fsutil.ForEachDirent(readFD, func(ino uint64, off int64, ftype uint8, name string, reclen uint16) {
857-
// We also want the device ID, which annoyingly incurs an additional
858-
// syscall per dirent.
859-
// TODO(gvisor.dev/issue/6665): Get rid of per-dirent stat.
863+
if !i.fs.opts.virtualInodeNumbers {
864+
recordDirent(name, inoKey{ino: ino}, ftype)
865+
return
866+
}
867+
// We also want the device ID, which incurs an additional syscall per
868+
// dirent.
860869
stat, err := fsutil.StatAt(i.controlFD, name)
861870
if err != nil {
862871
log.Warningf("Getdent64: skipping file %q with failed stat, err: %v", path.Join(genericDebugPathname(i.fs, d), name), err)
@@ -919,22 +928,8 @@ func (i *directfsInode) restoreFile(ctx context.Context, controlFD int, opts *vf
919928
return fmt.Errorf("failed to stat %q in mount %q: %w", genericDebugPathname(i.fs, d), i.fs.iopts.UniqueID, err)
920929
}
921930
i.controlFD = controlFD
922-
// We do not preserve inoKey across checkpoint/restore, so:
923-
//
924-
// - We must assume that the host filesystem did not change in a way that
925-
// would invalidate dentries, since we can't revalidate dentries by
926-
// checking inoKey.
927-
//
928-
// - We need to associate the new inoKey with the existing d.ino.
929-
i.inoKey = inoKeyFromUnixStatx(&stat)
930-
i.fs.inoMu.Lock()
931-
i.fs.inoByKey[i.inoKey] = i.ino
932-
i.fs.inoMu.Unlock()
933-
// inodeByKey contains non-directory inodes only.
934-
if !d.isDir() {
935-
i.fs.inodeMu.Lock()
936-
i.fs.inodeByKey[i.inoKey] = &i.inode
937-
i.fs.inodeMu.Unlock()
931+
if err := i.restoreInoKey(inoKeyFromUnixStatx(&stat)); err != nil {
932+
return err
938933
}
939934

940935
// Check metadata stability before updating metadata.

‎pkg/sentry/fsimpl/gofer/directory.go‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,7 @@ func (fs *filesystem) newSyntheticDentry(opts *createSyntheticOpts) *dentry {
130130
inode: &inode{
131131
fs: fs,
132132
ino: fs.nextIno(),
133+
devMinor: fs.devMinor,
133134
mode: atomicbitops.FromUint32(uint32(opts.mode)),
134135
uid: atomicbitops.FromUint32(uint32(opts.kuid)),
135136
gid: atomicbitops.FromUint32(uint32(opts.kgid)),

‎pkg/sentry/fsimpl/gofer/gofer.go‎

Lines changed: 152 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
// dentry.handleMu
3232
// dentry.dataMu
3333
// filesystem.ancestryMu
34+
// filesystem.devMu
3435
// filesystem.inoMu
3536
// filesystem.inodeMu
3637
// specialFileFD.mu
@@ -91,6 +92,7 @@ const (
9192
moptDisableFileHandleSharing = "disable_file_handle_sharing"
9293
moptDisableFifoOpen = "disable_fifo_open"
9394
moptCharDevicePolicy = "char_device_policy"
95+
moptVirtualInodeNumbers = "virtual_inode_numbers"
9496

9597
// Directfs options.
9698
moptDirectfs = "directfs"
@@ -211,7 +213,9 @@ type filesystem struct {
211213
// clock is a realtime clock used to set timestamps in file operations.
212214
clock ktime.Clock
213215

214-
// devMinor is the filesystem's minor device number. devMinor is immutable.
216+
// devMinor is the filesystem's minor device number. It is used for
217+
// synthetic files, and for all files if opts.virtualInodeNumbers.
218+
// devMinor is immutable.
215219
devMinor uint32
216220

217221
// root is the root dentry. root is immutable.
@@ -241,9 +245,18 @@ type filesystem struct {
241245
syncableDentries dentryList
242246
specialFileFDs specialFDList
243247

248+
// devMinorByHostDev maps host device numbers, as encoded by unix.Mkdev,
249+
// to minor device numbers assigned to files on those devices. It is only
250+
// used if !opts.virtualInodeNumbers. Host device numbers may change
251+
// across checkpoint/restore, so devMinorByHostDev is rebuilt on restore.
252+
// devMinorByHostDev is protected by devMu.
253+
devMu sync.RWMutex `state:"nosave"`
254+
devMinorByHostDev map[uint64]uint32
255+
244256
// inoByKey maps previously-observed device ID and host inode numbers to
245-
// internal inode numbers assigned to those files. inoByKey is not preserved
246-
// across checkpoint/restore because inode numbers may be reused between
257+
// internal inode numbers assigned to those files. It is only used if
258+
// opts.virtualInodeNumbers. inoByKey is not preserved across
259+
// checkpoint/restore because inode numbers may be reused between
247260
// different gofer processes, so inode numbers may be repeated for different
248261
// files across checkpoint/restore. inoByKey is protected by inoMu.
249262
inoMu sync.Mutex `state:"nosave"`
@@ -348,6 +361,12 @@ type filesystemOptions struct {
348361
// via the gofer, or a combination of both.
349362
charDevicePolicy charDevicePolicy
350363

364+
// If virtualInodeNumbers is true, files are assigned sequential inode
365+
// numbers, recorded in filesystem.inoByKey, and the filesystem's device
366+
// number. Otherwise files report their host inode numbers, and their host
367+
// device numbers are mapped to sandbox-private device numbers.
368+
virtualInodeNumbers bool
369+
351370
// directfs holds options for directfs mode.
352371
directfs directfsOpts
353372
}
@@ -605,6 +624,10 @@ func (fstype FilesystemType) GetFilesystem(ctx context.Context, vfsObj *vfs.Virt
605624
delete(mopts, moptDirectfs)
606625
fsopts.directfs.enabled = true
607626
}
627+
if _, ok := mopts[moptVirtualInodeNumbers]; ok {
628+
delete(mopts, moptVirtualInodeNumbers)
629+
fsopts.virtualInodeNumbers = true
630+
}
608631
// fsopts.regularFilesUseSpecialFileFD can only be enabled by specifying
609632
// "cache=none".
610633

@@ -638,13 +661,14 @@ func (fstype FilesystemType) GetFilesystem(ctx context.Context, vfsObj *vfs.Virt
638661
return nil, nil, err
639662
}
640663
fs := &filesystem{
641-
mf: mf,
642-
opts: fsopts,
643-
iopts: iopts,
644-
clock: ktime.RealtimeClockFromContext(ctx),
645-
devMinor: devMinor,
646-
inoByKey: make(map[inoKey]uint64),
647-
inodeByKey: make(map[inoKey]*inode),
664+
mf: mf,
665+
opts: fsopts,
666+
iopts: iopts,
667+
clock: ktime.RealtimeClockFromContext(ctx),
668+
devMinor: devMinor,
669+
inoByKey: make(map[inoKey]uint64),
670+
inodeByKey: make(map[inoKey]*inode),
671+
devMinorByHostDev: make(map[uint64]uint32),
648672
}
649673

650674
// Did the user configure a global dentry cache?
@@ -828,7 +852,11 @@ func (fs *filesystem) Release(ctx context.Context) {
828852
}
829853
}
830854

831-
fs.vfsfs.VirtualFilesystem().PutAnonBlockDevMinor(fs.devMinor)
855+
vfsObj := fs.vfsfs.VirtualFilesystem()
856+
vfsObj.PutAnonBlockDevMinor(fs.devMinor)
857+
for _, devMinor := range fs.devMinorByHostDev {
858+
vfsObj.PutAnonBlockDevMinor(devMinor)
859+
}
832860
}
833861

834862
// releaseExtraRefsRecursiveLocked traverses the tree with root d and
@@ -914,7 +942,8 @@ type inode struct {
914942
// - Lock metadataMu and use atomic operations to update because we might
915943
// have atomic readers that don't hold the lock.
916944
metadataMu sync.Mutex `state:"nosave"`
917-
ino uint64 // immutable
945+
ino uint64 // immutable, except during restore
946+
devMinor uint32 // immutable, except during restore
918947
mode atomicbitops.Uint32 // type is immutable, perms are mutable
919948
uid atomicbitops.Uint32 // auth.KUID, but stored as raw uint32 for sync/atomic
920949
gid atomicbitops.Uint32 // auth.KGID, but ...
@@ -1251,7 +1280,12 @@ type dentryListElem struct {
12511280
dentryEntry
12521281
}
12531282

1283+
// inoFromKey returns the inode number of the file identified by key. With
1284+
// virtual inode numbers, key must include the host device numbers.
12541285
func (fs *filesystem) inoFromKey(key inoKey) uint64 {
1286+
if !fs.opts.virtualInodeNumbers {
1287+
return key.ino
1288+
}
12551289
fs.inoMu.Lock()
12561290
defer fs.inoMu.Unlock()
12571291

@@ -1263,6 +1297,111 @@ func (fs *filesystem) inoFromKey(key inoKey) uint64 {
12631297
return ino
12641298
}
12651299

1300+
// devMinorFromKey returns the minor device number of the file identified by
1301+
// key.
1302+
func (fs *filesystem) devMinorFromKey(key inoKey) (uint32, error) {
1303+
if fs.opts.virtualInodeNumbers {
1304+
return fs.devMinor, nil
1305+
}
1306+
dev := unix.Mkdev(key.devMajor, key.devMinor)
1307+
fs.devMu.RLock()
1308+
devMinor, ok := fs.devMinorByHostDev[dev]
1309+
fs.devMu.RUnlock()
1310+
if ok {
1311+
return devMinor, nil
1312+
}
1313+
fs.devMu.Lock()
1314+
defer fs.devMu.Unlock()
1315+
if devMinor, ok := fs.devMinorByHostDev[dev]; ok {
1316+
return devMinor, nil
1317+
}
1318+
return fs.newDevMinorLocked(dev)
1319+
}
1320+
1321+
// Preconditions: fs.devMu is locked.
1322+
func (fs *filesystem) newDevMinorLocked(dev uint64) (uint32, error) {
1323+
devMinor, err := fs.vfsfs.VirtualFilesystem().GetAnonBlockDevMinor()
1324+
if err != nil {
1325+
return 0, err
1326+
}
1327+
fs.devMinorByHostDev[dev] = devMinor
1328+
return devMinor, nil
1329+
}
1330+
1331+
func hasDevMinor(devMinorByHostDev map[uint64]uint32, devMinor uint32) bool {
1332+
for _, m := range devMinorByHostDev {
1333+
if m == devMinor {
1334+
return true
1335+
}
1336+
}
1337+
return false
1338+
}
1339+
1340+
// restoreDevMinor returns the minor device number for the host device of key,
1341+
// which identifies a restored inode that had minor device number saved at
1342+
// checkpoint. saved is reused unless another host device has already been
1343+
// mapped to it, so that device numbers are stable across checkpoint/restore
1344+
// when the host filesystem is unchanged.
1345+
func (fs *filesystem) restoreDevMinor(key inoKey, saved uint32) (uint32, error) {
1346+
dev := unix.Mkdev(key.devMajor, key.devMinor)
1347+
fs.devMu.Lock()
1348+
defer fs.devMu.Unlock()
1349+
if devMinor, ok := fs.devMinorByHostDev[dev]; ok {
1350+
return devMinor, nil
1351+
}
1352+
if hasDevMinor(fs.devMinorByHostDev, saved) {
1353+
return fs.newDevMinorLocked(dev)
1354+
}
1355+
fs.devMinorByHostDev[dev] = saved
1356+
return saved, nil
1357+
}
1358+
1359+
// releaseDevMinors releases the minor device numbers in prevDevMinors, the
1360+
// devMinorByHostDev saved at checkpoint, that restore did not reuse.
1361+
func (fs *filesystem) releaseDevMinors(prevDevMinors map[uint64]uint32) {
1362+
vfsObj := fs.vfsfs.VirtualFilesystem()
1363+
fs.devMu.Lock()
1364+
defer fs.devMu.Unlock()
1365+
for _, devMinor := range prevDevMinors {
1366+
if !hasDevMinor(fs.devMinorByHostDev, devMinor) {
1367+
vfsObj.PutAnonBlockDevMinor(devMinor)
1368+
}
1369+
}
1370+
}
1371+
1372+
// restoreInoKey records the host identity of a restored inode, and updates
1373+
// the inode and device numbers derived from it. Host inode and device
1374+
// numbers are not preserved across checkpoint/restore, so:
1375+
//
1376+
// - We must assume that the host filesystem did not change in a way that
1377+
// would invalidate dentries, since we can't revalidate dentries by
1378+
// checking inoKey.
1379+
//
1380+
// - With virtual inode numbers, the new inoKey is associated with the
1381+
// existing i.ino. Otherwise, i.ino follows the host inode number.
1382+
func (i *inode) restoreInoKey(key inoKey) error {
1383+
i.inoKey = key
1384+
if i.fs.opts.virtualInodeNumbers {
1385+
i.fs.inoMu.Lock()
1386+
i.fs.inoByKey[key] = i.ino
1387+
i.fs.inoMu.Unlock()
1388+
} else {
1389+
i.ino = key.ino
1390+
devMinor, err := i.fs.restoreDevMinor(key, i.devMinor)
1391+
if err != nil {
1392+
return err
1393+
}
1394+
i.devMinor = devMinor
1395+
}
1396+
// inodeByKey contains non-directory inodes only.
1397+
if i.fileType() != linux.S_IFDIR {
1398+
i.fs.inodeMu.Lock()
1399+
i.fs.inodeByKey[key] = i
1400+
i.fs.inodeMu.Unlock()
1401+
}
1402+
return nil
1403+
}
1404+
12661405
func (fs *filesystem) nextIno() uint64 {
12671406
return fs.lastIno.Add(1)
12681407
}
@@ -1346,7 +1485,7 @@ func (d *dentry) statTo(stat *linux.Statx) {
13461485
stat.Ctime = linux.NsecToStatxTimestamp(d.inode.ctime.Load())
13471486
stat.Mtime = linux.NsecToStatxTimestamp(d.inode.mtime.Load())
13481487
stat.DevMajor = linux.UNNAMED_MAJOR
1349-
stat.DevMinor = d.inode.fs.devMinor
1488+
stat.DevMinor = d.inode.devMinor
13501489
stat.RdevMajor = d.inode.rdevMajor
13511490
stat.RdevMinor = d.inode.rdevMinor
13521491
}

‎pkg/sentry/fsimpl/gofer/gofer_test.go‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,8 @@ func TestDestroyIdempotent(t *testing.T) {
3232
inoByKey: make(map[inoKey]uint64),
3333
inodeByKey: make(map[inoKey]*inode),
3434
clock: ktime.RealtimeClockFromContext(ctx),
35+
// There is no VFS to allocate device numbers from.
36+
devMinorByHostDev: map[uint64]uint32{0: 1},
3537
// Test relies on no dentry being held in the cache.
3638
dentryCache: &dentryCache{maxCachedDentries: 0},
3739
client: &lisafs.Client{},

0 commit comments

Comments
 (0)