194 lines
7.0 KiB
Go
194 lines
7.0 KiB
Go
// Copyright 2018 The gVisor Authors.
|
|
//
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
|
|
package fdpipe
|
|
|
|
import (
|
|
"io"
|
|
"os"
|
|
"syscall"
|
|
"time"
|
|
|
|
"gvisor.dev/gvisor/pkg/fd"
|
|
"gvisor.dev/gvisor/pkg/sentry/context"
|
|
"gvisor.dev/gvisor/pkg/sentry/fs"
|
|
"gvisor.dev/gvisor/pkg/syserror"
|
|
)
|
|
|
|
// NonBlockingOpener is a generic host file opener used to retry opening host
|
|
// pipes if necessary.
|
|
type NonBlockingOpener interface {
|
|
// NonBlockingOpen tries to open a host pipe in a non-blocking way,
|
|
// and otherwise returns an error. Implementations should be idempotent.
|
|
NonBlockingOpen(context.Context, fs.PermMask) (*fd.FD, error)
|
|
}
|
|
|
|
// Open blocks until a host pipe can be opened or the action was cancelled.
|
|
// On success, returns fs.FileOperations wrapping the opened host pipe.
|
|
func Open(ctx context.Context, opener NonBlockingOpener, flags fs.FileFlags) (fs.FileOperations, error) {
|
|
p := &pipeOpenState{}
|
|
canceled := false
|
|
for {
|
|
if file, err := p.TryOpen(ctx, opener, flags); err != syserror.ErrWouldBlock {
|
|
return file, err
|
|
}
|
|
|
|
// Honor the cancellation request if open still blocks.
|
|
if canceled {
|
|
// If we were canceled but we have a handle to a host
|
|
// file, we need to close it.
|
|
if p.hostFile != nil {
|
|
p.hostFile.Close()
|
|
}
|
|
return nil, syserror.ErrInterrupted
|
|
}
|
|
|
|
cancel := ctx.SleepStart()
|
|
select {
|
|
case <-cancel:
|
|
// The cancellation request received here really says
|
|
// "cancel from now on (or ASAP)". Any environmental
|
|
// changes happened before receiving it, that might have
|
|
// caused open to not block anymore, should still be
|
|
// respected. So we cannot just return here. We have to
|
|
// give open another try below first.
|
|
canceled = true
|
|
ctx.SleepFinish(false)
|
|
case <-time.After(100 * time.Millisecond):
|
|
// If we would block, then delay retrying for a bit, since there
|
|
// is no way to know when the pipe would be ready to be
|
|
// re-opened. This is identical to sending an event notification
|
|
// to stop blocking in Task.Block, given that this routine will
|
|
// stop retrying if a cancelation is received.
|
|
ctx.SleepFinish(true)
|
|
}
|
|
}
|
|
}
|
|
|
|
// pipeOpenState holds state needed to open a blocking named pipe read only, for instance the
|
|
// file that has been opened but doesn't yet have a corresponding writer.
|
|
type pipeOpenState struct {
|
|
// hostFile is the read only named pipe which lacks a corresponding writer.
|
|
hostFile *fd.FD
|
|
}
|
|
|
|
// unwrapError is needed to match against ENXIO primarily.
|
|
func unwrapError(err error) error {
|
|
if pe, ok := err.(*os.PathError); ok {
|
|
return pe.Err
|
|
}
|
|
return err
|
|
}
|
|
|
|
// TryOpen uses a NonBlockingOpener to try to open a host pipe, respecting the fs.FileFlags.
|
|
func (p *pipeOpenState) TryOpen(ctx context.Context, opener NonBlockingOpener, flags fs.FileFlags) (*pipeOperations, error) {
|
|
switch {
|
|
// Reject invalid configurations so they don't accidentally succeed below.
|
|
case !flags.Read && !flags.Write:
|
|
return nil, syscall.EINVAL
|
|
|
|
// Handle opening RDWR or with O_NONBLOCK: will never block, so try only once.
|
|
case (flags.Read && flags.Write) || flags.NonBlocking:
|
|
f, err := opener.NonBlockingOpen(ctx, fs.PermMask{Read: flags.Read, Write: flags.Write})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return newPipeOperations(ctx, opener, flags, f, nil)
|
|
|
|
// Handle opening O_WRONLY blocking: convert ENXIO to syserror.ErrWouldBlock.
|
|
// See TryOpenWriteOnly for more details.
|
|
case flags.Write:
|
|
return p.TryOpenWriteOnly(ctx, opener)
|
|
|
|
default:
|
|
// Handle opening O_RDONLY blocking: convert EOF from read to syserror.ErrWouldBlock.
|
|
// See TryOpenReadOnly for more details.
|
|
return p.TryOpenReadOnly(ctx, opener)
|
|
}
|
|
}
|
|
|
|
// TryOpenReadOnly tries to open a host pipe read only but only returns a fs.File when
|
|
// there is a coordinating writer. Call TryOpenReadOnly repeatedly on the same pipeOpenState
|
|
// until syserror.ErrWouldBlock is no longer returned.
|
|
//
|
|
// How it works:
|
|
//
|
|
// Opening a pipe read only will return no error, but each non zero Read will return EOF
|
|
// until a writer becomes available, then EWOULDBLOCK. This is the only state change
|
|
// available to us. We keep a read ahead buffer in case we read bytes instead of getting
|
|
// EWOULDBLOCK, to be read from on the first read request to this fs.File.
|
|
func (p *pipeOpenState) TryOpenReadOnly(ctx context.Context, opener NonBlockingOpener) (*pipeOperations, error) {
|
|
// Waiting for a blocking read only open involves reading from the host pipe until
|
|
// bytes or other writers are available, so instead of retrying opening the pipe,
|
|
// it's necessary to retry reading from the pipe. To do this we need to keep around
|
|
// the read only pipe we opened, until success or an irrecoverable read error (at
|
|
// which point it must be closed).
|
|
if p.hostFile == nil {
|
|
var err error
|
|
p.hostFile, err = opener.NonBlockingOpen(ctx, fs.PermMask{Read: true})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
// Try to read from the pipe to see if writers are around.
|
|
tryReadBuffer := make([]byte, 1)
|
|
n, rerr := p.hostFile.Read(tryReadBuffer)
|
|
|
|
// No bytes were read.
|
|
if n == 0 {
|
|
// EOF means that we're not ready yet.
|
|
if rerr == nil || rerr == io.EOF {
|
|
return nil, syserror.ErrWouldBlock
|
|
}
|
|
// Any error that is not EWOULDBLOCK also means we're not
|
|
// ready yet, and probably never will be ready. In this
|
|
// case we need to close the host pipe we opened.
|
|
if unwrapError(rerr) != syscall.EWOULDBLOCK {
|
|
p.hostFile.Close()
|
|
return nil, rerr
|
|
}
|
|
}
|
|
|
|
// If any bytes were read, no matter the corresponding error, we need
|
|
// to keep them around so they can be read by the application.
|
|
var readAheadBuffer []byte
|
|
if n > 0 {
|
|
readAheadBuffer = tryReadBuffer
|
|
}
|
|
|
|
// Successfully opened read only blocking pipe with either bytes available
|
|
// to read and/or a writer available.
|
|
return newPipeOperations(ctx, opener, fs.FileFlags{Read: true}, p.hostFile, readAheadBuffer)
|
|
}
|
|
|
|
// TryOpenWriteOnly tries to open a host pipe write only but only returns a fs.File when
|
|
// there is a coordinating reader. Call TryOpenWriteOnly repeatedly on the same pipeOpenState
|
|
// until syserror.ErrWouldBlock is no longer returned.
|
|
//
|
|
// How it works:
|
|
//
|
|
// Opening a pipe write only will return ENXIO until readers are available. Converts the ENXIO
|
|
// to an syserror.ErrWouldBlock, to tell callers to retry.
|
|
func (*pipeOpenState) TryOpenWriteOnly(ctx context.Context, opener NonBlockingOpener) (*pipeOperations, error) {
|
|
hostFile, err := opener.NonBlockingOpen(ctx, fs.PermMask{Write: true})
|
|
if unwrapError(err) == syscall.ENXIO {
|
|
return nil, syserror.ErrWouldBlock
|
|
}
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return newPipeOperations(ctx, opener, fs.FileFlags{Write: true}, hostFile, nil)
|
|
}
|