mm: add fallback to buffered I/O when memmap.File.MapInternal() is unavailable

MapInternal() returns a coherent memory mapping of the host file descriptor
represented by a memmap.File, in the sentry's address space. This is
principally used when the sentry needs to access the contents of application
memory (for e.g. syscall arguments passed by pointer, or the source/destination
of a write()/read() syscall); it usually looks up the memmap.Files backing
application addresses and obtains mappings via MapInternal().

/dev/nvidia-uvm cannot generally be mapped into the sentry's address space, for
reasons described by
https://github.com/google/gvisor/blob/master/g3doc/proposals/nvidia_driver_proxy.md#unified-virtual-memory-uvm
(in short, nvidia-uvm requires that a given page at file offset X can only be
mapped at address X). To allow the sentry to access the contents of such
mappings, make it possible for memmap.File.MapInternal() to indicate that a
fallback to buffered I/O is required, add interface methods
memmap.File.Buffer{Read,Write}At() to perform this buffered I/O, and implement
this fallback in the MM I/O path.

This CL does not use the new buffered I/O fallback anywhere; a following CL
adds it to nvproxy's nvidia-uvm.

Updates #10331

PiperOrigin-RevId: 629830825
This commit is contained in:
Jamie Liu
2024-05-01 14:05:55 -07:00
committed by gVisor bot
parent 1643e55713
commit 31979a7187
13 changed files with 361 additions and 77 deletions
+2
View File
@@ -61,6 +61,8 @@ func (fd *tpuV4FD) InvalidateUnsavable(ctx context.Context) error {
}
type accelFDMemmapFile struct {
memmap.NoBufferedIOFallback
fd *tpuV4FD
}
@@ -61,6 +61,8 @@ func (fd *frontendFD) InvalidateUnsavable(ctx context.Context) error {
}
type frontendFDMemmapFile struct {
memmap.NoBufferedIOFallback
fd *frontendFD
}
+2
View File
@@ -69,6 +69,8 @@ func (fd *uvmFD) InvalidateUnsavable(ctx context.Context) error {
}
type uvmFDMemmapFile struct {
memmap.NoBufferedIOFallback
fd *uvmFD
}
+4
View File
@@ -61,6 +61,8 @@ func (fd *tpuFD) InvalidateUnsavable(ctx context.Context) error {
}
type tpuFDMemmapFile struct {
memmap.NoBufferedIOFallback
fd *tpuFD
}
@@ -120,6 +122,8 @@ func (fd *pciDeviceFD) InvalidateUnsavable(ctx context.Context) error {
}
type pciDeviceFdMemmapFile struct {
memmap.NoBufferedIOFallback
fd *pciDeviceFD
}
+2
View File
@@ -61,6 +61,8 @@ func (fd *vfioFD) InvalidateUnsavable(ctx context.Context) error {
}
type vfioFDMemmapFile struct {
memmap.NoBufferedIOFallback
fd *vfioFD
}
+2
View File
@@ -190,6 +190,8 @@ func (i *inode) InvalidateUnsavable(ctx context.Context) error {
// +stateify savable
type imageMemmapFile struct {
memmap.NoBufferedIOFallback
image *erofs.Image
}
+2
View File
@@ -916,6 +916,8 @@ func (d *dentry) Evict(ctx context.Context, er pgalloc.EvictableRange) {
//
// +stateify savable
type dentryPlatformFile struct {
memmap.NoBufferedIOFallback
*dentry
// fdRefs counts references on memmap.File offsets. fdRefs is protected
+1
View File
@@ -43,6 +43,7 @@ import (
type specialFileFD struct {
fileDescription
specialFDEntry
memmap.NoBufferedIOFallback
// releaseMu synchronizes the closing of fd.handle with fd.sync(). It's safe
// to access fd.handle without locking for operations that require a ref to
+2
View File
@@ -28,6 +28,8 @@ import (
//
// +stateify savable
type inodePlatformFile struct {
memmap.NoBufferedIOFallback
// hostFD contains the host fd that this file was originally created from,
// which must be available at time of restore.
//
+51
View File
@@ -436,6 +436,32 @@ type File interface {
// reference is held on the mapped pages.
MapInternal(fr FileRange, at hostarch.AccessType) (safemem.BlockSeq, error)
// BufferReadAt reads len(dst) bytes from the file into dst, starting at
// file offset off. It returns the number of bytes read. Like
// io.ReaderAt.ReadAt(), it never returns a short read with a nil error.
//
// Implementations of File for which MapInternal() never returns
// BufferedIOFallbackErr can embed NoBufferedIOFallback to obtain an
// appropriate implementation of BufferReadAt.
//
// Preconditions:
// * MapInternal() returned a BufferedIOFallbackErr.
// * At least one reference must be held on all read pages.
BufferReadAt(off uint64, dst []byte) (uint64, error)
// BufferWriteAt writes len(src) bytes src to the file, starting at file
// offset off. It returns the number of bytes written. Like
// io.WriterAt.WriteAt(), it never returns a short write with a nil error.
//
// Implementations of File for which MapInternal() never returns
// BufferedIOFallbackErr can embed NoBufferedIOFallback to obtain an
// appropriate implementation of BufferWriteAt.
//
// Preconditions:
// * MapInternal() returned a BufferedIOFallbackErr.
// * At least one reference must be held on all written pages.
BufferWriteAt(off uint64, src []byte) (uint64, error)
// FD returns the file descriptor represented by the File.
//
// The only permitted operation on the returned file descriptor is to map
@@ -443,6 +469,31 @@ type File interface {
FD() int
}
// BufferedIOFallbackErr is returned (by value) by implementations of
// File.MapInternal() that cannot succeed, but can still support memory-mapped
// I/O by falling back to buffered reads and writes.
type BufferedIOFallbackErr struct{}
// Error implements error.Error.
func (BufferedIOFallbackErr) Error() string {
return "memmap.File.MapInternal() is unsupported, fall back to buffered R/W for internally-mapped I/O"
}
// NoBufferedIOFallback implements File.BufferReadAt() and BufferWriteAt() for
// implementations of File for which MapInternal() never returns
// BufferedIOFallbackErr.
type NoBufferedIOFallback struct{}
// BufferReadAt implements File.BufferReadAt.
func (NoBufferedIOFallback) BufferReadAt(off uint64, dst []byte) (uint64, error) {
panic("unimplemented: memmap.File.MapInternal() should not have returned BufferedIOFallbackErr")
}
// BufferWriteAt implements File.BufferWriteAt.
func (NoBufferedIOFallback) BufferWriteAt(off uint64, src []byte) (uint64, error) {
panic("unimplemented: memmap.File.MapInternal() should not have returned BufferedIOFallbackErr")
}
// FileRange represents a range of uint64 offsets into a File.
//
// type FileRange <generated using go_generics>
+280 -8
View File
@@ -15,12 +15,15 @@
package mm
import (
"fmt"
"gvisor.dev/gvisor/pkg/context"
"gvisor.dev/gvisor/pkg/errors/linuxerr"
"gvisor.dev/gvisor/pkg/hostarch"
"gvisor.dev/gvisor/pkg/safemem"
"gvisor.dev/gvisor/pkg/sentry/memmap"
"gvisor.dev/gvisor/pkg/sentry/platform"
"gvisor.dev/gvisor/pkg/sync"
"gvisor.dev/gvisor/pkg/usermem"
)
@@ -118,6 +121,11 @@ func (mm *MemoryManager) CopyOut(ctx context.Context, addr hostarch.Addr, src []
}
// Go through internal mappings.
// NOTE(gvisor.dev/issue/10331): Using mm.withInternalMappings() here means
// that if we encounter any memmap.BufferedIOFallbackErrs, this copy will
// traverse an unnecessary layer of buffering. This can be fixed by
// inlining mm.withInternalMappings() and passing src subslices directly to
// memmap.File.BufferWriteAt().
n64, err := mm.withInternalMappings(ctx, ar, hostarch.Write, opts.IgnorePermissions, func(ims safemem.BlockSeq) (uint64, error) {
n, err := safemem.CopySeq(ims, safemem.BlockSeqOf(safemem.BlockFromSafeSlice(src)))
return n, translateIOError(ctx, err)
@@ -161,6 +169,11 @@ func (mm *MemoryManager) CopyIn(ctx context.Context, addr hostarch.Addr, dst []b
}
// Go through internal mappings.
// NOTE(gvisor.dev/issue/10331): Using mm.withInternalMappings() here means
// that if we encounter any memmap.BufferedIOFallbackErrs, this copy will
// traverse an unnecessary layer of buffering. This can be fixed by
// inlining mm.withInternalMappings() and passing dst subslices directly to
// memmap.File.BufferReadAt().
n64, err := mm.withInternalMappings(ctx, ar, hostarch.Read, opts.IgnorePermissions, func(ims safemem.BlockSeq) (uint64, error) {
n, err := safemem.CopySeq(safemem.BlockSeqOf(safemem.BlockFromSafeSlice(dst)), ims)
return n, translateIOError(ctx, err)
@@ -549,18 +562,19 @@ func (mm *MemoryManager) withInternalMappings(ctx context.Context, ar hostarch.A
}
ar.End = pendaddr
}
imend, imerr := mm.getPMAInternalMappingsLocked(pseg, ar)
imbs, t, imerr := mm.getIOMappingsLocked(pseg, ar, at)
mm.activeMu.DowngradeLock()
if imendaddr := imend.Start(); imendaddr < ar.End {
if imendaddr <= ar.Start {
if imlen := imbs.NumBytes(); imlen < uint64(ar.Length()) {
if imlen == 0 {
t.flush(0, nil)
mm.activeMu.RUnlock()
return 0, translateIOError(ctx, imerr)
}
ar.End = imendaddr
ar.End = ar.Start + hostarch.Addr(imlen)
}
// Do I/O.
un, err := f(mm.internalMappingsLocked(pseg, ar))
un, err := t.flush(f(imbs))
mm.activeMu.RUnlock()
n := int64(un)
@@ -619,15 +633,16 @@ func (mm *MemoryManager) withVecInternalMappings(ctx context.Context, ars hostar
mm.activeMu.Unlock()
return 0, translateIOError(ctx, perr)
}
imars, imerr := mm.getVecPMAInternalMappingsLocked(pars)
imbs, t, imerr := mm.getVecIOMappingsLocked(pars, at)
mm.activeMu.DowngradeLock()
if imars.NumBytes() == 0 {
if imbs.NumBytes() == 0 {
t.flush(0, nil)
mm.activeMu.RUnlock()
return 0, translateIOError(ctx, imerr)
}
// Do I/O.
un, err := f(mm.vecInternalMappingsLocked(imars))
un, err := t.flush(f(imbs))
mm.activeMu.RUnlock()
n := int64(un)
@@ -645,6 +660,263 @@ func (mm *MemoryManager) withVecInternalMappings(ctx context.Context, ars hostar
return n, translateIOError(ctx, verr)
}
// getIOMappingsLocked returns internal mappings appropriate for I/O for
// addresses in ar. If mappings are only available for a strict subset of ar,
// the returned error is non-nil.
//
// ioBufTracker.flush() must be called on the returned ioBufTracker when the
// returned mappings are no longer in use, and its return value indicates the
// number of bytes actually completed after buffer flushing. Returned mappings
// are valid until either mm.activeMu is unlocked or ioBufTracker.flush() is
// called.
//
// Preconditions:
// - mm.activeMu must be locked for writing.
// - pseg.Range().Contains(ar.Start).
// - pmas must exist for all addresses in ar.
// - ar.Length() != 0.
//
// Postconditions: getIOMappingsLocked does not invalidate iterators into mm.pmas.
func (mm *MemoryManager) getIOMappingsLocked(pseg pmaIterator, ar hostarch.AddrRange, at hostarch.AccessType) (safemem.BlockSeq, *ioBufTracker, error) {
if checkInvariants {
if !ar.WellFormed() || ar.Length() == 0 {
panic(fmt.Sprintf("invalid ar: %v", ar))
}
if !pseg.Range().Contains(ar.Start) {
panic(fmt.Sprintf("initial pma %v does not cover start of ar %v", pseg.Range(), ar))
}
}
if ar.End <= pseg.End() {
// Since only one pma is involved, we can use pma.internalMappings
// directly, avoiding a slice allocation.
if err := pseg.getInternalMappingsLocked(); err != nil {
if _, ok := err.(memmap.BufferedIOFallbackErr); ok {
goto slowPath
}
return safemem.BlockSeq{}, nil, err
}
offset := uint64(ar.Start - pseg.Start())
return pseg.ValuePtr().internalMappings.DropFirst64(offset).TakeFirst64(uint64(ar.Length())), nil, nil
}
slowPath:
ims, t, _, err := mm.getIOMappingsTrackedLocked(pseg, ar, at, nil, nil, 0)
return safemem.BlockSeqFromSlice(ims), t, err
}
// getVecIOMappingsLocked returns internal mappings appropriate for I/O for
// addresses in ars. If mappings are only available for a strict subset of ar,
// the returned error is non-nil.
//
// ioBufTracker.flush() must be called on the returned ioBufTracker when the
// returned mappings are no longer in use, and its return value indicates the
// number of bytes actually completed after buffer flushing. Returned mappings
// are valid until either mm.activeMu is unlocked or ioBufTracker.flush() is
// called.
//
// Preconditions:
// - mm.activeMu must be locked for writing.
// - pmas must exist for all addresses in ar.
//
// Postconditions: getVecIOMappingsLocked does not invalidate iterators into
// mm.pmas
func (mm *MemoryManager) getVecIOMappingsLocked(ars hostarch.AddrRangeSeq, at hostarch.AccessType) (safemem.BlockSeq, *ioBufTracker, error) {
if ars.NumRanges() == 1 {
ar := ars.Head()
return mm.getIOMappingsLocked(mm.pmas.FindSegment(ar.Start), ar, at)
}
var ims []safemem.Block
var t *ioBufTracker
unbufBytes := uint64(0)
for arsit := ars; !arsit.IsEmpty(); arsit = arsit.Tail() {
ar := arsit.Head()
if ar.Length() == 0 {
continue
}
var err error
ims, t, unbufBytes, err = mm.getIOMappingsTrackedLocked(mm.pmas.FindSegment(ar.Start), ar, at, ims, t, unbufBytes)
if err != nil {
return safemem.BlockSeqFromSlice(ims), t, err
}
}
return safemem.BlockSeqFromSlice(ims), t, nil
}
// getIOMappingsTrackedLocked collects internal mappings appropriate for I/O
// for addresses in ar, appends them to ims, and returns an updated slice. If
// mappings are only available for a strict subset of ar, the returned error is
// non-nil.
//
// If any iterated memmap.Files require buffering for I/O, they are recorded in
// an ioBufTracker. Since the ioBufTracker pointer is initially nil (to
// minimize overhead for the common case where no memmap.files require
// buffering for I/O), getIOMappingsTrackedLocked returns an updated
// ioBufTracker pointer.
//
// unbufBytes is the number of bytes of unbuffered mappings that have been
// appended to ims since the last buffered mapping; getIOMappingsTrackedLocked
// also returns an updated value for unbufBytes.
//
// Returned mappings are valid until either mm.activeMu is unlocked or
// ioBufTracker.flush() is called.
//
// Preconditions:
// - mm.activeMu must be locked for writing.
// - pseg.Range().Contains(ar.Start).
// - pmas must exist for all addresses in ar.
// - ar.Length() != 0.
//
// Postconditions: getIOMappingsTrackedLocked does not invalidate iterators
// into mm.pmas.
func (mm *MemoryManager) getIOMappingsTrackedLocked(pseg pmaIterator, ar hostarch.AddrRange, at hostarch.AccessType, ims []safemem.Block, t *ioBufTracker, unbufBytes uint64) ([]safemem.Block, *ioBufTracker, uint64, error) {
for {
pmaAR := ar.Intersect(pseg.Range())
if err := pseg.getInternalMappingsLocked(); err == nil {
// Iterate the subset of the PMA's cached internal mappings that
// correspond to pmaAR, and append them to ims.
for pims := pseg.ValuePtr().internalMappings.DropFirst64(uint64(pmaAR.Start - pseg.Start())).TakeFirst64(uint64(pmaAR.Length())); !pims.IsEmpty(); pims = pims.Tail() {
ims = append(ims, pims.Head())
}
unbufBytes += uint64(pmaAR.Length())
} else if _, ok := err.(memmap.BufferedIOFallbackErr); !ok {
return ims, t, unbufBytes, err
} else {
// Fall back to buffered I/O as instructed.
if t == nil {
t = getIOBufTracker(at.Write)
}
buf := getByteSlicePtr(int(pmaAR.Length()))
pma := pseg.ValuePtr()
off := pseg.fileRangeOf(pmaAR).Start
// If the caller will read from the buffer, fill it from the file;
// otherwise leave it zeroed.
if at.Read || at.Execute {
var n uint64
n, err = pma.file.BufferReadAt(off, *buf)
*buf = (*buf)[:n]
} else {
err = nil
}
if len(*buf) != 0 {
ims = append(ims, safemem.BlockFromSafeSlice(*buf))
t.bufs = append(t.bufs, ioBuf{
unbufBytesBefore: unbufBytes,
file: pma.file,
off: off,
buf: buf,
})
unbufBytes = 0
}
if err != nil {
return ims, t, unbufBytes, err
}
}
if ar.End <= pseg.End() {
return ims, t, unbufBytes, nil
}
pseg, _ = pseg.NextNonEmpty()
}
}
type ioBuf struct {
unbufBytesBefore uint64
file memmap.File
off uint64
buf *[]byte
}
type ioBufTracker struct {
write bool
bufs []ioBuf
}
var ioBufTrackerPool = sync.Pool{
New: func() any {
return &ioBufTracker{}
},
}
func getIOBufTracker(write bool) *ioBufTracker {
t := ioBufTrackerPool.Get().(*ioBufTracker)
t.write = write
return t
}
func putIOBufTracker(t *ioBufTracker) {
for i := range t.bufs {
t.bufs[i].file = nil
putByteSlicePtr(t.bufs[i].buf)
t.bufs[i].buf = nil
}
t.bufs = t.bufs[:0]
ioBufTrackerPool.Put(t)
}
func (t *ioBufTracker) flush(prevN uint64, prevErr error) (uint64, error) {
if t == nil {
return prevN, prevErr
}
return t.flushSlow(prevN, prevErr)
}
func (t *ioBufTracker) flushSlow(prevN uint64, prevErr error) (uint64, error) {
defer putIOBufTracker(t)
if !t.write {
return prevN, prevErr
}
// Flush dirty buffers to underlying memmap.Files.
rem := prevN
done := uint64(0)
for i := range t.bufs {
buf := &t.bufs[i]
if rem <= buf.unbufBytesBefore {
// The write ended before reaching buf.buf.
break
}
rem -= buf.unbufBytesBefore
done += buf.unbufBytesBefore
n, err := buf.file.BufferWriteAt(buf.off, (*buf.buf)[:min(len(*buf.buf), int(rem))])
rem -= n
done += n
if err != nil {
return done, err
}
}
// All buffers covered by prevN were written back successfully.
return prevN, prevErr
}
var byteSlicePtrPool sync.Pool
// getByteSlicePtr returns a pointer to a byte slice with the given length. The
// slice is either newly-allocated or recycled from a previous call to
// putByteSlicePtr. The pointer should be passed to putByteSlicePtr when the
// slice is no longer in use.
func getByteSlicePtr(l int) *[]byte {
a := byteSlicePtrPool.Get()
if a == nil {
s := make([]byte, l)
return &s
}
sp := a.(*[]byte)
s := *sp
if l <= cap(s) {
s = s[:l]
} else {
s = make([]byte, l)
}
*sp = s
return sp
}
// putByteSlicePtr marks all of the given's slice capacity reusable by a future
// call to getByteSlicePtr.
func putByteSlicePtr(s *[]byte) {
byteSlicePtrPool.Put(s)
}
// truncatedAddrRangeSeq returns a copy of ars, but with the end truncated to
// at most address end on AddrRange arsit.Head(). It is used in vector I/O paths to
// truncate hostarch.AddrRangeSeq when errors occur.
+9 -69
View File
@@ -796,75 +796,13 @@ func (mm *MemoryManager) movePMAsLocked(oldAR, newAR hostarch.AddrRange) {
mm.unmapASLocked(oldAR)
}
// getPMAInternalMappingsLocked ensures that pmas for all addresses in ar have
// cached internal mappings. It returns:
//
// - An iterator to the gap after the last pma with internal mappings
// containing an address in ar. If internal mappings exist for no addresses in
// ar, the iterator is to a gap that begins before ar.Start.
//
// - An error that is non-nil if internal mappings exist for only a subset of
// ar.
//
// Preconditions:
// - mm.activeMu must be locked for writing.
// - pseg.Range().Contains(ar.Start).
// - pmas must exist for all addresses in ar.
// - ar.Length() != 0.
//
// Postconditions: getPMAInternalMappingsLocked does not invalidate iterators
// into mm.pmas.
func (mm *MemoryManager) getPMAInternalMappingsLocked(pseg pmaIterator, ar hostarch.AddrRange) (pmaGapIterator, error) {
if checkInvariants {
if !ar.WellFormed() || ar.Length() == 0 {
panic(fmt.Sprintf("invalid ar: %v", ar))
}
if !pseg.Range().Contains(ar.Start) {
panic(fmt.Sprintf("initial pma %v does not cover start of ar %v", pseg.Range(), ar))
}
}
for {
if err := pseg.getInternalMappingsLocked(); err != nil {
return pseg.PrevGap(), err
}
if ar.End <= pseg.End() {
return pseg.NextGap(), nil
}
pseg, _ = pseg.NextNonEmpty()
}
}
// getVecPMAInternalMappingsLocked ensures that pmas for all addresses in ars
// have cached internal mappings. It returns the subset of ars for which
// internal mappings exist. If this is not equal to ars, it returns a non-nil
// error explaining why.
//
// Preconditions:
// - mm.activeMu must be locked for writing.
// - pmas must exist for all addresses in ar.
//
// Postconditions: getVecPMAInternalMappingsLocked does not invalidate iterators
// into mm.pmas.
func (mm *MemoryManager) getVecPMAInternalMappingsLocked(ars hostarch.AddrRangeSeq) (hostarch.AddrRangeSeq, error) {
for arsit := ars; !arsit.IsEmpty(); arsit = arsit.Tail() {
ar := arsit.Head()
if ar.Length() == 0 {
continue
}
if pend, err := mm.getPMAInternalMappingsLocked(mm.pmas.FindSegment(ar.Start), ar); err != nil {
return truncatedAddrRangeSeq(ars, arsit, pend.Start()), err
}
}
return ars, nil
}
// internalMappingsLocked returns internal mappings for addresses in ar.
// internalMappingsLocked returns cached internal mappings for addresses in ar.
//
// Preconditions:
// - mm.activeMu must be locked.
// - Internal mappings must have been previously established for all addresses
// in ar.
// - While mm.activeMu was locked, a call to
// existingPMAsLocked(needInternalMappings=true) succeeded for all
// addresses in ar.
// - ar.Length() != 0.
// - pseg.Range().Contains(ar.Start).
func (mm *MemoryManager) internalMappingsLocked(pseg pmaIterator, ar hostarch.AddrRange) safemem.BlockSeq {
@@ -898,12 +836,14 @@ func (mm *MemoryManager) internalMappingsLocked(pseg pmaIterator, ar hostarch.Ad
return safemem.BlockSeqFromSlice(ims)
}
// vecInternalMappingsLocked returns internal mappings for addresses in ars.
// vecInternalMappingsLocked returns cached internal mappings for addresses in
// ars.
//
// Preconditions:
// - mm.activeMu must be locked.
// - Internal mappings must have been previously established for all addresses
// in ars.
// - While mm.activeMu was locked, a call to
// existingVecPMAsLocked(needInternalMappings=true) succeeded for all
// addresses in ars.
func (mm *MemoryManager) vecInternalMappingsLocked(ars hostarch.AddrRangeSeq) safemem.BlockSeq {
var ims []safemem.Block
for ; !ars.IsEmpty(); ars = ars.Tail() {
+2
View File
@@ -66,6 +66,8 @@ func (d Direction) String() string {
// MemoryFile is a memmap.File whose pages may be allocated to arbitrary
// users.
type MemoryFile struct {
memmap.NoBufferedIOFallback
// opts holds options passed to NewMemoryFile. opts is immutable.
opts MemoryFileOpts