mirror of
https://github.com/hanzoai/visor.git
synced 2026-09-24 17:04:14 +00:00
627 lines
19 KiB
Go
627 lines
19 KiB
Go
// Copyright 2020 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 fuse
|
|
|
|
import (
|
|
goContext "context"
|
|
"sync"
|
|
|
|
"golang.org/x/sys/unix"
|
|
"gvisor.dev/gvisor/pkg/abi/linux"
|
|
"gvisor.dev/gvisor/pkg/atomicbitops"
|
|
"gvisor.dev/gvisor/pkg/context"
|
|
"gvisor.dev/gvisor/pkg/errors/linuxerr"
|
|
"gvisor.dev/gvisor/pkg/log"
|
|
"gvisor.dev/gvisor/pkg/sentry/kernel"
|
|
"gvisor.dev/gvisor/pkg/sentry/kernel/auth"
|
|
"gvisor.dev/gvisor/pkg/syserr"
|
|
"gvisor.dev/gvisor/pkg/usermem"
|
|
"gvisor.dev/gvisor/pkg/waiter"
|
|
)
|
|
|
|
const (
|
|
// fuseDefaultMaxBackground is the default value for MaxBackground.
|
|
fuseDefaultMaxBackground = 12
|
|
|
|
// fuseDefaultCongestionThreshold is the default value for CongestionThreshold,
|
|
// and is 75% of the default maximum of MaxGround.
|
|
fuseDefaultCongestionThreshold = (fuseDefaultMaxBackground * 3 / 4)
|
|
|
|
// fuseDefaultMaxPagesPerReq is the default value for MaxPagesPerReq.
|
|
fuseDefaultMaxPagesPerReq = 32
|
|
)
|
|
|
|
// fuseConn abstracts the FUSE request/response transport. The connection
|
|
// struct delegates call dispatch to its fuseConn implementation.
|
|
type fuseConn interface {
|
|
call(ctx context.Context, r *Request) (*Response, error)
|
|
release(ctx context.Context)
|
|
}
|
|
|
|
// deviceConn implements fuseConn for the in-sandbox /dev/fuse path.
|
|
// It uses the queue-based mechanism where the FUSE daemon reads requests
|
|
// from and writes responses to the DeviceFD.
|
|
type deviceConn struct {
|
|
conn *connection
|
|
}
|
|
|
|
func (dc *deviceConn) call(ctx context.Context, r *Request) (*Response, error) {
|
|
fut, err := dc.conn.callFuture(ctx, r)
|
|
if err != nil {
|
|
return nil, linuxError(err)
|
|
}
|
|
res, err := fut.resolve(ctx)
|
|
if err != nil {
|
|
return res, linuxError(err)
|
|
}
|
|
return res, nil
|
|
}
|
|
|
|
func (dc *deviceConn) release(ctx context.Context) {}
|
|
|
|
// connection is the struct by which the sentry communicates with the FUSE server daemon.
|
|
//
|
|
// Lock order:
|
|
// - conn.fd.mu
|
|
// - conn.mu
|
|
// - conn.asyncMu
|
|
//
|
|
// +stateify savable
|
|
type connection struct {
|
|
connectionRefs
|
|
|
|
// fuseConn is the transport implementation. For the DeviceFD path this
|
|
// is a *deviceConn; for host passthrough this is a *hostConnection.
|
|
fuseConn fuseConn `state:"nosave"`
|
|
|
|
// We target FUSE 7.23.
|
|
// The following FUSE_INIT flags are currently unsupported by this implementation:
|
|
// - FUSE_EXPORT_SUPPORT
|
|
// - FUSE_POSIX_LOCKS: requires POSIX locks
|
|
// - FUSE_FLOCK_LOCKS: requires POSIX locks
|
|
// - FUSE_AUTO_INVAL_DATA: requires page caching eviction
|
|
// - FUSE_DO_READDIRPLUS/FUSE_READDIRPLUS_AUTO: requires FUSE_READDIRPLUS implementation
|
|
// - FUSE_ASYNC_DIO
|
|
// - FUSE_PARALLEL_DIROPS (7.25)
|
|
// - FUSE_HANDLE_KILLPRIV (7.26)
|
|
// - FUSE_POSIX_ACL: affects defaultPermissions, posixACL, xattr handler (7.26)
|
|
// - FUSE_ABORT_ERROR (7.27)
|
|
// - FUSE_CACHE_SYMLINKS (7.28)
|
|
// - FUSE_NO_OPENDIR_SUPPORT (7.29)
|
|
// - FUSE_EXPLICIT_INVAL_DATA: requires page caching eviction (7.30)
|
|
// - FUSE_MAP_ALIGNMENT (7.31)
|
|
|
|
// initialized after receiving FUSE_INIT reply.
|
|
// Until it's set, suspend sending FUSE requests.
|
|
// Use setInitialized() and isInitialized() for atomic access.
|
|
initialized atomicbitops.Int32
|
|
|
|
// initializedChan is used to block requests before initialization.
|
|
initializedChan chan struct{} `state:".(bool)"`
|
|
|
|
// waitQueue is used to notify the readers that there is something to read.
|
|
waitQueue waiter.Queue
|
|
|
|
// fullQueueCh is a channel used to synchronize the readers with the writers.
|
|
// Writers (inbound requests to the filesystem) block if there are too many
|
|
// unprocessed in-flight requests.
|
|
fullQueueCh chan struct{} `state:".(int)"`
|
|
|
|
// mu protects access to struct members.
|
|
mu sync.Mutex `state:"nosave"`
|
|
|
|
// numActiveRequests is the number of active requests.
|
|
//
|
|
// +checklocks:mu
|
|
numActiveRequests uint64
|
|
|
|
// completions is used to map a request to its response. A Writer will use this
|
|
// to notify the caller of a completed response.
|
|
//
|
|
// +checklocks:mu
|
|
completions map[linux.FUSEOpID]*futureResponse
|
|
|
|
// queue is the list of requests that need to be processed by the FUSE server.
|
|
//
|
|
// +checklocks:mu
|
|
queue requestList
|
|
|
|
// nextOpID is used to create new requests.
|
|
//
|
|
// +checklocks:mu
|
|
nextOpID linux.FUSEOpID
|
|
|
|
// writeBuf is the memory buffer used to copy in the FUSE out header from
|
|
// userspace.
|
|
//
|
|
// +checklocks:mu
|
|
writeBuf [fuseHeaderOutSize]byte
|
|
|
|
// attributeVersion is the version of connection's attributes.
|
|
attributeVersion atomicbitops.Uint64
|
|
|
|
// connected (connection established) when a new FUSE file system is created.
|
|
// Set to false when:
|
|
// umount,
|
|
// connection abort,
|
|
// device release.
|
|
//
|
|
// +checklocks:mu
|
|
connected bool
|
|
|
|
// connInitError if FUSE_INIT encountered error (major version mismatch).
|
|
// Only set in INIT.
|
|
//
|
|
// +checklocks:mu
|
|
connInitError bool
|
|
|
|
// connInitSuccess if FUSE_INIT is successful.
|
|
// Only set in INIT.
|
|
// Used for destroy (not yet implemented).
|
|
//
|
|
// +checklocks:mu
|
|
connInitSuccess bool
|
|
|
|
// aborted via sysfs, and will send ECONNABORTED to read after disconnection (instead of ENODEV).
|
|
// Set only if abortErr is true and via fuse control fs (not yet implemented).
|
|
// TODO(gvisor.dev/issue/3525): set this to true when user aborts.
|
|
aborted bool
|
|
|
|
// numWaiting is the number of requests waiting to be
|
|
// sent to FUSE device or being processed by FUSE daemon.
|
|
numWaiting uint32
|
|
|
|
// Terminology note:
|
|
//
|
|
// - `asyncNumMax` is the `MaxBackground` in the FUSE_INIT_IN struct.
|
|
//
|
|
// - `asyncCongestionThreshold` is the `CongestionThreshold` in the FUSE_INIT_IN struct.
|
|
//
|
|
// We call the "background" requests in unix term as async requests.
|
|
// The "async requests" in unix term is our async requests that expect a reply,
|
|
// i.e. `!request.noReply`
|
|
|
|
// asyncMu protects the async request fields.
|
|
asyncMu sync.Mutex `state:"nosave"`
|
|
|
|
// asyncNum is the number of async requests.
|
|
//
|
|
// +checklocks:asyncMu
|
|
asyncNum uint16
|
|
|
|
// asyncCongestionThreshold the number of async requests.
|
|
// Negotiated in FUSE_INIT as "CongestionThreshold".
|
|
// TODO(gvisor.dev/issue/3529): add congestion control.
|
|
//
|
|
// +checklocks:asyncMu
|
|
asyncCongestionThreshold uint16
|
|
|
|
// asyncNumMax is the maximum number of asyncNum.
|
|
// Connection blocks the async requests when it is reached.
|
|
// Negotiated in FUSE_INIT as "MaxBackground".
|
|
//
|
|
// +checklocks:asyncMu
|
|
asyncNumMax uint16
|
|
|
|
// maxRead is the maximum size of a read buffer in in bytes.
|
|
// Initialized from a fuse fs parameter.
|
|
maxRead uint32
|
|
|
|
// maxWrite is the maximum size of a write buffer in bytes.
|
|
// Negotiated in FUSE_INIT.
|
|
maxWrite uint32
|
|
|
|
// maxPages is the maximum number of pages for a single request to use.
|
|
// Negotiated in FUSE_INIT.
|
|
maxPages uint16
|
|
|
|
// maxActiveRequests specifies the maximum number of active requests that can
|
|
// exist at any time. Any further requests will block when trying to CAll
|
|
// the server.
|
|
maxActiveRequests uint64
|
|
|
|
// minor version of the FUSE protocol.
|
|
// Negotiated and only set in INIT.
|
|
minor uint32
|
|
|
|
// atomicOTrunc is true when FUSE does not send a separate SETATTR request
|
|
// before open with O_TRUNC flag.
|
|
// Negotiated and only set in INIT.
|
|
atomicOTrunc bool
|
|
|
|
// asyncRead if read pages asynchronously.
|
|
// Negotiated and only set in INIT.
|
|
asyncRead bool
|
|
|
|
// writebackCache is true for write-back cache policy,
|
|
// false for write-through policy.
|
|
// Negotiated and only set in INIT.
|
|
writebackCache bool
|
|
|
|
// bigWrites if doing multi-page cached writes.
|
|
// Negotiated and only set in INIT.
|
|
bigWrites bool
|
|
|
|
// dontMask if filesystem does not apply umask to creation modes.
|
|
// Negotiated in INIT.
|
|
dontMask bool
|
|
|
|
// noOpen if FUSE server doesn't support open operation.
|
|
// This flag only influences performance, not correctness of the program.
|
|
noOpen bool
|
|
}
|
|
|
|
func linuxError(err error) error {
|
|
if err == nil {
|
|
return nil
|
|
}
|
|
// The error may contain arbitrary errno values that can't be converted.
|
|
switch e := err.(type) {
|
|
case unix.Errno:
|
|
if syserr.IsValid(e) {
|
|
return linuxerr.ErrorFromUnix(e)
|
|
}
|
|
default:
|
|
return err
|
|
}
|
|
log.Warningf("fusefs: failed with invalid error: %v", err)
|
|
return linuxerr.EINVAL
|
|
}
|
|
|
|
// setInitializedLocked atomically sets the connection as initialized.
|
|
//
|
|
// +checklocks:conn.mu
|
|
func (conn *connection) setInitializedLocked() {
|
|
if conn.initialized.Load() != 0 {
|
|
return
|
|
}
|
|
|
|
// Unblock the requests sent before INIT.
|
|
close(conn.initializedChan)
|
|
|
|
// Close the channel first to avoid the non-atomic situation
|
|
// where conn.initialized is true but there are
|
|
// tasks being blocked on the channel.
|
|
// And it prevents the newer tasks from gaining
|
|
// unnecessary higher chance to be issued before the blocked one.
|
|
|
|
conn.initialized.Store(1)
|
|
}
|
|
|
|
// setInitialized atomically sets the connection as initialized.
|
|
func (conn *connection) setInitialized() {
|
|
if conn.isInitialized() {
|
|
return
|
|
}
|
|
|
|
conn.mu.Lock()
|
|
defer conn.mu.Unlock()
|
|
|
|
conn.setInitializedLocked()
|
|
}
|
|
|
|
// isInitialized atomically check if the connection is initialized.
|
|
func (conn *connection) isInitialized() bool {
|
|
return conn.initialized.Load() != 0
|
|
}
|
|
|
|
func (conn *connection) saveInitializedChan() bool {
|
|
select {
|
|
case <-conn.initializedChan:
|
|
return true // Closed.
|
|
default:
|
|
return false // Not closed.
|
|
}
|
|
}
|
|
|
|
func (conn *connection) loadInitializedChan(_ goContext.Context, closed bool) {
|
|
conn.initializedChan = make(chan struct{}, 1)
|
|
if closed {
|
|
close(conn.initializedChan)
|
|
}
|
|
}
|
|
|
|
// newFUSEConnection creates a FUSE connection to fuseFD.
|
|
// +checklocks:fuseFD.mu
|
|
func newFUSEConnection(_ context.Context, fuseFD *DeviceFD, opts *filesystemOptions) (*connection, error) {
|
|
// Mark the device as ready so it can be used.
|
|
// FIXME(gvisor.dev/issue/4813): fuseFD's fields are accessed without
|
|
// synchronization and without checking if fuseFD has already been used to
|
|
// mount another filesystem.
|
|
|
|
return newFUSEConnectionOpts(opts)
|
|
}
|
|
|
|
// newFUSEConnectionOpts creates a FUSE connection with the given options.
|
|
// This is used by both the DeviceFD path and the host FD passthrough path.
|
|
func newFUSEConnectionOpts(opts *filesystemOptions) (*connection, error) {
|
|
conn := &connection{
|
|
completions: make(map[linux.FUSEOpID]*futureResponse),
|
|
fullQueueCh: make(chan struct{}, opts.maxActiveRequests),
|
|
asyncNumMax: fuseDefaultMaxBackground,
|
|
asyncCongestionThreshold: fuseDefaultCongestionThreshold,
|
|
maxRead: opts.maxRead,
|
|
maxPages: fuseDefaultMaxPagesPerReq,
|
|
maxActiveRequests: opts.maxActiveRequests,
|
|
initializedChan: make(chan struct{}),
|
|
connected: true,
|
|
}
|
|
conn.fuseConn = &deviceConn{conn: conn}
|
|
conn.InitRefs()
|
|
return conn, nil
|
|
}
|
|
|
|
func (conn *connection) DecRef(ctx context.Context) {
|
|
conn.connectionRefs.DecRef(func() {
|
|
conn.Abort(ctx)
|
|
conn.waitQueue.Notify(waiter.ReadableEvents)
|
|
})
|
|
}
|
|
|
|
// CallAsync makes an async (aka background) request.
|
|
// It's a simple wrapper around Call().
|
|
func (conn *connection) CallAsync(ctx context.Context, r *Request) error {
|
|
r.async = true
|
|
_, err := conn.Call(ctx, r)
|
|
return err
|
|
}
|
|
|
|
// Call makes a request to the server.
|
|
// Block before the connection is initialized.
|
|
// When the Request is FUSE_INIT, it will not be blocked before initialization.
|
|
// Task should never be nil.
|
|
//
|
|
// For a sync request, it blocks the invoking task until
|
|
// a server responds with a response.
|
|
//
|
|
// For an async request (that do not expect a response immediately),
|
|
// it returns directly unless being blocked either before initialization
|
|
// or when there are too many async requests ongoing.
|
|
//
|
|
// Example for async request:
|
|
// init, readahead, write, async read/write, fuse_notify_reply,
|
|
// non-sync release, interrupt, forget.
|
|
//
|
|
// The forget request does not have a reply,
|
|
// as documented in include/uapi/linux/fuse.h:FUSE_FORGET.
|
|
func (conn *connection) Call(ctx context.Context, r *Request) (*Response, error) {
|
|
// Block requests sent before connection is initialized.
|
|
if !conn.isInitialized() && r.hdr.Opcode != linux.FUSE_INIT {
|
|
if err := ctx.Block(conn.initializedChan); err != nil {
|
|
return nil, linuxError(err)
|
|
}
|
|
}
|
|
|
|
conn.mu.Lock()
|
|
connected := conn.connected
|
|
connInitError := conn.connInitError
|
|
conn.mu.Unlock()
|
|
|
|
if !connected {
|
|
return nil, linuxerr.ENOTCONN
|
|
}
|
|
|
|
if connInitError {
|
|
return nil, linuxerr.ECONNREFUSED
|
|
}
|
|
|
|
return conn.fuseConn.call(ctx, r)
|
|
}
|
|
|
|
// callFuture makes a request to the server and returns a future response.
|
|
// Call resolve() when the response needs to be fulfilled.
|
|
func (conn *connection) callFuture(b context.Blocker, r *Request) (*futureResponse, error) {
|
|
conn.mu.Lock()
|
|
defer conn.mu.Unlock()
|
|
|
|
// Is the queue full?
|
|
//
|
|
// We must busy wait here until the request can be queued. We don't
|
|
// block on the fd.fullQueueCh with a lock - so after being signalled,
|
|
// before we acquire the lock, it is possible that a barging task enters
|
|
// and queues a request. As a result, upon acquiring the lock we must
|
|
// again check if the room is available.
|
|
//
|
|
// This can potentially starve a request forever but this can only happen
|
|
// if there are always too many ongoing requests all the time. The
|
|
// supported maxActiveRequests setting should be really high to avoid this.
|
|
for conn.numActiveRequests == conn.maxActiveRequests {
|
|
log.Infof("Blocking request %v from being queued. Too many active requests: %v",
|
|
r.id, conn.numActiveRequests)
|
|
conn.mu.Unlock()
|
|
err := b.Block(conn.fullQueueCh)
|
|
conn.mu.Lock()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
return conn.callFutureLocked(r)
|
|
}
|
|
|
|
// callFutureLocked makes a request to the server and returns a future response.
|
|
//
|
|
// +checklocks:conn.mu
|
|
func (conn *connection) callFutureLocked(r *Request) (*futureResponse, error) {
|
|
// Check connected again holding conn.mu.
|
|
if !conn.connected {
|
|
// we checked connected before,
|
|
// this must be due to aborted connection.
|
|
return nil, linuxerr.ECONNABORTED
|
|
}
|
|
|
|
conn.queue.PushBack(r)
|
|
conn.numActiveRequests++
|
|
fut := newFutureResponse(r)
|
|
conn.completions[r.id] = fut
|
|
|
|
// Signal the readers that there is something to read.
|
|
conn.waitQueue.Notify(waiter.ReadableEvents)
|
|
|
|
return fut, nil
|
|
}
|
|
|
|
// sendResponse sends a response to the waiting task (if any).
|
|
//
|
|
// +checklocks:conn.mu
|
|
func (conn *connection) sendResponse(ctx context.Context, fut *futureResponse) error {
|
|
// Signal the task waiting on a response if any.
|
|
defer close(fut.ch)
|
|
|
|
// Signal that the queue is no longer full.
|
|
select {
|
|
case conn.fullQueueCh <- struct{}{}:
|
|
default:
|
|
}
|
|
conn.numActiveRequests--
|
|
|
|
if fut.async {
|
|
return conn.asyncCallBack(ctx, fut.getResponse())
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// asyncCallBack executes pre-defined callback function for async requests.
|
|
// Currently used by: FUSE_INIT.
|
|
//
|
|
// +checklocks:conn.mu
|
|
func (conn *connection) asyncCallBack(ctx context.Context, r *Response) error {
|
|
switch r.opcode {
|
|
case linux.FUSE_INIT:
|
|
creds := auth.CredentialsFromContext(ctx)
|
|
rootUserNs := kernel.KernelFromContext(ctx).RootUserNamespace()
|
|
return conn.InitRecv(r, creds.HasCapabilityIn(linux.CAP_SYS_ADMIN, rootUserNs))
|
|
// TODO(gvisor.dev/issue/3247): support async read: correctly process the response.
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// readiness returns the readiness of the connection.
|
|
func (conn *connection) readiness(ready waiter.EventMask) waiter.EventMask {
|
|
conn.mu.Lock()
|
|
defer conn.mu.Unlock()
|
|
// FD is always writable.
|
|
ready |= waiter.WritableEvents
|
|
if !conn.queue.Empty() {
|
|
// Have reqs available, FD is readable.
|
|
ready |= waiter.ReadableEvents
|
|
}
|
|
return ready
|
|
}
|
|
|
|
func (conn *connection) read(ctx context.Context, dst usermem.IOSequence) (int64, error) {
|
|
conn.mu.Lock()
|
|
defer conn.mu.Unlock()
|
|
minBuffSize := linux.FUSE_MIN_READ_BUFFER
|
|
// We require that any Read done on this filesystem have a sane minimum
|
|
// read buffer. It must have the capacity for the fixed parts of any request
|
|
// header (Linux uses the request header and the FUSEWriteIn header for this
|
|
// calculation) + the negotiated MaxWrite room for the data.
|
|
negotiatedMinBuffSize := linux.SizeOfFUSEHeaderIn + linux.SizeOfFUSEHeaderOut + conn.maxWrite
|
|
if minBuffSize < negotiatedMinBuffSize {
|
|
minBuffSize = negotiatedMinBuffSize
|
|
}
|
|
|
|
// If the read buffer is too small, error out.
|
|
if dst.NumBytes() < int64(minBuffSize) {
|
|
return 0, linuxerr.EINVAL
|
|
}
|
|
// Find the first valid request. For the normal case this loop only executes
|
|
// once.
|
|
var req *Request
|
|
for req = conn.queue.Front(); !conn.queue.Empty(); req = conn.queue.Front() {
|
|
if int64(req.hdr.Len) <= dst.NumBytes() {
|
|
break
|
|
}
|
|
// The request is too large so we cannot process it. All requests must be
|
|
// smaller than the negotiated size as specified by Connection.MaxWrite set
|
|
// as part of the FUSE_INIT handshake.
|
|
errno := -int32(unix.EIO)
|
|
if req.hdr.Opcode == linux.FUSE_SETXATTR {
|
|
errno = -int32(unix.E2BIG)
|
|
}
|
|
|
|
if err := conn.sendError(ctx, errno, req.hdr.Unique); err != nil {
|
|
return 0, err
|
|
}
|
|
conn.queue.Remove(req)
|
|
req = nil
|
|
}
|
|
if req == nil {
|
|
return 0, linuxerr.ErrWouldBlock
|
|
}
|
|
|
|
// We already checked the size: dst must be able to fit the whole request.
|
|
n, err := dst.CopyOut(ctx, req.data)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
if n != len(req.data) {
|
|
return 0, linuxerr.EIO
|
|
}
|
|
conn.queue.Remove(req)
|
|
// Remove noReply ones from the map of requests expecting a reply.
|
|
if req.noReply {
|
|
conn.numActiveRequests--
|
|
delete(conn.completions, req.hdr.Unique)
|
|
}
|
|
return int64(n), nil
|
|
}
|
|
|
|
func (conn *connection) write(ctx context.Context, src usermem.IOSequence) (int64, error) {
|
|
conn.mu.Lock()
|
|
defer conn.mu.Unlock()
|
|
var hdr linux.FUSEHeaderOut
|
|
if src.NumBytes() < int64(hdr.SizeBytes()) {
|
|
return 0, linuxerr.EINVAL
|
|
}
|
|
n, err := src.CopyIn(ctx, conn.writeBuf[:])
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
hdr.UnmarshalBytes(conn.writeBuf[:])
|
|
if src.NumBytes() != int64(hdr.Len) {
|
|
return 0, linuxerr.EINVAL
|
|
}
|
|
|
|
fut, ok := conn.completions[hdr.Unique]
|
|
if !ok {
|
|
// Server sent us a response for a request we never sent, or for which we
|
|
// already received a reply (e.g. aborted), an unlikely event.
|
|
return 0, linuxerr.EINVAL
|
|
}
|
|
delete(conn.completions, hdr.Unique)
|
|
|
|
// Copy over the header into the future response. The rest of the payload
|
|
// will be copied over to the FR's data in the next iteration.
|
|
fut.hdr = &hdr
|
|
fut.data = make([]byte, fut.hdr.Len)
|
|
copy(fut.data, conn.writeBuf[:])
|
|
if fut.hdr.Len > uint32(len(conn.writeBuf)) {
|
|
src = src.DropFirst(len(conn.writeBuf))
|
|
n2, err := src.CopyIn(ctx, fut.data[len(conn.writeBuf):])
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
n += n2
|
|
}
|
|
if err := conn.sendResponse(ctx, fut); err != nil {
|
|
return 0, err
|
|
}
|
|
return int64(n), nil
|
|
}
|