From f4da072c765d45c6a841ca03c1511b2b4d6e4d37 Mon Sep 17 00:00:00 2001 From: Xiaodong Wang Date: Tue, 18 Aug 2026 22:11:17 +0000 Subject: [PATCH] Return a typed error with raw bytes for malformed messages When convertInMessage fails, ReadOp previously returned a plain fmt.Errorf whose text discarded the offending message. That makes kernel/protocol corruption hard to diagnose in production, since the raw bytes are often the only useful signal. Introduce MalformedMessageError, which carries the underlying conversion error (exposed via Unwrap) plus a copy of the raw message bytes, and return it from ReadOp. A new InMessage.Bytes() accessor exposes the message read by the most recent Init so ReadOp can copy the bytes before the buffer is recycled. The error string is unchanged ("convertInMessage: "), so existing log output is preserved; callers that want the bytes can recover them with errors.As. Co-authored-by: Isaac --- connection.go | 25 ++++++++++++++++- connection_test.go | 27 ++++++++++++++++++ internal/buffer/in_message.go | 5 ++++ internal/buffer/in_message_test.go | 44 ++++++++++++++++++++++++++++++ 4 files changed, 100 insertions(+), 1 deletion(-) create mode 100644 internal/buffer/in_message_test.go diff --git a/connection.go b/connection.go index 429a3b7..83d33c6 100644 --- a/connection.go +++ b/connection.go @@ -94,6 +94,22 @@ type opState struct { wlog *WireLogRecord } +// MalformedMessageError reports a FUSE message that could not be converted to +// an operation. Message holds a copy of the raw bytes of the offending +// message, which callers can log to diagnose the corruption. +type MalformedMessageError struct { + Err error + Message []byte +} + +func (e *MalformedMessageError) Error() string { + return fmt.Sprintf("convertInMessage: %v", e.Err) +} + +func (e *MalformedMessageError) Unwrap() error { + return e.Err +} + // Return the current wirelog record from the context if the MountConfig // contained a non-nil wireLogger, nil otherwise. func GetWirelog(ctx context.Context) *WireLogRecord { @@ -477,8 +493,15 @@ func (c *Connection) ReadOp() (_ context.Context, op interface{}, _ error) { outMsg := c.getOutMessage() op, err = convertInMessage(&c.cfg, inMsg, outMsg, c.protocol) if err != nil { + // Copy the raw message before recycling the buffer so that the + // caller can log the bytes that failed to convert. + message := append([]byte(nil), inMsg.Bytes()...) + c.putInMessage(inMsg) c.putOutMessage(outMsg) - return nil, nil, fmt.Errorf("convertInMessage: %v", err) + return nil, nil, &MalformedMessageError{ + Err: err, + Message: message, + } } // Choose an ID for this operation for the purposes of logging, and log it. diff --git a/connection_test.go b/connection_test.go index c1a4f01..8342a6b 100644 --- a/connection_test.go +++ b/connection_test.go @@ -15,12 +15,39 @@ package fuse import ( + "bytes" + "errors" + "fmt" "strings" "testing" "github.com/jacobsa/fuse/internal/buffer" ) +func TestMalformedMessageError(t *testing.T) { + inner := errors.New("Corrupt OpLookup") + msg := []byte{0x00, 0x01, 0x02, 0xff} + err := &MalformedMessageError{Err: inner, Message: msg} + + if got, want := err.Error(), "convertInMessage: Corrupt OpLookup"; got != want { + t.Errorf("Error() = %q, want %q", got, want) + } + + // The error unwraps to the underlying conversion error and is recoverable + // via errors.As even when wrapped. + if !errors.Is(err, inner) { + t.Errorf("errors.Is(err, inner) = false, want true") + } + + var target *MalformedMessageError + if !errors.As(fmt.Errorf("read op: %w", err), &target) { + t.Fatalf("errors.As failed to recover *MalformedMessageError") + } + if !bytes.Equal(target.Message, msg) { + t.Errorf("Message = %v, want %v", target.Message, msg) + } +} + func TestSanitizeMaxPagesAndWrite(t *testing.T) { pageSize := uint32(buffer.GetPageSize()) defaultMaxWrite := uint32(buffer.MaxWriteSize) diff --git a/internal/buffer/in_message.go b/internal/buffer/in_message.go index 60eb763..5c299ec 100644 --- a/internal/buffer/in_message.go +++ b/internal/buffer/in_message.go @@ -115,6 +115,11 @@ func (m *InMessage) Init(r io.Reader) error { return nil } +// Bytes returns the complete message read by the most recent call to Init. +func (m *InMessage) Bytes() []byte { + return m.storage[:m.size] +} + // Return a reference to the header read in the most recent call to Init. func (m *InMessage) Header() *fusekernel.InHeader { return (*fusekernel.InHeader)(unsafe.Pointer(&m.storage[0])) diff --git a/internal/buffer/in_message_test.go b/internal/buffer/in_message_test.go new file mode 100644 index 0000000..cd50a60 --- /dev/null +++ b/internal/buffer/in_message_test.go @@ -0,0 +1,44 @@ +// Copyright 2026 Google Inc. All Rights Reserved. +// +// 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 buffer + +import ( + "bytes" + "encoding/binary" + "testing" + + "github.com/jacobsa/fuse/internal/fusekernel" +) + +func TestInMessageBytes(t *testing.T) { + // Construct a well-formed message: a header whose Len field matches the + // total size, followed by a small payload. + const payloadLen = 8 + total := fusekernel.InHeaderSize + payloadLen + raw := make([]byte, total) + binary.LittleEndian.PutUint32(raw[0:4], uint32(total)) + for i := fusekernel.InHeaderSize; i < total; i++ { + raw[i] = byte(i) + } + + m := NewInMessage(GetPageSize() + MaxWriteSize) + if err := m.Init(bytes.NewReader(raw)); err != nil { + t.Fatalf("Init: %v", err) + } + + if got := m.Bytes(); !bytes.Equal(got, raw) { + t.Errorf("Bytes() = %v, want %v", got, raw) + } +}