gvisor/pkg/sentry/fs/fdpipe/pipe_opener_test.go

524 lines
15 KiB
Go
Raw Normal View History

// 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 (
"bytes"
"fmt"
"io"
"os"
"path"
"syscall"
"testing"
"time"
"github.com/google/uuid"
"gvisor.dev/gvisor/pkg/context"
"gvisor.dev/gvisor/pkg/fd"
"gvisor.dev/gvisor/pkg/sentry/contexttest"
"gvisor.dev/gvisor/pkg/sentry/fs"
"gvisor.dev/gvisor/pkg/syserror"
"gvisor.dev/gvisor/pkg/usermem"
)
type hostOpener struct {
name string
}
func (h *hostOpener) NonBlockingOpen(_ context.Context, p fs.PermMask) (*fd.FD, error) {
var flags int
switch {
case p.Read && p.Write:
flags = syscall.O_RDWR
case p.Write:
flags = syscall.O_WRONLY
case p.Read:
flags = syscall.O_RDONLY
default:
return nil, syscall.EINVAL
}
f, err := syscall.Open(h.name, flags|syscall.O_NONBLOCK, 0666)
if err != nil {
return nil, err
}
return fd.New(f), nil
}
func pipename() string {
return fmt.Sprintf(path.Join(os.TempDir(), "test-named-pipe-%s"), uuid.New())
}
func mkpipe(name string) error {
return syscall.Mknod(name, syscall.S_IFIFO|0666, 0)
}
func TestTryOpen(t *testing.T) {
for _, test := range []struct {
// desc is the test's description.
desc string
// makePipe is true if the test case should create the pipe.
makePipe bool
// flags are the fs.FileFlags used to open the pipe.
flags fs.FileFlags
// expectFile is true if a fs.File is expected.
expectFile bool
// err is the expected error
err error
}{
{
desc: "FileFlags lacking Read and Write are invalid",
makePipe: false,
flags: fs.FileFlags{}, /* bogus */
expectFile: false,
err: syscall.EINVAL,
},
{
desc: "NonBlocking Read only error returns immediately",
makePipe: false, /* causes the error */
flags: fs.FileFlags{Read: true, NonBlocking: true},
expectFile: false,
err: syscall.ENOENT,
},
{
desc: "NonBlocking Read only success returns immediately",
makePipe: true,
flags: fs.FileFlags{Read: true, NonBlocking: true},
expectFile: true,
err: nil,
},
{
desc: "NonBlocking Write only error returns immediately",
makePipe: false, /* causes the error */
flags: fs.FileFlags{Write: true, NonBlocking: true},
expectFile: false,
err: syscall.ENOENT,
},
{
desc: "NonBlocking Write only no reader error returns immediately",
makePipe: true,
flags: fs.FileFlags{Write: true, NonBlocking: true},
expectFile: false,
err: syscall.ENXIO,
},
{
desc: "ReadWrite error returns immediately",
makePipe: false, /* causes the error */
flags: fs.FileFlags{Read: true, Write: true},
expectFile: false,
err: syscall.ENOENT,
},
{
desc: "ReadWrite returns immediately",
makePipe: true,
flags: fs.FileFlags{Read: true, Write: true},
expectFile: true,
err: nil,
},
{
desc: "Blocking Write only returns open error",
makePipe: false, /* causes the error */
flags: fs.FileFlags{Write: true},
expectFile: false,
err: syscall.ENOENT, /* from bogus perms */
},
{
desc: "Blocking Read only returns open error",
makePipe: false, /* causes the error */
flags: fs.FileFlags{Read: true},
expectFile: false,
err: syscall.ENOENT,
},
{
desc: "Blocking Write only returns with syserror.ErrWouldBlock",
makePipe: true,
flags: fs.FileFlags{Write: true},
expectFile: false,
err: syserror.ErrWouldBlock,
},
{
desc: "Blocking Read only returns with syserror.ErrWouldBlock",
makePipe: true,
flags: fs.FileFlags{Read: true},
expectFile: false,
err: syserror.ErrWouldBlock,
},
} {
name := pipename()
if test.makePipe {
// Create the pipe. We do this per-test case to keep tests independent.
if err := mkpipe(name); err != nil {
t.Errorf("%s: failed to make host pipe: %v", test.desc, err)
continue
}
defer syscall.Unlink(name)
}
// Use a host opener to keep things simple.
opener := &hostOpener{name: name}
pipeOpenState := &pipeOpenState{}
ctx := contexttest.Context(t)
pipeOps, err := pipeOpenState.TryOpen(ctx, opener, test.flags)
if unwrapError(err) != test.err {
t.Errorf("%s: got error %v, want %v", test.desc, err, test.err)
if pipeOps != nil {
// Cleanup the state of the pipe, and remove the fd from the
// fdnotifier. Sadly this needed to maintain the correctness
// of other tests because the fdnotifier is global.
pipeOps.Release()
}
continue
}
if (pipeOps != nil) != test.expectFile {
t.Errorf("%s: got non-nil file %v, want %v", test.desc, pipeOps != nil, test.expectFile)
}
if pipeOps != nil {
// Same as above.
pipeOps.Release()
}
}
}
func TestPipeOpenUnblocksEventually(t *testing.T) {
for _, test := range []struct {
// desc is the test's description.
desc string
// partnerIsReader is true if the goroutine opening the same pipe as the test case
// should open the pipe read only. Otherwise write only. This also means that the
// test case will open the pipe in the opposite way.
partnerIsReader bool
// partnerIsBlocking is true if the goroutine opening the same pipe as the test case
// should do so without the O_NONBLOCK flag, otherwise opens the pipe with O_NONBLOCK
// until ENXIO is not returned.
partnerIsBlocking bool
}{
{
desc: "Blocking Read with blocking writer partner opens eventually",
partnerIsReader: false,
partnerIsBlocking: true,
},
{
desc: "Blocking Write with blocking reader partner opens eventually",
partnerIsReader: true,
partnerIsBlocking: true,
},
{
desc: "Blocking Read with non-blocking writer partner opens eventually",
partnerIsReader: false,
partnerIsBlocking: false,
},
{
desc: "Blocking Write with non-blocking reader partner opens eventually",
partnerIsReader: true,
partnerIsBlocking: false,
},
} {
// Create the pipe. We do this per-test case to keep tests independent.
name := pipename()
if err := mkpipe(name); err != nil {
t.Errorf("%s: failed to make host pipe: %v", test.desc, err)
continue
}
defer syscall.Unlink(name)
// Spawn the partner.
type fderr struct {
fd int
err error
}
errch := make(chan fderr, 1)
go func() {
var flags int
if test.partnerIsReader {
flags = syscall.O_RDONLY
} else {
flags = syscall.O_WRONLY
}
if test.partnerIsBlocking {
fd, err := syscall.Open(name, flags, 0666)
errch <- fderr{fd: fd, err: err}
} else {
var fd int
err := error(syscall.ENXIO)
for err == syscall.ENXIO {
fd, err = syscall.Open(name, flags|syscall.O_NONBLOCK, 0666)
time.Sleep(1 * time.Second)
}
errch <- fderr{fd: fd, err: err}
}
}()
// Setup file flags for either a read only or write only open.
flags := fs.FileFlags{
Read: !test.partnerIsReader,
Write: test.partnerIsReader,
}
// Open the pipe in a blocking way, which should succeed eventually.
opener := &hostOpener{name: name}
ctx := contexttest.Context(t)
pipeOps, err := Open(ctx, opener, flags)
if pipeOps != nil {
// Same as TestTryOpen.
pipeOps.Release()
}
// Check that the partner opened the file successfully.
e := <-errch
if e.err != nil {
t.Errorf("%s: partner got error %v, wanted nil", test.desc, e.err)
continue
}
// If so, then close the partner fd to avoid leaking an fd.
syscall.Close(e.fd)
// Check that our blocking open was successful.
if err != nil {
t.Errorf("%s: blocking open got error %v, wanted nil", test.desc, err)
continue
}
if pipeOps == nil {
t.Errorf("%s: blocking open got nil file, wanted non-nil", test.desc)
continue
}
}
}
func TestCopiedReadAheadBuffer(t *testing.T) {
// Create the pipe.
name := pipename()
if err := mkpipe(name); err != nil {
t.Fatalf("failed to make host pipe: %v", err)
}
defer syscall.Unlink(name)
// We're taking advantage of the fact that pipes opened read only always return
// success, but internally they are not deemed "opened" until we're sure that
// another writer comes along. This means we can open the same pipe write only
// with no problems + write to it, given that opener.Open already tried to open
// the pipe RDONLY and succeeded, which we know happened if TryOpen returns
// syserror.ErrwouldBlock.
//
// This simulates the open(RDONLY) <-> open(WRONLY)+write race we care about, but
// does not cause our test to be racy (which would be terrible).
opener := &hostOpener{name: name}
pipeOpenState := &pipeOpenState{}
ctx := contexttest.Context(t)
pipeOps, err := pipeOpenState.TryOpen(ctx, opener, fs.FileFlags{Read: true})
if pipeOps != nil {
pipeOps.Release()
t.Fatalf("open(%s, %o) got file, want nil", name, syscall.O_RDONLY)
}
if err != syserror.ErrWouldBlock {
t.Fatalf("open(%s, %o) got error %v, want %v", name, syscall.O_RDONLY, err, syserror.ErrWouldBlock)
}
// Then open the same pipe write only and write some bytes to it. The next
// time we try to open the pipe read only again via the pipeOpenState, we should
// succeed and buffer some of the bytes written.
fd, err := syscall.Open(name, syscall.O_WRONLY, 0666)
if err != nil {
t.Fatalf("open(%s, %o) got error %v, want nil", name, syscall.O_WRONLY, err)
}
defer syscall.Close(fd)
data := []byte("hello")
if n, err := syscall.Write(fd, data); n != len(data) || err != nil {
t.Fatalf("write(%v) got (%d, %v), want (%d, nil)", data, n, err, len(data))
}
// Try the read again, knowing that it should succeed this time.
pipeOps, err = pipeOpenState.TryOpen(ctx, opener, fs.FileFlags{Read: true})
if pipeOps == nil {
t.Fatalf("open(%s, %o) got nil file, want not nil", name, syscall.O_RDONLY)
}
defer pipeOps.Release()
if err != nil {
t.Fatalf("open(%s, %o) got error %v, want nil", name, syscall.O_RDONLY, err)
}
inode := fs.NewMockInode(ctx, fs.NewMockMountSource(nil), fs.StableAttr{
Type: fs.Pipe,
})
file := fs.NewFile(ctx, fs.NewDirent(ctx, inode, "pipe"), fs.FileFlags{Read: true}, pipeOps)
// Check that the file we opened points to a pipe with a non-empty read ahead buffer.
bufsize := len(pipeOps.readAheadBuffer)
if bufsize != 1 {
t.Fatalf("read ahead buffer got %d bytes, want %d", bufsize, 1)
}
// Now for the final test, try to read everything in, expecting to get back all of
// the bytes that were written at once. Note that in the wild there is no atomic
// read size so expecting to get all bytes from a single writer when there are
// multiple readers is a bad expectation.
buf := make([]byte, len(data))
ioseq := usermem.BytesIOSequence(buf)
n, err := pipeOps.Read(ctx, file, ioseq, 0)
if err != nil {
t.Fatalf("read request got error %v, want nil", err)
}
if n != int64(len(data)) {
t.Fatalf("read request got %d bytes, want %d", n, len(data))
}
if !bytes.Equal(buf, data) {
t.Errorf("read request got bytes [%v], want [%v]", buf, data)
}
}
func TestPipeHangup(t *testing.T) {
for _, test := range []struct {
// desc is the test's description.
desc string
// flags control how we open our end of the pipe and must be read
// only or write only. They also dicate how a coordinating partner
// fd is opened, which is their inverse (read only -> write only, etc).
flags fs.FileFlags
// hangupSelf if true causes the test case to close our end of the pipe
// and causes hangup errors to be asserted on our coordinating partner's
// fd. If hangupSelf is false, then our partner's fd is closed and the
// hangup errors are expected on our end of the pipe.
hangupSelf bool
}{
{
desc: "Read only gets hangup error",
flags: fs.FileFlags{Read: true},
},
{
desc: "Write only gets hangup error",
flags: fs.FileFlags{Write: true},
},
{
desc: "Read only generates hangup error",
flags: fs.FileFlags{Read: true},
hangupSelf: true,
},
{
desc: "Write only generates hangup error",
flags: fs.FileFlags{Write: true},
hangupSelf: true,
},
} {
if test.flags.Read == test.flags.Write {
t.Errorf("%s: test requires a single reader or writer", test.desc)
continue
}
// Create the pipe. We do this per-test case to keep tests independent.
name := pipename()
if err := mkpipe(name); err != nil {
t.Errorf("%s: failed to make host pipe: %v", test.desc, err)
continue
}
defer syscall.Unlink(name)
// Fire off a partner routine which tries to open the same pipe blocking,
// which will synchronize with us. The channel allows us to get back the
// fd once we expect this partner routine to succeed, so we can manifest
// hangup events more directly.
fdchan := make(chan int, 1)
go func() {
// Be explicit about the flags to protect the test from
// misconfiguration.
var flags int
if test.flags.Read {
flags = syscall.O_WRONLY
} else {
flags = syscall.O_RDONLY
}
fd, err := syscall.Open(name, flags, 0666)
if err != nil {
t.Logf("Open(%q, %o, 0666) partner failed: %v", name, flags, err)
}
fdchan <- fd
}()
// Open our end in a blocking way to ensure that we coordinate.
opener := &hostOpener{name: name}
ctx := contexttest.Context(t)
pipeOps, err := Open(ctx, opener, test.flags)
if err != nil {
t.Errorf("%s: Open got error %v, want nil", test.desc, err)
continue
}
// Don't defer file.DecRef here because that causes the hangup we're
// trying to test for.
// Expect the partner routine to have coordinated with us and get back
// its open fd.
f := <-fdchan
if f < 0 {
t.Errorf("%s: partner routine got fd %d, want > 0", test.desc, f)
pipeOps.Release()
continue
}
if test.hangupSelf {
// Hangup self and assert that our partner got the expected hangup
// error.
pipeOps.Release()
if test.flags.Read {
// Partner is writer.
assertWriterHungup(t, test.desc, fd.NewReadWriter(f))
} else {
// Partner is reader.
assertReaderHungup(t, test.desc, fd.NewReadWriter(f))
}
} else {
// Hangup our partner and expect us to get the hangup error.
syscall.Close(f)
defer pipeOps.Release()
if test.flags.Read {
assertReaderHungup(t, test.desc, pipeOps.(*pipeOperations).file)
} else {
assertWriterHungup(t, test.desc, pipeOps.(*pipeOperations).file)
}
}
}
}
func assertReaderHungup(t *testing.T, desc string, reader io.Reader) bool {
// Drain the pipe completely, it might have crap in it, but expect EOF eventually.
var err error
for err == nil {
_, err = reader.Read(make([]byte, 10))
}
if err != io.EOF {
t.Errorf("%s: read from self after hangup got error %v, want %v", desc, err, io.EOF)
return false
}
return true
}
func assertWriterHungup(t *testing.T, desc string, writer io.Writer) bool {
if _, err := writer.Write([]byte("hello")); unwrapError(err) != syscall.EPIPE {
t.Errorf("%s: write to self after hangup got error %v, want %v", desc, err, syscall.EPIPE)
return false
}
return true
}