From c0261bcb12bb527c5dbef9e469e3085285a5bdd3 Mon Sep 17 00:00:00 2001 From: SaladDay <1203511142@qq.com> Date: Wed, 30 Sep 2026 22:07:20 +0000 Subject: [PATCH] Add sandboxwire framing and primitives for the sandbox protocols The File, Process and Link protocols share one 16-byte frame header, a primitive encoder and decoder that validates lengths and counts before allocating, tag classification, and the ID and Effect values more than one protocol uses. Each protocol's protocol.go owns its own messages. --- internal/sandboxwire/codec.go | 179 ++++++++++++++++ internal/sandboxwire/frame.go | 134 ++++++++++++ internal/sandboxwire/sandboxwire_test.go | 233 +++++++++++++++++++++ internal/sandboxwire/testdata/frame_v1.hex | 18 ++ internal/sandboxwire/vocabulary.go | 35 ++++ 5 files changed, 599 insertions(+) create mode 100644 internal/sandboxwire/codec.go create mode 100644 internal/sandboxwire/frame.go create mode 100644 internal/sandboxwire/sandboxwire_test.go create mode 100644 internal/sandboxwire/testdata/frame_v1.hex create mode 100644 internal/sandboxwire/vocabulary.go diff --git a/internal/sandboxwire/codec.go b/internal/sandboxwire/codec.go new file mode 100644 index 000000000..da12a9dff --- /dev/null +++ b/internal/sandboxwire/codec.go @@ -0,0 +1,179 @@ +package sandboxwire + +import ( + "encoding/binary" + "fmt" +) + +// Encoder appends primitives to a payload. The zero value is ready to use. +type Encoder struct { + b []byte +} + +// Payload returns the encoded bytes. +func (e *Encoder) Payload() []byte { return e.b } + +func (e *Encoder) U8(v uint8) { e.b = append(e.b, v) } +func (e *Encoder) U16(v uint16) { e.b = binary.BigEndian.AppendUint16(e.b, v) } +func (e *Encoder) U32(v uint32) { e.b = binary.BigEndian.AppendUint32(e.b, v) } +func (e *Encoder) U64(v uint64) { e.b = binary.BigEndian.AppendUint64(e.b, v) } +func (e *Encoder) I64(v int64) { e.U64(uint64(v)) } + +// Bool writes 1 for true and 0 for false. +func (e *Encoder) Bool(v bool) { + if v { + e.U8(1) + } else { + e.U8(0) + } +} + +// Bytes writes a uint32 length followed by v. +func (e *Encoder) Bytes(v []byte) { + e.U32(uint32(len(v))) + e.b = append(e.b, v...) +} + +// Count writes the uint32 element count of an array. +func (e *Encoder) Count(n int) { e.U32(uint32(n)) } + +// Present writes the presence byte of an optional field; the value follows only +// when ok. +func (e *Encoder) Present(ok bool) { e.Bool(ok) } + +// Enum writes a uint16 enum value. Zero is invalid on the wire. +func (e *Encoder) Enum(v uint16) { e.U16(v) } + +func (e *Encoder) ID(id ID) { e.b = append(e.b, id[:]...) } + +func (e *Encoder) Effect(v Effect) { e.Enum(uint16(v)) } + +// Decoder reads primitives from a payload. Every error wraps ErrMalformed; +// after an error the Decoder's position is unspecified. +type Decoder struct { + b []byte +} + +func NewDecoder(payload []byte) *Decoder { return &Decoder{b: payload} } + +// Remaining returns the number of unread bytes. +func (d *Decoder) Remaining() int { return len(d.b) } + +// Finish fails when bytes remain unread. +func (d *Decoder) Finish() error { + if len(d.b) != 0 { + return fmt.Errorf("%w: %d trailing bytes", ErrMalformed, len(d.b)) + } + return nil +} + +func (d *Decoder) take(n uint64, what string) ([]byte, error) { + if n > uint64(len(d.b)) { + return nil, fmt.Errorf("%w: %s needs %d bytes, %d remain", ErrMalformed, what, n, len(d.b)) + } + v := d.b[:n:n] + d.b = d.b[n:] + return v, nil +} + +func (d *Decoder) U8() (uint8, error) { + b, err := d.take(1, "u8") + if err != nil { + return 0, err + } + return b[0], nil +} + +func (d *Decoder) U16() (uint16, error) { + b, err := d.take(2, "u16") + if err != nil { + return 0, err + } + return binary.BigEndian.Uint16(b), nil +} + +func (d *Decoder) U32() (uint32, error) { + b, err := d.take(4, "u32") + if err != nil { + return 0, err + } + return binary.BigEndian.Uint32(b), nil +} + +func (d *Decoder) U64() (uint64, error) { + b, err := d.take(8, "u64") + if err != nil { + return 0, err + } + return binary.BigEndian.Uint64(b), nil +} + +func (d *Decoder) I64() (int64, error) { + v, err := d.U64() + return int64(v), err +} + +// Bool accepts exactly 0 or 1. +func (d *Decoder) Bool() (bool, error) { + v, err := d.U8() + if err == nil && v > 1 { + err = fmt.Errorf("%w: boolean byte %d", ErrMalformed, v) + } + return v == 1, err +} + +// Bytes reads a uint32 length and that many bytes. The result aliases the +// payload, with its capacity clipped to its length. +func (d *Decoder) Bytes() ([]byte, error) { + n, err := d.U32() + if err != nil { + return nil, err + } + return d.take(uint64(n), "byte string") +} + +// Count reads an array's uint32 element count. The count is at most max and at +// most the remaining bytes, because every element encodes to at least one byte, +// so a caller may allocate count elements. +func (d *Decoder) Count(max uint32) (int, error) { + n, err := d.U32() + switch { + case err != nil: + return 0, err + case n > max: + return 0, fmt.Errorf("%w: count %d exceeds maximum %d", ErrMalformed, n, max) + case uint64(n) > uint64(len(d.b)): + return 0, fmt.Errorf("%w: count %d exceeds %d remaining bytes", ErrMalformed, n, len(d.b)) + } + return int(n), nil +} + +// Present reads the presence byte of an optional field. +func (d *Decoder) Present() (bool, error) { return d.Bool() } + +// Enum reads a uint16 enum value, rejecting zero and values valid does not +// accept. +func (d *Decoder) Enum(valid func(uint16) bool) (uint16, error) { + v, err := d.U16() + if err == nil && (v == 0 || !valid(v)) { + err = fmt.Errorf("%w: enum value %d", ErrMalformed, v) + } + return v, err +} + +// ID reads a required identifier, rejecting the zero ID. +func (d *Decoder) ID() (ID, error) { + b, err := d.take(uint64(len(ID{})), "identifier") + if err != nil { + return ID{}, err + } + if id := ID(b); !id.IsZero() { + return id, nil + } + return ID{}, fmt.Errorf("%w: zero identifier", ErrMalformed) +} + +func (d *Decoder) Effect() (Effect, error) { + v, err := d.Enum(func(v uint16) bool { return Effect(v).Valid() }) + return Effect(v), err +} diff --git a/internal/sandboxwire/frame.go b/internal/sandboxwire/frame.go new file mode 100644 index 000000000..bd4d53737 --- /dev/null +++ b/internal/sandboxwire/frame.go @@ -0,0 +1,134 @@ +// Package sandboxwire holds the frame header and the primitive encoding shared +// by the File, Process and Link protocols, plus the few values more than one of +// them uses. +// +// It defines no messages. Each protocol's protocol.go owns its message tags, +// ordered payload layouts, validators and encoding. On the wire an enum is a +// uint16 whose zero value is invalid, and an optional field is a presence byte +// followed by the value. +package sandboxwire + +import ( + "encoding/binary" + "errors" + "fmt" + "io" +) + +const ( + // HeaderSize is the length of the frame header: PayloadLength uint32, + // MessageType uint16, Flags uint16 and RequestID uint64, in network byte + // order. + HeaderSize = 16 + // MaxPayload is the hard maximum frame payload. Advertised limits may be + // smaller. + MaxPayload = 1 << 20 + // MaxChunk is the maximum file or process data chunk. + MaxChunk = 64 << 10 +) + +// ErrMalformed is the single error for bytes that violate the wire rules. +// Errors from this package and from protocol decoders wrap it. +var ErrMalformed = errors.New("sandboxwire: malformed message") + +// Frame is one decoded frame. RequestID is zero only for unsolicited events. +type Frame struct { + Type uint16 + RequestID uint64 + Payload []byte +} + +// ReadFrame reads one frame. It rejects a payload length above maxPayload +// (capped at MaxPayload) before allocating, and rejects nonzero flags. It +// returns io.EOF only when r ends before the first header byte. +func ReadFrame(r io.Reader, maxPayload uint32) (Frame, error) { + var h [HeaderSize]byte + if _, err := io.ReadFull(r, h[:]); err != nil { + return Frame{}, err + } + n, limit := binary.BigEndian.Uint32(h[0:4]), min(maxPayload, MaxPayload) + if n > limit { + return Frame{}, fmt.Errorf("%w: payload length %d exceeds limit %d", ErrMalformed, n, limit) + } + if flags := binary.BigEndian.Uint16(h[6:8]); flags != 0 { + return Frame{}, fmt.Errorf("%w: frame flags %#04x", ErrMalformed, flags) + } + f := Frame{ + Type: binary.BigEndian.Uint16(h[4:6]), + RequestID: binary.BigEndian.Uint64(h[8:16]), + Payload: make([]byte, n), + } + if _, err := io.ReadFull(r, f.Payload); err != nil { + if err == io.EOF { + err = io.ErrUnexpectedEOF + } + return Frame{}, err + } + return f, nil +} + +// WriteFrame writes f with a single Write call, so frames written by goroutines +// sharing w under a mutex never interleave. +func WriteFrame(w io.Writer, f Frame) error { + if len(f.Payload) > MaxPayload { + return fmt.Errorf("%w: payload length %d exceeds limit %d", ErrMalformed, len(f.Payload), MaxPayload) + } + b := make([]byte, HeaderSize+len(f.Payload)) + binary.BigEndian.PutUint32(b[0:4], uint32(len(f.Payload))) + binary.BigEndian.PutUint16(b[4:6], f.Type) + binary.BigEndian.PutUint64(b[8:16], f.RequestID) + copy(b[HeaderSize:], f.Payload) + _, err := w.Write(b) + return err +} + +const responseBit = 0x8000 + +// FirstEvent is the first event tag. A protocol numbers its events upward from +// it in the order it lists them. +const FirstEvent = 0x4001 + +// ResponseType returns the tag of the response to request tag req. +func ResponseType(req uint16) uint16 { return req | responseBit } + +// IsResponse reports whether t is a response tag. +func IsResponse(t uint16) bool { return t&responseBit != 0 } + +// ValidRequestID reports whether id may identify a request. Zero is reserved +// for unsolicited events. +func ValidRequestID(id uint64) bool { return id != 0 } + +// Kind is the role of a message type. +type Kind uint8 + +const ( + KindRequest Kind = iota + 1 + KindResponse + KindEvent +) + +// Tags is a protocol's known message types: requests 1 through Requests, their +// responses, and events FirstEvent through FirstEvent+Events-1. Requests stays +// below FirstEvent. +type Tags struct { + Requests uint16 + Events uint16 +} + +// Classify returns the kind of message type t, or ErrMalformed when t is not +// one of the known tags. +func (k Tags) Classify(t uint16) (Kind, error) { + switch { + case IsResponse(t): + if req := t &^ responseBit; req >= 1 && req <= k.Requests { + return KindResponse, nil + } + case t >= FirstEvent: + if t-FirstEvent < k.Events { + return KindEvent, nil + } + case t >= 1 && t <= k.Requests: + return KindRequest, nil + } + return 0, fmt.Errorf("%w: unknown message type %#04x", ErrMalformed, t) +} diff --git a/internal/sandboxwire/sandboxwire_test.go b/internal/sandboxwire/sandboxwire_test.go new file mode 100644 index 000000000..4a09b39e9 --- /dev/null +++ b/internal/sandboxwire/sandboxwire_test.go @@ -0,0 +1,233 @@ +package sandboxwire + +import ( + "bytes" + "encoding/binary" + "encoding/hex" + "errors" + "io" + "os" + "reflect" + "strings" + "testing" +) + +// sample exercises every primitive in the order the golden fixture lists them. +type sample struct { + A uint8 + B uint16 + C uint32 + D uint64 + E int64 + F bool + Kind uint16 + Effect Effect + Ref *ID + Items [][]byte +} + +const sampleMaxItems = 8 + +func sampleKind(v uint16) bool { return v <= 3 } + +func (s sample) encode(e *Encoder) { + e.U8(s.A) + e.U16(s.B) + e.U32(s.C) + e.U64(s.D) + e.I64(s.E) + e.Bool(s.F) + e.Enum(s.Kind) + e.Effect(s.Effect) + e.Present(s.Ref != nil) + if s.Ref != nil { + e.ID(*s.Ref) + } + e.Count(len(s.Items)) + for _, item := range s.Items { + e.Bytes(item) + } +} + +// get runs read unless an earlier read failed, keeping the first error. +func get[T any](err *error, read func() (T, error)) T { + var v T + if *err == nil { + v, *err = read() + } + return v +} + +func decodeSample(t *testing.T, p []byte) (sample, error) { + var s sample + var err error + d := NewDecoder(p) + s.A = get(&err, d.U8) + s.B = get(&err, d.U16) + s.C = get(&err, d.U32) + s.D = get(&err, d.U64) + s.E = get(&err, d.I64) + s.F = get(&err, d.Bool) + s.Kind = get(&err, func() (uint16, error) { return d.Enum(sampleKind) }) + s.Effect = get(&err, d.Effect) + if get(&err, d.Present) { + id := get(&err, d.ID) + s.Ref = &id + } + remaining := d.Remaining() + n := get(&err, func() (int, error) { return d.Count(sampleMaxItems) }) + if n > sampleMaxItems || n > remaining { + t.Fatalf("count %d escapes its bounds (max %d, %d bytes remained)", n, sampleMaxItems, remaining) + } + s.Items = make([][]byte, n) + for i := range s.Items { + s.Items[i] = get(&err, d.Bytes) + if cap(s.Items[i]) != len(s.Items[i]) { + t.Fatalf("byte string capacity %d exceeds its length %d", cap(s.Items[i]), len(s.Items[i])) + } + } + if err != nil { + return s, err + } + return s, d.Finish() +} + +func readHexFixture(t testing.TB, name string) []byte { + t.Helper() + raw, err := os.ReadFile(name) + if err != nil { + t.Fatal(err) + } + var digits strings.Builder + for _, line := range strings.Split(string(raw), "\n") { + line, _, _ = strings.Cut(line, "#") + digits.WriteString(strings.Join(strings.Fields(line), "")) + } + b, err := hex.DecodeString(digits.String()) + if err != nil { + t.Fatal(err) + } + return b +} + +type countingWriter struct { + bytes.Buffer + writes int +} + +func (w *countingWriter) Write(p []byte) (int, error) { + w.writes++ + return w.Buffer.Write(p) +} + +func TestGoldenFrame(t *testing.T) { + want := readHexFixture(t, "testdata/frame_v1.hex") + ref := ID{0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f} + msg := sample{A: 7, B: 0x0102, C: 0x03040506, D: 0x0708090a0b0c0d0e, E: -2, F: true, Kind: 2, Effect: EffectPossible, Ref: &ref, Items: [][]byte{[]byte("ab"), {}}} + + var e Encoder + msg.encode(&e) + var w countingWriter + if err := WriteFrame(&w, Frame{Type: ResponseType(3), RequestID: 0x0102030405060708, Payload: e.Payload()}); err != nil { + t.Fatal(err) + } + if !bytes.Equal(w.Bytes(), want) || w.writes != 1 { + t.Fatalf("encoded frame in %d writes:\n got %x\nwant %x", w.writes, w.Bytes(), want) + } + + f, err := ReadFrame(bytes.NewReader(want), MaxPayload) + if err != nil { + t.Fatal(err) + } + got, err := decodeSample(t, f.Payload) + if err != nil { + t.Fatal(err) + } + if f.Type != 0x8003 || f.RequestID != 0x0102030405060708 || !reflect.DeepEqual(got, msg) { + t.Fatalf("decoded %#x %#x %+v, want %+v", f.Type, f.RequestID, got, msg) + } +} + +func header(length uint32, flags uint16) []byte { + h := binary.BigEndian.AppendUint32(nil, length) + h = binary.BigEndian.AppendUint16(h, 1) + h = binary.BigEndian.AppendUint16(h, flags) + return binary.BigEndian.AppendUint64(h, 1) +} + +func errOf[T any](_ T, err error) error { return err } + +func TestRejectsMalformed(t *testing.T) { + anyEnum := func(uint16) bool { return true } + tags := Tags{Requests: 2, Events: 1} + for _, tc := range []struct { + name string + err error + }{ + // Each reader holds only a header, so these fail before any payload read. + {"length above MaxPayload", errOf(ReadFrame(bytes.NewReader(header(MaxPayload+1, 0)), ^uint32(0)))}, + {"length above advertised limit", errOf(ReadFrame(bytes.NewReader(header(65, 0)), 64))}, + {"nonzero flags", errOf(ReadFrame(bytes.NewReader(header(0, 1)), MaxPayload))}, + {"oversize write", WriteFrame(io.Discard, Frame{Type: 1, RequestID: 1, Payload: make([]byte, MaxPayload+1)})}, + {"trailing bytes", NewDecoder([]byte{0}).Finish()}, + {"bool 2", errOf(NewDecoder([]byte{2}).Bool())}, + {"zero enum", errOf(NewDecoder([]byte{0, 0}).Enum(anyEnum))}, + {"unknown enum", errOf(NewDecoder([]byte{0, 4}).Enum(sampleKind))}, + {"zero ID", errOf(NewDecoder(make([]byte, 16)).ID())}, + {"count above remaining bytes", errOf(NewDecoder([]byte{0, 0, 0, 2, 0}).Count(10))}, + {"count above maximum", errOf(NewDecoder([]byte{0, 0, 0, 2, 0, 0}).Count(1))}, + {"byte string above remaining bytes", errOf(NewDecoder([]byte{0, 0, 0, 2, 0}).Bytes())}, + {"truncated integer", errOf(NewDecoder([]byte{0, 0, 0}).U32())}, + {"unknown request", errOf(tags.Classify(3))}, + {"unknown response", errOf(tags.Classify(ResponseType(3)))}, + {"unknown event", errOf(tags.Classify(FirstEvent + 1))}, + {"type zero", errOf(tags.Classify(0))}, + } { + if !errors.Is(tc.err, ErrMalformed) { + t.Errorf("%s: got %v, want ErrMalformed", tc.name, tc.err) + } + } +} + +func TestClassify(t *testing.T) { + tags := Tags{Requests: 2, Events: 1} + for typ, want := range map[uint16]Kind{2: KindRequest, ResponseType(2): KindResponse, FirstEvent: KindEvent} { + if got, err := tags.Classify(typ); got != want || err != nil { + t.Errorf("Classify(%#x) = %v, %v; want %v", typ, got, err, want) + } + } +} + +// FuzzDecoder decodes a frame carrying the sample payload. Whatever decodes +// must re-encode to the same bytes, every failure must be ErrMalformed, and no +// allocation may exceed the input: the payload limit is the input length and +// the item count is bounded. +func FuzzDecoder(f *testing.F) { + f.Add(readHexFixture(f, "testdata/frame_v1.hex")) + f.Add([]byte{}) + f.Fuzz(func(t *testing.T, data []byte) { + fr, err := ReadFrame(bytes.NewReader(data), uint32(len(data))) + if err != nil { + if !errors.Is(err, ErrMalformed) && err != io.EOF && err != io.ErrUnexpectedEOF { + t.Fatalf("frame error %v", err) + } + return + } + msg, err := decodeSample(t, fr.Payload) + if err != nil { + if !errors.Is(err, ErrMalformed) { + t.Fatalf("payload error %v is not ErrMalformed", err) + } + return + } + var e Encoder + msg.encode(&e) + var w bytes.Buffer + if err := WriteFrame(&w, Frame{Type: fr.Type, RequestID: fr.RequestID, Payload: e.Payload()}); err != nil { + t.Fatal(err) + } + if !bytes.HasPrefix(data, w.Bytes()) { + t.Fatalf("round trip changed the frame:\n got %x\nwant prefix of %x", w.Bytes(), data) + } + }) +} diff --git a/internal/sandboxwire/testdata/frame_v1.hex b/internal/sandboxwire/testdata/frame_v1.hex new file mode 100644 index 000000000..14921566f --- /dev/null +++ b/internal/sandboxwire/testdata/frame_v1.hex @@ -0,0 +1,18 @@ +# One frame carrying the sample payload from sandboxwire_test.go. Hex bytes; text after # is a comment. +0000003b # PayloadLength 59 +8003 # MessageType: response to request 3 +0000 # Flags +0102030405060708 # RequestID +07 # U8 7 +0102 # U16 0x0102 +03040506 # U32 0x03040506 +0708090a0b0c0d0e # U64 0x0708090a0b0c0d0e +fffffffffffffffe # I64 -2 +01 # Bool true +0002 # Enum 2 +0002 # Effect EffectPossible +01 # Present +101112131415161718191a1b1c1d1e1f # ID +00000002 # Count 2 +00000002 6162 # Bytes "ab" +00000000 # Bytes "" diff --git a/internal/sandboxwire/vocabulary.go b/internal/sandboxwire/vocabulary.go new file mode 100644 index 000000000..364ebd0a0 --- /dev/null +++ b/internal/sandboxwire/vocabulary.go @@ -0,0 +1,35 @@ +package sandboxwire + +import ( + "crypto/rand" + "encoding/hex" +) + +// ID is an opaque 16-byte identifier, such as an OperationID, AttachmentID or +// ServerInstanceID. The zero ID is invalid wherever an ID is required. +type ID [16]byte + +// NewID returns a random nonzero ID. +func NewID() ID { + var id ID + for id.IsZero() { + rand.Read(id[:]) + } + return id +} + +func (id ID) IsZero() bool { return id == ID{} } + +func (id ID) String() string { return hex.EncodeToString(id[:]) } + +// Effect says whether a failed request may have taken effect. Transport loss +// after dispatch is EffectPossible unless the server later establishes the +// result. +type Effect uint16 + +const ( + EffectNone Effect = 1 + EffectPossible Effect = 2 +) + +func (e Effect) Valid() bool { return e == EffectNone || e == EffectPossible }