diff options
Diffstat (limited to 'pkg/sentry/fsimpl/fuse')
-rw-r--r-- | pkg/sentry/fsimpl/fuse/BUILD | 87 | ||||
-rw-r--r-- | pkg/sentry/fsimpl/fuse/connection_test.go | 117 | ||||
-rw-r--r-- | pkg/sentry/fsimpl/fuse/dev_test.go | 323 | ||||
-rw-r--r-- | pkg/sentry/fsimpl/fuse/fuse_state_autogen.go | 525 | ||||
-rw-r--r-- | pkg/sentry/fsimpl/fuse/inode_refs.go | 137 | ||||
-rw-r--r-- | pkg/sentry/fsimpl/fuse/request_list.go | 193 | ||||
-rw-r--r-- | pkg/sentry/fsimpl/fuse/utils_test.go | 126 |
7 files changed, 855 insertions, 653 deletions
diff --git a/pkg/sentry/fsimpl/fuse/BUILD b/pkg/sentry/fsimpl/fuse/BUILD deleted file mode 100644 index 2158b1bbc..000000000 --- a/pkg/sentry/fsimpl/fuse/BUILD +++ /dev/null @@ -1,87 +0,0 @@ -load("//tools:defs.bzl", "go_library", "go_test") -load("//tools/go_generics:defs.bzl", "go_template_instance") - -licenses(["notice"]) - -go_template_instance( - name = "request_list", - out = "request_list.go", - package = "fuse", - prefix = "request", - template = "//pkg/ilist:generic_list", - types = { - "Element": "*Request", - "Linker": "*Request", - }, -) - -go_template_instance( - name = "inode_refs", - out = "inode_refs.go", - package = "fuse", - prefix = "inode", - template = "//pkg/refsvfs2:refs_template", - types = { - "T": "inode", - }, -) - -go_library( - name = "fuse", - srcs = [ - "connection.go", - "connection_control.go", - "dev.go", - "directory.go", - "file.go", - "fusefs.go", - "inode_refs.go", - "read_write.go", - "register.go", - "regular_file.go", - "request_list.go", - "request_response.go", - ], - visibility = ["//pkg/sentry:internal"], - deps = [ - "//pkg/abi/linux", - "//pkg/context", - "//pkg/log", - "//pkg/marshal", - "//pkg/refs", - "//pkg/refsvfs2", - "//pkg/safemem", - "//pkg/sentry/fsimpl/devtmpfs", - "//pkg/sentry/fsimpl/kernfs", - "//pkg/sentry/kernel", - "//pkg/sentry/kernel/auth", - "//pkg/sentry/vfs", - "//pkg/sync", - "//pkg/syserror", - "//pkg/usermem", - "//pkg/waiter", - "@org_golang_x_sys//unix:go_default_library", - ], -) - -go_test( - name = "fuse_test", - size = "small", - srcs = [ - "connection_test.go", - "dev_test.go", - "utils_test.go", - ], - library = ":fuse", - deps = [ - "//pkg/abi/linux", - "//pkg/marshal", - "//pkg/sentry/fsimpl/testutil", - "//pkg/sentry/kernel", - "//pkg/sentry/kernel/auth", - "//pkg/sentry/vfs", - "//pkg/syserror", - "//pkg/usermem", - "//pkg/waiter", - ], -) diff --git a/pkg/sentry/fsimpl/fuse/connection_test.go b/pkg/sentry/fsimpl/fuse/connection_test.go deleted file mode 100644 index 91d16c1cf..000000000 --- a/pkg/sentry/fsimpl/fuse/connection_test.go +++ /dev/null @@ -1,117 +0,0 @@ -// 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 ( - "math/rand" - "syscall" - "testing" - - "gvisor.dev/gvisor/pkg/sentry/kernel" - "gvisor.dev/gvisor/pkg/sentry/kernel/auth" - "gvisor.dev/gvisor/pkg/syserror" -) - -// TestConnectionInitBlock tests if initialization -// correctly blocks and unblocks the connection. -// Since it's unfeasible to test kernelTask.Block() in unit test, -// the code in Call() are not tested here. -func TestConnectionInitBlock(t *testing.T) { - s := setup(t) - defer s.Destroy() - - k := kernel.KernelFromContext(s.Ctx) - - conn, _, err := newTestConnection(s, k, maxActiveRequestsDefault) - if err != nil { - t.Fatalf("newTestConnection: %v", err) - } - - select { - case <-conn.initializedChan: - t.Fatalf("initializedChan should be blocking before SetInitialized") - default: - } - - conn.SetInitialized() - - select { - case <-conn.initializedChan: - default: - t.Fatalf("initializedChan should not be blocking after SetInitialized") - } -} - -func TestConnectionAbort(t *testing.T) { - s := setup(t) - defer s.Destroy() - - k := kernel.KernelFromContext(s.Ctx) - creds := auth.CredentialsFromContext(s.Ctx) - task := kernel.TaskFromContext(s.Ctx) - - const numRequests uint64 = 256 - - conn, _, err := newTestConnection(s, k, numRequests) - if err != nil { - t.Fatalf("newTestConnection: %v", err) - } - - testObj := &testPayload{ - data: rand.Uint32(), - } - - var futNormal []*futureResponse - - for i := 0; i < int(numRequests); i++ { - req, err := conn.NewRequest(creds, uint32(i), uint64(i), 0, testObj) - if err != nil { - t.Fatalf("NewRequest creation failed: %v", err) - } - fut, err := conn.callFutureLocked(task, req) - if err != nil { - t.Fatalf("callFutureLocked failed: %v", err) - } - futNormal = append(futNormal, fut) - } - - conn.Abort(s.Ctx) - - // Abort should unblock the initialization channel. - // Note: no test requests are actually blocked on `conn.initializedChan`. - select { - case <-conn.initializedChan: - default: - t.Fatalf("initializedChan should not be blocking after SetInitialized") - } - - // Abort will return ECONNABORTED error to unblocked requests. - for _, fut := range futNormal { - if fut.getResponse().hdr.Error != -int32(syscall.ECONNABORTED) { - t.Fatalf("Incorrect error code received for aborted connection: %v", fut.getResponse().hdr.Error) - } - } - - // After abort, Call() should return directly with ENOTCONN. - req, err := conn.NewRequest(creds, 0, 0, 0, testObj) - if err != nil { - t.Fatalf("NewRequest creation failed: %v", err) - } - _, err = conn.Call(task, req) - if err != syserror.ENOTCONN { - t.Fatalf("Incorrect error code received for Call() after connection aborted") - } - -} diff --git a/pkg/sentry/fsimpl/fuse/dev_test.go b/pkg/sentry/fsimpl/fuse/dev_test.go deleted file mode 100644 index 95c475a65..000000000 --- a/pkg/sentry/fsimpl/fuse/dev_test.go +++ /dev/null @@ -1,323 +0,0 @@ -// 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 ( - "fmt" - "math/rand" - "testing" - - "gvisor.dev/gvisor/pkg/abi/linux" - "gvisor.dev/gvisor/pkg/sentry/fsimpl/testutil" - "gvisor.dev/gvisor/pkg/sentry/kernel" - "gvisor.dev/gvisor/pkg/sentry/kernel/auth" - "gvisor.dev/gvisor/pkg/sentry/vfs" - "gvisor.dev/gvisor/pkg/syserror" - "gvisor.dev/gvisor/pkg/usermem" - "gvisor.dev/gvisor/pkg/waiter" -) - -// echoTestOpcode is the Opcode used during testing. The server used in tests -// will simply echo the payload back with the appropriate headers. -const echoTestOpcode linux.FUSEOpcode = 1000 - -// TestFUSECommunication tests that the communication layer between the Sentry and the -// FUSE server daemon works as expected. -func TestFUSECommunication(t *testing.T) { - s := setup(t) - defer s.Destroy() - - k := kernel.KernelFromContext(s.Ctx) - creds := auth.CredentialsFromContext(s.Ctx) - - // Create test cases with different number of concurrent clients and servers. - testCases := []struct { - Name string - NumClients int - NumServers int - MaxActiveRequests uint64 - }{ - { - Name: "SingleClientSingleServer", - NumClients: 1, - NumServers: 1, - MaxActiveRequests: maxActiveRequestsDefault, - }, - { - Name: "SingleClientMultipleServers", - NumClients: 1, - NumServers: 10, - MaxActiveRequests: maxActiveRequestsDefault, - }, - { - Name: "MultipleClientsSingleServer", - NumClients: 10, - NumServers: 1, - MaxActiveRequests: maxActiveRequestsDefault, - }, - { - Name: "MultipleClientsMultipleServers", - NumClients: 10, - NumServers: 10, - MaxActiveRequests: maxActiveRequestsDefault, - }, - { - Name: "RequestCapacityFull", - NumClients: 10, - NumServers: 1, - MaxActiveRequests: 1, - }, - { - Name: "RequestCapacityContinuouslyFull", - NumClients: 100, - NumServers: 2, - MaxActiveRequests: 2, - }, - } - - for _, testCase := range testCases { - t.Run(testCase.Name, func(t *testing.T) { - conn, fd, err := newTestConnection(s, k, testCase.MaxActiveRequests) - if err != nil { - t.Fatalf("newTestConnection: %v", err) - } - - clientsDone := make([]chan struct{}, testCase.NumClients) - serversDone := make([]chan struct{}, testCase.NumServers) - serversKill := make([]chan struct{}, testCase.NumServers) - - // FUSE clients. - for i := 0; i < testCase.NumClients; i++ { - clientsDone[i] = make(chan struct{}) - go func(i int) { - fuseClientRun(t, s, k, conn, creds, uint32(i), uint64(i), clientsDone[i]) - }(i) - } - - // FUSE servers. - for j := 0; j < testCase.NumServers; j++ { - serversDone[j] = make(chan struct{}) - serversKill[j] = make(chan struct{}, 1) // The kill command shouldn't block. - go func(j int) { - fuseServerRun(t, s, k, fd, serversDone[j], serversKill[j]) - }(j) - } - - // Tear down. - // - // Make sure all the clients are done. - for i := 0; i < testCase.NumClients; i++ { - <-clientsDone[i] - } - - // Kill any server that is potentially waiting. - for j := 0; j < testCase.NumServers; j++ { - serversKill[j] <- struct{}{} - } - - // Make sure all the servers are done. - for j := 0; j < testCase.NumServers; j++ { - <-serversDone[j] - } - }) - } -} - -// CallTest makes a request to the server and blocks the invoking -// goroutine until a server responds with a response. Doesn't block -// a kernel.Task. Analogous to Connection.Call but used for testing. -func CallTest(conn *connection, t *kernel.Task, r *Request, i uint32) (*Response, error) { - conn.fd.mu.Lock() - - // Wait until we're certain that a new request can be processed. - for conn.fd.numActiveRequests == conn.fd.fs.opts.maxActiveRequests { - conn.fd.mu.Unlock() - select { - case <-conn.fd.fullQueueCh: - } - conn.fd.mu.Lock() - } - - fut, err := conn.callFutureLocked(t, r) // No task given. - conn.fd.mu.Unlock() - - if err != nil { - return nil, err - } - - // Resolve the response. - // - // Block without a task. - select { - case <-fut.ch: - } - - // A response is ready. Resolve and return it. - return fut.getResponse(), nil -} - -// ReadTest is analogous to vfs.FileDescription.Read and reads from the FUSE -// device. However, it does so by - not blocking the task that is calling - and -// instead just waits on a channel. The behaviour is essentially the same as -// DeviceFD.Read except it guarantees that the task is not blocked. -func ReadTest(serverTask *kernel.Task, fd *vfs.FileDescription, inIOseq usermem.IOSequence, killServer chan struct{}) (int64, bool, error) { - var err error - var n, total int64 - - dev := fd.Impl().(*DeviceFD) - - // Register for notifications. - w, ch := waiter.NewChannelEntry(nil) - dev.EventRegister(&w, waiter.EventIn) - for { - // Issue the request and break out if it completes with anything other than - // "would block". - n, err = dev.Read(serverTask, inIOseq, vfs.ReadOptions{}) - total += n - if err != syserror.ErrWouldBlock { - break - } - - // Wait for a notification that we should retry. - // Emulate the blocking for when no requests are available - select { - case <-ch: - case <-killServer: - // Server killed by the main program. - return 0, true, nil - } - } - - dev.EventUnregister(&w) - return total, false, err -} - -// fuseClientRun emulates all the actions of a normal FUSE request. It creates -// a header, a payload, calls the server, waits for the response, and processes -// the response. -func fuseClientRun(t *testing.T, s *testutil.System, k *kernel.Kernel, conn *connection, creds *auth.Credentials, pid uint32, inode uint64, clientDone chan struct{}) { - defer func() { clientDone <- struct{}{} }() - - tc := k.NewThreadGroup(nil, k.RootPIDNamespace(), kernel.NewSignalHandlers(), linux.SIGCHLD, k.GlobalInit().Limits()) - clientTask, err := testutil.CreateTask(s.Ctx, fmt.Sprintf("fuse-client-%v", pid), tc, s.MntNs, s.Root, s.Root) - if err != nil { - t.Fatal(err) - } - testObj := &testPayload{ - data: rand.Uint32(), - } - - req, err := conn.NewRequest(creds, pid, inode, echoTestOpcode, testObj) - if err != nil { - t.Fatalf("NewRequest creation failed: %v", err) - } - - // Queue up a request. - // Analogous to Call except it doesn't block on the task. - resp, err := CallTest(conn, clientTask, req, pid) - if err != nil { - t.Fatalf("CallTaskNonBlock failed: %v", err) - } - - if err = resp.Error(); err != nil { - t.Fatalf("Server responded with an error: %v", err) - } - - var respTestPayload testPayload - if err := resp.UnmarshalPayload(&respTestPayload); err != nil { - t.Fatalf("Unmarshalling payload error: %v", err) - } - - if resp.hdr.Unique != req.hdr.Unique { - t.Fatalf("got response for another request. Expected response for req %v but got response for req %v", - req.hdr.Unique, resp.hdr.Unique) - } - - if respTestPayload.data != testObj.data { - t.Fatalf("read incorrect data. Data expected: %v, but got %v", testObj.data, respTestPayload.data) - } - -} - -// fuseServerRun creates a task and emulates all the actions of a simple FUSE server -// that simply reads a request and echos the same struct back as a response using the -// appropriate headers. -func fuseServerRun(t *testing.T, s *testutil.System, k *kernel.Kernel, fd *vfs.FileDescription, serverDone, killServer chan struct{}) { - defer func() { serverDone <- struct{}{} }() - - // Create the tasks that the server will be using. - tc := k.NewThreadGroup(nil, k.RootPIDNamespace(), kernel.NewSignalHandlers(), linux.SIGCHLD, k.GlobalInit().Limits()) - var readPayload testPayload - - serverTask, err := testutil.CreateTask(s.Ctx, "fuse-server", tc, s.MntNs, s.Root, s.Root) - if err != nil { - t.Fatal(err) - } - - // Read the request. - for { - inHdrLen := uint32((*linux.FUSEHeaderIn)(nil).SizeBytes()) - payloadLen := uint32(readPayload.SizeBytes()) - - // The raed buffer must meet some certain size criteria. - buffSize := inHdrLen + payloadLen - if buffSize < linux.FUSE_MIN_READ_BUFFER { - buffSize = linux.FUSE_MIN_READ_BUFFER - } - inBuf := make([]byte, buffSize) - inIOseq := usermem.BytesIOSequence(inBuf) - - n, serverKilled, err := ReadTest(serverTask, fd, inIOseq, killServer) - if err != nil { - t.Fatalf("Read failed :%v", err) - } - - // Server should shut down. No new requests are going to be made. - if serverKilled { - break - } - - if n <= 0 { - t.Fatalf("Read read no bytes") - } - - var readFUSEHeaderIn linux.FUSEHeaderIn - readFUSEHeaderIn.UnmarshalUnsafe(inBuf[:inHdrLen]) - readPayload.UnmarshalUnsafe(inBuf[inHdrLen : inHdrLen+payloadLen]) - - if readFUSEHeaderIn.Opcode != echoTestOpcode { - t.Fatalf("read incorrect data. Header: %v, Payload: %v", readFUSEHeaderIn, readPayload) - } - - // Write the response. - outHdrLen := uint32((*linux.FUSEHeaderOut)(nil).SizeBytes()) - outBuf := make([]byte, outHdrLen+payloadLen) - outHeader := linux.FUSEHeaderOut{ - Len: outHdrLen + payloadLen, - Error: 0, - Unique: readFUSEHeaderIn.Unique, - } - - // Echo the payload back. - outHeader.MarshalUnsafe(outBuf[:outHdrLen]) - readPayload.MarshalUnsafe(outBuf[outHdrLen:]) - outIOseq := usermem.BytesIOSequence(outBuf) - - _, err = fd.Write(s.Ctx, outIOseq, vfs.WriteOptions{}) - if err != nil { - t.Fatalf("Write failed :%v", err) - } - } -} diff --git a/pkg/sentry/fsimpl/fuse/fuse_state_autogen.go b/pkg/sentry/fsimpl/fuse/fuse_state_autogen.go new file mode 100644 index 000000000..f59e82755 --- /dev/null +++ b/pkg/sentry/fsimpl/fuse/fuse_state_autogen.go @@ -0,0 +1,525 @@ +// automatically generated by stateify. + +package fuse + +import ( + "gvisor.dev/gvisor/pkg/state" +) + +func (conn *connection) StateTypeName() string { + return "pkg/sentry/fsimpl/fuse.connection" +} + +func (conn *connection) StateFields() []string { + return []string{ + "fd", + "attributeVersion", + "initialized", + "initializedChan", + "connected", + "connInitError", + "connInitSuccess", + "aborted", + "numWaiting", + "asyncNum", + "asyncCongestionThreshold", + "asyncNumMax", + "maxRead", + "maxWrite", + "maxPages", + "minor", + "atomicOTrunc", + "asyncRead", + "writebackCache", + "bigWrites", + "dontMask", + "noOpen", + } +} + +func (conn *connection) beforeSave() {} + +func (conn *connection) StateSave(stateSinkObject state.Sink) { + conn.beforeSave() + var initializedChanValue bool = conn.saveInitializedChan() + stateSinkObject.SaveValue(3, initializedChanValue) + stateSinkObject.Save(0, &conn.fd) + stateSinkObject.Save(1, &conn.attributeVersion) + stateSinkObject.Save(2, &conn.initialized) + stateSinkObject.Save(4, &conn.connected) + stateSinkObject.Save(5, &conn.connInitError) + stateSinkObject.Save(6, &conn.connInitSuccess) + stateSinkObject.Save(7, &conn.aborted) + stateSinkObject.Save(8, &conn.numWaiting) + stateSinkObject.Save(9, &conn.asyncNum) + stateSinkObject.Save(10, &conn.asyncCongestionThreshold) + stateSinkObject.Save(11, &conn.asyncNumMax) + stateSinkObject.Save(12, &conn.maxRead) + stateSinkObject.Save(13, &conn.maxWrite) + stateSinkObject.Save(14, &conn.maxPages) + stateSinkObject.Save(15, &conn.minor) + stateSinkObject.Save(16, &conn.atomicOTrunc) + stateSinkObject.Save(17, &conn.asyncRead) + stateSinkObject.Save(18, &conn.writebackCache) + stateSinkObject.Save(19, &conn.bigWrites) + stateSinkObject.Save(20, &conn.dontMask) + stateSinkObject.Save(21, &conn.noOpen) +} + +func (conn *connection) afterLoad() {} + +func (conn *connection) StateLoad(stateSourceObject state.Source) { + stateSourceObject.Load(0, &conn.fd) + stateSourceObject.Load(1, &conn.attributeVersion) + stateSourceObject.Load(2, &conn.initialized) + stateSourceObject.Load(4, &conn.connected) + stateSourceObject.Load(5, &conn.connInitError) + stateSourceObject.Load(6, &conn.connInitSuccess) + stateSourceObject.Load(7, &conn.aborted) + stateSourceObject.Load(8, &conn.numWaiting) + stateSourceObject.Load(9, &conn.asyncNum) + stateSourceObject.Load(10, &conn.asyncCongestionThreshold) + stateSourceObject.Load(11, &conn.asyncNumMax) + stateSourceObject.Load(12, &conn.maxRead) + stateSourceObject.Load(13, &conn.maxWrite) + stateSourceObject.Load(14, &conn.maxPages) + stateSourceObject.Load(15, &conn.minor) + stateSourceObject.Load(16, &conn.atomicOTrunc) + stateSourceObject.Load(17, &conn.asyncRead) + stateSourceObject.Load(18, &conn.writebackCache) + stateSourceObject.Load(19, &conn.bigWrites) + stateSourceObject.Load(20, &conn.dontMask) + stateSourceObject.Load(21, &conn.noOpen) + stateSourceObject.LoadValue(3, new(bool), func(y interface{}) { conn.loadInitializedChan(y.(bool)) }) +} + +func (f *fuseDevice) StateTypeName() string { + return "pkg/sentry/fsimpl/fuse.fuseDevice" +} + +func (f *fuseDevice) StateFields() []string { + return []string{} +} + +func (f *fuseDevice) beforeSave() {} + +func (f *fuseDevice) StateSave(stateSinkObject state.Sink) { + f.beforeSave() +} + +func (f *fuseDevice) afterLoad() {} + +func (f *fuseDevice) StateLoad(stateSourceObject state.Source) { +} + +func (fd *DeviceFD) StateTypeName() string { + return "pkg/sentry/fsimpl/fuse.DeviceFD" +} + +func (fd *DeviceFD) StateFields() []string { + return []string{ + "vfsfd", + "FileDescriptionDefaultImpl", + "DentryMetadataFileDescriptionImpl", + "NoLockFD", + "nextOpID", + "queue", + "numActiveRequests", + "completions", + "writeCursor", + "writeBuf", + "writeCursorFR", + "waitQueue", + "fullQueueCh", + "fs", + } +} + +func (fd *DeviceFD) beforeSave() {} + +func (fd *DeviceFD) StateSave(stateSinkObject state.Sink) { + fd.beforeSave() + var fullQueueChValue int = fd.saveFullQueueCh() + stateSinkObject.SaveValue(12, fullQueueChValue) + stateSinkObject.Save(0, &fd.vfsfd) + stateSinkObject.Save(1, &fd.FileDescriptionDefaultImpl) + stateSinkObject.Save(2, &fd.DentryMetadataFileDescriptionImpl) + stateSinkObject.Save(3, &fd.NoLockFD) + stateSinkObject.Save(4, &fd.nextOpID) + stateSinkObject.Save(5, &fd.queue) + stateSinkObject.Save(6, &fd.numActiveRequests) + stateSinkObject.Save(7, &fd.completions) + stateSinkObject.Save(8, &fd.writeCursor) + stateSinkObject.Save(9, &fd.writeBuf) + stateSinkObject.Save(10, &fd.writeCursorFR) + stateSinkObject.Save(11, &fd.waitQueue) + stateSinkObject.Save(13, &fd.fs) +} + +func (fd *DeviceFD) afterLoad() {} + +func (fd *DeviceFD) StateLoad(stateSourceObject state.Source) { + stateSourceObject.Load(0, &fd.vfsfd) + stateSourceObject.Load(1, &fd.FileDescriptionDefaultImpl) + stateSourceObject.Load(2, &fd.DentryMetadataFileDescriptionImpl) + stateSourceObject.Load(3, &fd.NoLockFD) + stateSourceObject.Load(4, &fd.nextOpID) + stateSourceObject.Load(5, &fd.queue) + stateSourceObject.Load(6, &fd.numActiveRequests) + stateSourceObject.Load(7, &fd.completions) + stateSourceObject.Load(8, &fd.writeCursor) + stateSourceObject.Load(9, &fd.writeBuf) + stateSourceObject.Load(10, &fd.writeCursorFR) + stateSourceObject.Load(11, &fd.waitQueue) + stateSourceObject.Load(13, &fd.fs) + stateSourceObject.LoadValue(12, new(int), func(y interface{}) { fd.loadFullQueueCh(y.(int)) }) +} + +func (fsType *FilesystemType) StateTypeName() string { + return "pkg/sentry/fsimpl/fuse.FilesystemType" +} + +func (fsType *FilesystemType) StateFields() []string { + return []string{} +} + +func (fsType *FilesystemType) beforeSave() {} + +func (fsType *FilesystemType) StateSave(stateSinkObject state.Sink) { + fsType.beforeSave() +} + +func (fsType *FilesystemType) afterLoad() {} + +func (fsType *FilesystemType) StateLoad(stateSourceObject state.Source) { +} + +func (f *filesystemOptions) StateTypeName() string { + return "pkg/sentry/fsimpl/fuse.filesystemOptions" +} + +func (f *filesystemOptions) StateFields() []string { + return []string{ + "userID", + "groupID", + "rootMode", + "maxActiveRequests", + "maxRead", + } +} + +func (f *filesystemOptions) beforeSave() {} + +func (f *filesystemOptions) StateSave(stateSinkObject state.Sink) { + f.beforeSave() + stateSinkObject.Save(0, &f.userID) + stateSinkObject.Save(1, &f.groupID) + stateSinkObject.Save(2, &f.rootMode) + stateSinkObject.Save(3, &f.maxActiveRequests) + stateSinkObject.Save(4, &f.maxRead) +} + +func (f *filesystemOptions) afterLoad() {} + +func (f *filesystemOptions) StateLoad(stateSourceObject state.Source) { + stateSourceObject.Load(0, &f.userID) + stateSourceObject.Load(1, &f.groupID) + stateSourceObject.Load(2, &f.rootMode) + stateSourceObject.Load(3, &f.maxActiveRequests) + stateSourceObject.Load(4, &f.maxRead) +} + +func (fs *filesystem) StateTypeName() string { + return "pkg/sentry/fsimpl/fuse.filesystem" +} + +func (fs *filesystem) StateFields() []string { + return []string{ + "Filesystem", + "devMinor", + "conn", + "opts", + "umounted", + } +} + +func (fs *filesystem) beforeSave() {} + +func (fs *filesystem) StateSave(stateSinkObject state.Sink) { + fs.beforeSave() + stateSinkObject.Save(0, &fs.Filesystem) + stateSinkObject.Save(1, &fs.devMinor) + stateSinkObject.Save(2, &fs.conn) + stateSinkObject.Save(3, &fs.opts) + stateSinkObject.Save(4, &fs.umounted) +} + +func (fs *filesystem) afterLoad() {} + +func (fs *filesystem) StateLoad(stateSourceObject state.Source) { + stateSourceObject.Load(0, &fs.Filesystem) + stateSourceObject.Load(1, &fs.devMinor) + stateSourceObject.Load(2, &fs.conn) + stateSourceObject.Load(3, &fs.opts) + stateSourceObject.Load(4, &fs.umounted) +} + +func (i *inode) StateTypeName() string { + return "pkg/sentry/fsimpl/fuse.inode" +} + +func (i *inode) StateFields() []string { + return []string{ + "inodeRefs", + "InodeAlwaysValid", + "InodeAttrs", + "InodeDirectoryNoNewChildren", + "InodeNotSymlink", + "OrderedChildren", + "fs", + "metadataMu", + "nodeID", + "locks", + "size", + "attributeVersion", + "attributeTime", + "version", + "link", + } +} + +func (i *inode) beforeSave() {} + +func (i *inode) StateSave(stateSinkObject state.Sink) { + i.beforeSave() + stateSinkObject.Save(0, &i.inodeRefs) + stateSinkObject.Save(1, &i.InodeAlwaysValid) + stateSinkObject.Save(2, &i.InodeAttrs) + stateSinkObject.Save(3, &i.InodeDirectoryNoNewChildren) + stateSinkObject.Save(4, &i.InodeNotSymlink) + stateSinkObject.Save(5, &i.OrderedChildren) + stateSinkObject.Save(6, &i.fs) + stateSinkObject.Save(7, &i.metadataMu) + stateSinkObject.Save(8, &i.nodeID) + stateSinkObject.Save(9, &i.locks) + stateSinkObject.Save(10, &i.size) + stateSinkObject.Save(11, &i.attributeVersion) + stateSinkObject.Save(12, &i.attributeTime) + stateSinkObject.Save(13, &i.version) + stateSinkObject.Save(14, &i.link) +} + +func (i *inode) afterLoad() {} + +func (i *inode) StateLoad(stateSourceObject state.Source) { + stateSourceObject.Load(0, &i.inodeRefs) + stateSourceObject.Load(1, &i.InodeAlwaysValid) + stateSourceObject.Load(2, &i.InodeAttrs) + stateSourceObject.Load(3, &i.InodeDirectoryNoNewChildren) + stateSourceObject.Load(4, &i.InodeNotSymlink) + stateSourceObject.Load(5, &i.OrderedChildren) + stateSourceObject.Load(6, &i.fs) + stateSourceObject.Load(7, &i.metadataMu) + stateSourceObject.Load(8, &i.nodeID) + stateSourceObject.Load(9, &i.locks) + stateSourceObject.Load(10, &i.size) + stateSourceObject.Load(11, &i.attributeVersion) + stateSourceObject.Load(12, &i.attributeTime) + stateSourceObject.Load(13, &i.version) + stateSourceObject.Load(14, &i.link) +} + +func (r *inodeRefs) StateTypeName() string { + return "pkg/sentry/fsimpl/fuse.inodeRefs" +} + +func (r *inodeRefs) StateFields() []string { + return []string{ + "refCount", + } +} + +func (r *inodeRefs) beforeSave() {} + +func (r *inodeRefs) StateSave(stateSinkObject state.Sink) { + r.beforeSave() + stateSinkObject.Save(0, &r.refCount) +} + +func (r *inodeRefs) StateLoad(stateSourceObject state.Source) { + stateSourceObject.Load(0, &r.refCount) + stateSourceObject.AfterLoad(r.afterLoad) +} + +func (l *requestList) StateTypeName() string { + return "pkg/sentry/fsimpl/fuse.requestList" +} + +func (l *requestList) StateFields() []string { + return []string{ + "head", + "tail", + } +} + +func (l *requestList) beforeSave() {} + +func (l *requestList) StateSave(stateSinkObject state.Sink) { + l.beforeSave() + stateSinkObject.Save(0, &l.head) + stateSinkObject.Save(1, &l.tail) +} + +func (l *requestList) afterLoad() {} + +func (l *requestList) StateLoad(stateSourceObject state.Source) { + stateSourceObject.Load(0, &l.head) + stateSourceObject.Load(1, &l.tail) +} + +func (e *requestEntry) StateTypeName() string { + return "pkg/sentry/fsimpl/fuse.requestEntry" +} + +func (e *requestEntry) StateFields() []string { + return []string{ + "next", + "prev", + } +} + +func (e *requestEntry) beforeSave() {} + +func (e *requestEntry) StateSave(stateSinkObject state.Sink) { + e.beforeSave() + stateSinkObject.Save(0, &e.next) + stateSinkObject.Save(1, &e.prev) +} + +func (e *requestEntry) afterLoad() {} + +func (e *requestEntry) StateLoad(stateSourceObject state.Source) { + stateSourceObject.Load(0, &e.next) + stateSourceObject.Load(1, &e.prev) +} + +func (r *Request) StateTypeName() string { + return "pkg/sentry/fsimpl/fuse.Request" +} + +func (r *Request) StateFields() []string { + return []string{ + "requestEntry", + "id", + "hdr", + "data", + "payload", + "async", + "noReply", + } +} + +func (r *Request) beforeSave() {} + +func (r *Request) StateSave(stateSinkObject state.Sink) { + r.beforeSave() + stateSinkObject.Save(0, &r.requestEntry) + stateSinkObject.Save(1, &r.id) + stateSinkObject.Save(2, &r.hdr) + stateSinkObject.Save(3, &r.data) + stateSinkObject.Save(4, &r.payload) + stateSinkObject.Save(5, &r.async) + stateSinkObject.Save(6, &r.noReply) +} + +func (r *Request) afterLoad() {} + +func (r *Request) StateLoad(stateSourceObject state.Source) { + stateSourceObject.Load(0, &r.requestEntry) + stateSourceObject.Load(1, &r.id) + stateSourceObject.Load(2, &r.hdr) + stateSourceObject.Load(3, &r.data) + stateSourceObject.Load(4, &r.payload) + stateSourceObject.Load(5, &r.async) + stateSourceObject.Load(6, &r.noReply) +} + +func (f *futureResponse) StateTypeName() string { + return "pkg/sentry/fsimpl/fuse.futureResponse" +} + +func (f *futureResponse) StateFields() []string { + return []string{ + "opcode", + "ch", + "hdr", + "data", + "async", + } +} + +func (f *futureResponse) beforeSave() {} + +func (f *futureResponse) StateSave(stateSinkObject state.Sink) { + f.beforeSave() + stateSinkObject.Save(0, &f.opcode) + stateSinkObject.Save(1, &f.ch) + stateSinkObject.Save(2, &f.hdr) + stateSinkObject.Save(3, &f.data) + stateSinkObject.Save(4, &f.async) +} + +func (f *futureResponse) afterLoad() {} + +func (f *futureResponse) StateLoad(stateSourceObject state.Source) { + stateSourceObject.Load(0, &f.opcode) + stateSourceObject.Load(1, &f.ch) + stateSourceObject.Load(2, &f.hdr) + stateSourceObject.Load(3, &f.data) + stateSourceObject.Load(4, &f.async) +} + +func (r *Response) StateTypeName() string { + return "pkg/sentry/fsimpl/fuse.Response" +} + +func (r *Response) StateFields() []string { + return []string{ + "opcode", + "hdr", + "data", + } +} + +func (r *Response) beforeSave() {} + +func (r *Response) StateSave(stateSinkObject state.Sink) { + r.beforeSave() + stateSinkObject.Save(0, &r.opcode) + stateSinkObject.Save(1, &r.hdr) + stateSinkObject.Save(2, &r.data) +} + +func (r *Response) afterLoad() {} + +func (r *Response) StateLoad(stateSourceObject state.Source) { + stateSourceObject.Load(0, &r.opcode) + stateSourceObject.Load(1, &r.hdr) + stateSourceObject.Load(2, &r.data) +} + +func init() { + state.Register((*connection)(nil)) + state.Register((*fuseDevice)(nil)) + state.Register((*DeviceFD)(nil)) + state.Register((*FilesystemType)(nil)) + state.Register((*filesystemOptions)(nil)) + state.Register((*filesystem)(nil)) + state.Register((*inode)(nil)) + state.Register((*inodeRefs)(nil)) + state.Register((*requestList)(nil)) + state.Register((*requestEntry)(nil)) + state.Register((*Request)(nil)) + state.Register((*futureResponse)(nil)) + state.Register((*Response)(nil)) +} diff --git a/pkg/sentry/fsimpl/fuse/inode_refs.go b/pkg/sentry/fsimpl/fuse/inode_refs.go new file mode 100644 index 000000000..37a39e976 --- /dev/null +++ b/pkg/sentry/fsimpl/fuse/inode_refs.go @@ -0,0 +1,137 @@ +package fuse + +import ( + "fmt" + "sync/atomic" + + "gvisor.dev/gvisor/pkg/refsvfs2" +) + +// enableLogging indicates whether reference-related events should be logged (with +// stack traces). This is false by default and should only be set to true for +// debugging purposes, as it can generate an extremely large amount of output +// and drastically degrade performance. +const inodeenableLogging = false + +// obj is used to customize logging. Note that we use a pointer to T so that +// we do not copy the entire object when passed as a format parameter. +var inodeobj *inode + +// Refs implements refs.RefCounter. It keeps a reference count using atomic +// operations and calls the destructor when the count reaches zero. +// +// +stateify savable +type inodeRefs struct { + // refCount is composed of two fields: + // + // [32-bit speculative references]:[32-bit real references] + // + // Speculative references are used for TryIncRef, to avoid a CompareAndSwap + // loop. See IncRef, DecRef and TryIncRef for details of how these fields are + // used. + refCount int64 +} + +// InitRefs initializes r with one reference and, if enabled, activates leak +// checking. +func (r *inodeRefs) InitRefs() { + atomic.StoreInt64(&r.refCount, 1) + refsvfs2.Register(r) +} + +// RefType implements refsvfs2.CheckedObject.RefType. +func (r *inodeRefs) RefType() string { + return fmt.Sprintf("%T", inodeobj)[1:] +} + +// LeakMessage implements refsvfs2.CheckedObject.LeakMessage. +func (r *inodeRefs) LeakMessage() string { + return fmt.Sprintf("[%s %p] reference count of %d instead of 0", r.RefType(), r, r.ReadRefs()) +} + +// LogRefs implements refsvfs2.CheckedObject.LogRefs. +func (r *inodeRefs) LogRefs() bool { + return inodeenableLogging +} + +// EnableLeakCheck enables reference leak checking on r. +func (r *inodeRefs) EnableLeakCheck() { + refsvfs2.Register(r) +} + +// ReadRefs returns the current number of references. The returned count is +// inherently racy and is unsafe to use without external synchronization. +func (r *inodeRefs) ReadRefs() int64 { + return atomic.LoadInt64(&r.refCount) +} + +// IncRef implements refs.RefCounter.IncRef. +// +//go:nosplit +func (r *inodeRefs) IncRef() { + v := atomic.AddInt64(&r.refCount, 1) + if inodeenableLogging { + refsvfs2.LogIncRef(r, v) + } + if v <= 1 { + panic(fmt.Sprintf("Incrementing non-positive count %p on %s", r, r.RefType())) + } +} + +// TryIncRef implements refs.RefCounter.TryIncRef. +// +// To do this safely without a loop, a speculative reference is first acquired +// on the object. This allows multiple concurrent TryIncRef calls to distinguish +// other TryIncRef calls from genuine references held. +// +//go:nosplit +func (r *inodeRefs) TryIncRef() bool { + const speculativeRef = 1 << 32 + if v := atomic.AddInt64(&r.refCount, speculativeRef); int32(v) == 0 { + + atomic.AddInt64(&r.refCount, -speculativeRef) + return false + } + + v := atomic.AddInt64(&r.refCount, -speculativeRef+1) + if inodeenableLogging { + refsvfs2.LogTryIncRef(r, v) + } + return true +} + +// DecRef implements refs.RefCounter.DecRef. +// +// Note that speculative references are counted here. Since they were added +// prior to real references reaching zero, they will successfully convert to +// real references. In other words, we see speculative references only in the +// following case: +// +// A: TryIncRef [speculative increase => sees non-negative references] +// B: DecRef [real decrease] +// A: TryIncRef [transform speculative to real] +// +//go:nosplit +func (r *inodeRefs) DecRef(destroy func()) { + v := atomic.AddInt64(&r.refCount, -1) + if inodeenableLogging { + refsvfs2.LogDecRef(r, v+1) + } + switch { + case v < 0: + panic(fmt.Sprintf("Decrementing non-positive ref count %p, owned by %s", r, r.RefType())) + + case v == 0: + refsvfs2.Unregister(r) + + if destroy != nil { + destroy() + } + } +} + +func (r *inodeRefs) afterLoad() { + if r.ReadRefs() > 0 { + r.EnableLeakCheck() + } +} diff --git a/pkg/sentry/fsimpl/fuse/request_list.go b/pkg/sentry/fsimpl/fuse/request_list.go new file mode 100644 index 000000000..002262f23 --- /dev/null +++ b/pkg/sentry/fsimpl/fuse/request_list.go @@ -0,0 +1,193 @@ +package fuse + +// ElementMapper provides an identity mapping by default. +// +// This can be replaced to provide a struct that maps elements to linker +// objects, if they are not the same. An ElementMapper is not typically +// required if: Linker is left as is, Element is left as is, or Linker and +// Element are the same type. +type requestElementMapper struct{} + +// linkerFor maps an Element to a Linker. +// +// This default implementation should be inlined. +// +//go:nosplit +func (requestElementMapper) linkerFor(elem *Request) *Request { return elem } + +// List is an intrusive list. Entries can be added to or removed from the list +// in O(1) time and with no additional memory allocations. +// +// The zero value for List is an empty list ready to use. +// +// To iterate over a list (where l is a List): +// for e := l.Front(); e != nil; e = e.Next() { +// // do something with e. +// } +// +// +stateify savable +type requestList struct { + head *Request + tail *Request +} + +// Reset resets list l to the empty state. +func (l *requestList) Reset() { + l.head = nil + l.tail = nil +} + +// Empty returns true iff the list is empty. +func (l *requestList) Empty() bool { + return l.head == nil +} + +// Front returns the first element of list l or nil. +func (l *requestList) Front() *Request { + return l.head +} + +// Back returns the last element of list l or nil. +func (l *requestList) Back() *Request { + return l.tail +} + +// Len returns the number of elements in the list. +// +// NOTE: This is an O(n) operation. +func (l *requestList) Len() (count int) { + for e := l.Front(); e != nil; e = (requestElementMapper{}.linkerFor(e)).Next() { + count++ + } + return count +} + +// PushFront inserts the element e at the front of list l. +func (l *requestList) PushFront(e *Request) { + linker := requestElementMapper{}.linkerFor(e) + linker.SetNext(l.head) + linker.SetPrev(nil) + if l.head != nil { + requestElementMapper{}.linkerFor(l.head).SetPrev(e) + } else { + l.tail = e + } + + l.head = e +} + +// PushBack inserts the element e at the back of list l. +func (l *requestList) PushBack(e *Request) { + linker := requestElementMapper{}.linkerFor(e) + linker.SetNext(nil) + linker.SetPrev(l.tail) + if l.tail != nil { + requestElementMapper{}.linkerFor(l.tail).SetNext(e) + } else { + l.head = e + } + + l.tail = e +} + +// PushBackList inserts list m at the end of list l, emptying m. +func (l *requestList) PushBackList(m *requestList) { + if l.head == nil { + l.head = m.head + l.tail = m.tail + } else if m.head != nil { + requestElementMapper{}.linkerFor(l.tail).SetNext(m.head) + requestElementMapper{}.linkerFor(m.head).SetPrev(l.tail) + + l.tail = m.tail + } + m.head = nil + m.tail = nil +} + +// InsertAfter inserts e after b. +func (l *requestList) InsertAfter(b, e *Request) { + bLinker := requestElementMapper{}.linkerFor(b) + eLinker := requestElementMapper{}.linkerFor(e) + + a := bLinker.Next() + + eLinker.SetNext(a) + eLinker.SetPrev(b) + bLinker.SetNext(e) + + if a != nil { + requestElementMapper{}.linkerFor(a).SetPrev(e) + } else { + l.tail = e + } +} + +// InsertBefore inserts e before a. +func (l *requestList) InsertBefore(a, e *Request) { + aLinker := requestElementMapper{}.linkerFor(a) + eLinker := requestElementMapper{}.linkerFor(e) + + b := aLinker.Prev() + eLinker.SetNext(a) + eLinker.SetPrev(b) + aLinker.SetPrev(e) + + if b != nil { + requestElementMapper{}.linkerFor(b).SetNext(e) + } else { + l.head = e + } +} + +// Remove removes e from l. +func (l *requestList) Remove(e *Request) { + linker := requestElementMapper{}.linkerFor(e) + prev := linker.Prev() + next := linker.Next() + + if prev != nil { + requestElementMapper{}.linkerFor(prev).SetNext(next) + } else if l.head == e { + l.head = next + } + + if next != nil { + requestElementMapper{}.linkerFor(next).SetPrev(prev) + } else if l.tail == e { + l.tail = prev + } + + linker.SetNext(nil) + linker.SetPrev(nil) +} + +// Entry is a default implementation of Linker. Users can add anonymous fields +// of this type to their structs to make them automatically implement the +// methods needed by List. +// +// +stateify savable +type requestEntry struct { + next *Request + prev *Request +} + +// Next returns the entry that follows e in the list. +func (e *requestEntry) Next() *Request { + return e.next +} + +// Prev returns the entry that precedes e in the list. +func (e *requestEntry) Prev() *Request { + return e.prev +} + +// SetNext assigns 'entry' as the entry that follows e in the list. +func (e *requestEntry) SetNext(elem *Request) { + e.next = elem +} + +// SetPrev assigns 'entry' as the entry that precedes e in the list. +func (e *requestEntry) SetPrev(elem *Request) { + e.prev = elem +} diff --git a/pkg/sentry/fsimpl/fuse/utils_test.go b/pkg/sentry/fsimpl/fuse/utils_test.go deleted file mode 100644 index 2c0cc0f4e..000000000 --- a/pkg/sentry/fsimpl/fuse/utils_test.go +++ /dev/null @@ -1,126 +0,0 @@ -// 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 ( - "io" - "testing" - - "gvisor.dev/gvisor/pkg/abi/linux" - "gvisor.dev/gvisor/pkg/marshal" - "gvisor.dev/gvisor/pkg/sentry/fsimpl/testutil" - "gvisor.dev/gvisor/pkg/sentry/kernel" - "gvisor.dev/gvisor/pkg/sentry/kernel/auth" - "gvisor.dev/gvisor/pkg/sentry/vfs" - "gvisor.dev/gvisor/pkg/usermem" -) - -func setup(t *testing.T) *testutil.System { - k, err := testutil.Boot() - if err != nil { - t.Fatalf("Error creating kernel: %v", err) - } - - ctx := k.SupervisorContext() - creds := auth.CredentialsFromContext(ctx) - - k.VFS().MustRegisterFilesystemType(Name, &FilesystemType{}, &vfs.RegisterFilesystemTypeOptions{ - AllowUserList: true, - AllowUserMount: true, - }) - - mntns, err := k.VFS().NewMountNamespace(ctx, creds, "", "tmpfs", &vfs.MountOptions{}) - if err != nil { - t.Fatalf("NewMountNamespace(): %v", err) - } - - return testutil.NewSystem(ctx, t, k.VFS(), mntns) -} - -// newTestConnection creates a fuse connection that the sentry can communicate with -// and the FD for the server to communicate with. -func newTestConnection(system *testutil.System, k *kernel.Kernel, maxActiveRequests uint64) (*connection, *vfs.FileDescription, error) { - fuseDev := &DeviceFD{} - - vd := system.VFS.NewAnonVirtualDentry("fuse") - defer vd.DecRef(system.Ctx) - if err := fuseDev.vfsfd.Init(fuseDev, linux.O_RDWR, vd.Mount(), vd.Dentry(), &vfs.FileDescriptionOptions{}); err != nil { - return nil, nil, err - } - - fsopts := filesystemOptions{ - maxActiveRequests: maxActiveRequests, - } - fs, err := newFUSEFilesystem(system.Ctx, system.VFS, &FilesystemType{}, fuseDev, 0, &fsopts) - if err != nil { - return nil, nil, err - } - return fs.conn, &fuseDev.vfsfd, nil -} - -type testPayload struct { - marshal.StubMarshallable - data uint32 -} - -// SizeBytes implements marshal.Marshallable.SizeBytes. -func (t *testPayload) SizeBytes() int { - return 4 -} - -// MarshalBytes implements marshal.Marshallable.MarshalBytes. -func (t *testPayload) MarshalBytes(dst []byte) { - usermem.ByteOrder.PutUint32(dst[:4], t.data) -} - -// UnmarshalBytes implements marshal.Marshallable.UnmarshalBytes. -func (t *testPayload) UnmarshalBytes(src []byte) { - *t = testPayload{data: usermem.ByteOrder.Uint32(src[:4])} -} - -// Packed implements marshal.Marshallable.Packed. -func (t *testPayload) Packed() bool { - return true -} - -// MarshalUnsafe implements marshal.Marshallable.MarshalUnsafe. -func (t *testPayload) MarshalUnsafe(dst []byte) { - t.MarshalBytes(dst) -} - -// UnmarshalUnsafe implements marshal.Marshallable.UnmarshalUnsafe. -func (t *testPayload) UnmarshalUnsafe(src []byte) { - t.UnmarshalBytes(src) -} - -// CopyOutN implements marshal.Marshallable.CopyOutN. -func (t *testPayload) CopyOutN(task marshal.CopyContext, addr usermem.Addr, limit int) (int, error) { - panic("not implemented") -} - -// CopyOut implements marshal.Marshallable.CopyOut. -func (t *testPayload) CopyOut(task marshal.CopyContext, addr usermem.Addr) (int, error) { - panic("not implemented") -} - -// CopyIn implements marshal.Marshallable.CopyIn. -func (t *testPayload) CopyIn(task marshal.CopyContext, addr usermem.Addr) (int, error) { - panic("not implemented") -} - -// WriteTo implements io.WriterTo.WriteTo. -func (t *testPayload) WriteTo(w io.Writer) (int64, error) { - panic("not implemented") -} |