Skip to content

Commit

Permalink
Expose underlying file's SyscallConn method.
Browse files Browse the repository at this point in the history
This allows raw access to the underlying FD, enabling things like
in-kernel copy methods such as `splice` and `tee`.

This interface was added to `*os.File` as part of go1.12beta2.

Signed-off-by: Brian Goff <cpuguy83@gmail.com>
  • Loading branch information
cpuguy83 committed Jan 15, 2019
1 parent 3d5202a commit 094bd06
Show file tree
Hide file tree
Showing 3 changed files with 299 additions and 0 deletions.
2 changes: 2 additions & 0 deletions fifo.go
Expand Up @@ -29,6 +29,7 @@ import (

type fifo struct {
flag int
block bool
opened chan struct{}
closed chan struct{}
closing chan struct{}
Expand Down Expand Up @@ -75,6 +76,7 @@ func OpenFifo(ctx context.Context, fn string, flag int, perm os.FileMode) (io.Re
f := &fifo{
handle: h,
flag: flag,
block: block,
opened: make(chan struct{}),
closed: make(chan struct{}),
closing: make(chan struct{}),
Expand Down
113 changes: 113 additions & 0 deletions raw.go
@@ -0,0 +1,113 @@
// +build go1.12

package fifo

import (
"syscall"

"github.com/pkg/errors"
)

// SyscallConn provides raw access to the fifo's underlying filedescrptor.
// See syscall.Conn for guarentees provided by this interface.
func (f *fifo) SyscallConn() (syscall.RawConn, error) {
select {
case <-f.opened:
return newRawConn(f)
default:
}

if !f.block {
rc := &rawConn{f: f, ready: make(chan struct{})}
go func() {
select {
case <-f.closed:
return
case <-f.opened:
rc.raw, rc.err = f.file.SyscallConn()
close(rc.ready)
}
}()

return rc, nil
}

select {
case <-f.opened:
return newRawConn(f)
case <-f.closed:
return nil, errors.New("fifo closed")
}
}

// newRawConn creates a new syscall.RawConn from a fifo
//
// Note that this assumes the fifo is open
// It is recommended to only call this through `fifo.SyscallConn`
func newRawConn(f *fifo) (syscall.RawConn, error) {
raw, err := f.file.SyscallConn()
if err != nil {
return nil, err
}

ready := make(chan struct{})
close(ready)
return &rawConn{f: f, raw: raw, ready: ready}, nil
}

type rawConn struct {
f *fifo
ready chan struct{}
raw syscall.RawConn
err error
}

func (r *rawConn) Control(f func(fd uintptr)) error {
select {
case <-r.f.closed:
return errors.New("control of closed fifo")
case <-r.ready:
}

if r.err != nil {
return r.err
}

return r.raw.Control(f)
}

func (r *rawConn) Read(f func(fd uintptr) (done bool)) error {
if r.f.flag&syscall.O_WRONLY > 0 {
return errors.New("reading from write-only fifo")
}

select {
case <-r.f.closed:
return errors.New("reading of a closed fifo")
case <-r.ready:
}

if r.err != nil {
return r.err
}

return r.raw.Read(f)
}

func (r *rawConn) Write(f func(fd uintptr) (done bool)) error {
if r.f.flag&(syscall.O_WRONLY|syscall.O_RDWR) == 0 {
return errors.New("writing to read-only fifo")
}

select {
case <-r.f.closed:
return errors.New("writing to a closed fifo")
case <-r.ready:
}

if r.err != nil {
return r.err
}

return r.raw.Write(f)
}
184 changes: 184 additions & 0 deletions raw_test.go
@@ -0,0 +1,184 @@
// +build go1.12

package fifo

import (
"bytes"
"io"
"io/ioutil"
"os"
"path"
"path/filepath"
"syscall"
"testing"
"time"

"github.com/stretchr/testify/assert"
"golang.org/x/net/context"
)

func TestRawReadWrite(t *testing.T) {
tmpdir, err := ioutil.TempDir("", "fifos")
assert.NoError(t, err)
defer os.RemoveAll(tmpdir)

ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()

r, err := OpenFifo(ctx, filepath.Join(tmpdir, t.Name()), syscall.O_RDONLY|syscall.O_CREAT|syscall.O_NONBLOCK, 0600)
assert.NoError(t, err)
defer r.Close()
rawR := makeRawConn(t, r, false)
assert.Error(t, rawR.Write(func(uintptr) bool { return true }))

w, err := OpenFifo(ctx, filepath.Join(tmpdir, t.Name()), syscall.O_WRONLY|syscall.O_NONBLOCK, 0)
assert.NoError(t, err)
defer w.Close()
rawW := makeRawConn(t, w, false)
assert.Error(t, rawW.Read(func(uintptr) bool { return true }))

data := []byte("hello world")
rawWrite(t, rawW, data)

dataR := make([]byte, len(data))
rawRead(t, rawR, dataR)
assert.True(t, bytes.Equal(data, dataR))
}

func TestRawWriteUserRead(t *testing.T) {
tmpdir, err := ioutil.TempDir("", "fifos")
assert.NoError(t, err)
defer os.RemoveAll(tmpdir)

ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()

w, err := OpenFifo(ctx, filepath.Join(tmpdir, t.Name()), syscall.O_WRONLY|syscall.O_CREAT|syscall.O_NONBLOCK, 0600)
assert.NoError(t, err)
defer w.Close()
rawW := makeRawConn(t, w, false)

r, err := OpenFifo(ctx, filepath.Join(tmpdir, t.Name()), syscall.O_RDONLY|syscall.O_CREAT|syscall.O_NONBLOCK, 0600)
assert.NoError(t, err)
defer r.Close()

data := []byte("hello world!")
rawWrite(t, rawW, data)
w.Close()

buf := make([]byte, len(data))
n, err := io.ReadFull(r, buf)
assert.NoError(t, err)
assert.True(t, bytes.Equal(data, buf[:n]))
}

func TestUserWriteRawRead(t *testing.T) {
tmpdir, err := ioutil.TempDir("", "fifos")
assert.NoError(t, err)
defer os.RemoveAll(tmpdir)

ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()

w, err := OpenFifo(ctx, filepath.Join(tmpdir, t.Name()), syscall.O_WRONLY|syscall.O_CREAT|syscall.O_NONBLOCK, 0600)
assert.NoError(t, err)
defer w.Close()

r, err := OpenFifo(ctx, filepath.Join(tmpdir, t.Name()), syscall.O_RDONLY|syscall.O_CREAT|syscall.O_NONBLOCK, 0600)
assert.NoError(t, err)
defer r.Close()
rawR := makeRawConn(t, r, false)

data := []byte("hello world!")
n, err := w.Write(data)
assert.NoError(t, err)
assert.Equal(t, n, len(data))
w.Close()

buf := make([]byte, len(data))
rawRead(t, rawR, buf)
assert.True(t, bytes.Equal(data, buf[:n]))
}

func TestRawCloseError(t *testing.T) {
tmpdir, err := ioutil.TempDir("", "fifos")
assert.NoError(t, err)
defer os.RemoveAll(tmpdir)

t.Run("SyscallConnAfterClose", func(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
defer cancel()

f, err := OpenFifo(ctx, filepath.Join(tmpdir, path.Base(t.Name())), syscall.O_RDWR|syscall.O_CREAT, 0600)
assert.NoError(t, err)
f.Close()
makeRawConn(t, f, true)
})

t.Run("RawOpsAfterClose", func(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
defer cancel()
f, err := OpenFifo(ctx, filepath.Join(tmpdir, path.Base(t.Name())), syscall.O_RDWR|syscall.O_CREAT, 0600)
assert.NoError(t, err)
defer f.Close()

raw := makeRawConn(t, f, false)

f.Close()

assert.Error(t, raw.Control(func(uintptr) {}))
dummy := func(uintptr) bool { return true }
assert.Error(t, raw.Write(dummy))
assert.Error(t, raw.Read(dummy))
})
}

func makeRawConn(t *testing.T, fifo io.ReadWriteCloser, expectError bool) syscall.RawConn {
sc, ok := fifo.(syscall.Conn)
assert.True(t, ok, "not a syscall.Conn")

raw, err := sc.SyscallConn()
if !expectError {
assert.NoError(t, err)
}

return raw
}

func rawWrite(t *testing.T, rc syscall.RawConn, data []byte) {
var written int
var wErr error

err := rc.Write(func(fd uintptr) bool {
var n int
n, wErr = syscall.Write(int(fd), data[written:])
written += n
if wErr != nil || n == 0 || written == len(data) {
return true
}
return false
})
assert.NoError(t, err)
assert.NoError(t, wErr)
assert.Equal(t, written, len(data))
}

func rawRead(t *testing.T, rc syscall.RawConn, data []byte) {
var (
rErr error
read int
)

err := rc.Read(func(fd uintptr) bool {
var n int
n, rErr = syscall.Read(int(fd), data[read:])
read += n
if rErr != nil || n == 0 || read == len(data) {
return true
}
return false
})
assert.NoError(t, err)
assert.NoError(t, rErr)
assert.Equal(t, read, len(data))
}

0 comments on commit 094bd06

Please sign in to comment.