523 lines
15 KiB
Go
523 lines
15 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 (
|
|
"bytes"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"path"
|
|
"syscall"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/google/uuid"
|
|
"gvisor.dev/gvisor/pkg/fd"
|
|
"gvisor.dev/gvisor/pkg/sentry/context"
|
|
"gvisor.dev/gvisor/pkg/sentry/context/contexttest"
|
|
"gvisor.dev/gvisor/pkg/sentry/fs"
|
|
"gvisor.dev/gvisor/pkg/sentry/usermem"
|
|
"gvisor.dev/gvisor/pkg/syserror"
|
|
)
|
|
|
|
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
|
|
}
|