v0.7.0 4c16dd8

scan.go RawHistoryFolder

1// Strict protowire inspection for generated envelope payloads.
2//
3// scanEnvelope runs before generated unmarshal: it enforces maximum depth,
4// field count, repeated-element counts and lengths, recursively rejects
5// unknown fields and wire types, and requires exactly one top-level
6// envelope variant occurrence. Repeated occurrences of singular fields and
7// concatenated envelope payloads are rejected; declared repeated fields
8// remain legal. Generated unmarshal and semantic validation run after.
9package wire
10
11import (
12	"errors"
13	"fmt"
14	"math"
15	"sync"
16
17	"google.golang.org/protobuf/encoding/protowire"
18	"google.golang.org/protobuf/proto"
19	"google.golang.org/protobuf/reflect/protoreflect"
20)
21
22// Scanner bounds. They cap decode work before any allocation a hostile
23// envelope could inflate; semantic validators apply tighter per-message
24// limits afterwards.
25const (
26	maxScanDepth       = 16
27	maxScanFields      = 65536
28	maxScanRepeated    = 8192
29	maxScanBytesField  = AbsoluteEnvelopeLimit
30	maxScanStringField = 4 << 20
31)
32
33var (
34	ErrScanDepth       = errors.New("wire: envelope exceeds maximum nesting depth")
35	ErrScanFields      = errors.New("wire: envelope exceeds maximum field count")
36	ErrScanRepeated    = errors.New("wire: envelope exceeds maximum repeated elements")
37	ErrScanLength      = errors.New("wire: envelope field exceeds maximum length")
38	ErrScanUnknown     = errors.New("wire: envelope has unknown field")
39	ErrScanWireType    = errors.New("wire: envelope has invalid wire type")
40	ErrScanDuplicate   = errors.New("wire: envelope repeats singular field")
41	ErrScanEmpty       = errors.New("wire: envelope has no payload variant")
42	ErrScanTrailing    = errors.New("wire: envelope has trailing bytes")
43	ErrScanTruncated   = errors.New("wire: envelope is truncated")
44	ErrScanNotMessage  = errors.New("wire: envelope field is not a message")
45	ErrScanVarintRange = errors.New("wire: envelope varint exceeds declared field range")
46)
47
48// scanSpec describes the expected shape of one message level: known fields
49// by number with their wire type and repetition, resolved from the
50// descriptor at scan time.
51type scanSpec struct {
52	fields map[protoreflect.FieldNumber]scanField
53	// oneofs marks every field number that belongs to a oneof: its
54	// members are exclusive, so any second occurrence is a duplicate.
55	oneofs map[protoreflect.FieldNumber]bool
56}
57
58// scanField records one known field's expectations.
59type scanField struct {
60	kind      protoreflect.Kind
61	isList    bool
62	isMessage bool
63}
64
65// specCache memoizes scan specs by descriptor: descriptors are process
66// singletons, so each message shape is reflected exactly once instead
67// of on every received envelope (the receive hot path).
68var specCache sync.Map // map[protoreflect.MessageDescriptor]scanSpec
69
70// specForMessage returns the cached scan spec for one message descriptor.
71func specForMessage(descriptor protoreflect.MessageDescriptor) scanSpec {
72	if cached, ok := specCache.Load(descriptor); ok {
73		return cached.(scanSpec)
74	}
75	spec := scanSpec{
76		fields: make(map[protoreflect.FieldNumber]scanField),
77		oneofs: make(map[protoreflect.FieldNumber]bool),
78	}
79	fields := descriptor.Fields()
80	for i := range fields.Len() {
81		field := fields.Get(i)
82		spec.fields[field.Number()] = scanField{
83			kind:      field.Kind(),
84			isList:    field.IsList(),
85			isMessage: field.Kind() == protoreflect.MessageKind || field.Kind() == protoreflect.GroupKind,
86		}
87	}
88	oneofs := descriptor.Oneofs()
89	for i := range oneofs.Len() {
90		oneof := oneofs.Get(i)
91		members := oneof.Fields()
92		for j := range members.Len() {
93			spec.oneofs[members.Get(j).Number()] = true
94		}
95	}
96	actual, _ := specCache.LoadOrStore(descriptor, spec)
97	return actual.(scanSpec)
98}
99
100// ScanEnvelope strictly inspects one serialized envelope of the given type
101// before generated unmarshal. It returns nil only when the payload is
102// well-formed, known, singular at the top level, and fully consumed.
103func ScanEnvelope(message proto.Message, payload []byte) error {
104	if message == nil {
105		return ErrScanNotMessage
106	}
107	if len(payload) == 0 {
108		return ErrScanEmpty
109	}
110	if len(payload) > AbsoluteEnvelopeLimit {
111		return ErrScanLength
112	}
113	scanner := &envelopeScanner{fields: 0}
114	consumed, err := scanner.scanMessage(message.ProtoReflect().Descriptor(), payload, 0)
115	if err != nil {
116		return err
117	}
118	if consumed != len(payload) {
119		return ErrScanTrailing
120	}
121	return nil
122}
123
124type envelopeScanner struct {
125	fields int
126}
127
128// scanMessage scans one message level, returning bytes consumed.
129func (s *envelopeScanner) scanMessage(descriptor protoreflect.MessageDescriptor, payload []byte, depth int) (int, error) {
130	if depth > maxScanDepth {
131		return 0, ErrScanDepth
132	}
133	spec := specForMessage(descriptor)
134	consumed := 0
135	variantSeen := false
136	repeated := 0
137	seen := map[protoreflect.FieldNumber]bool{}
138	for len(payload) > 0 {
139		number, wireType, length := protowire.ConsumeTag(payload)
140		if length < 0 {
141			return 0, ErrScanTruncated
142		}
143		field, ok := spec.fields[number]
144		if !ok {
145			return 0, fmt.Errorf("%w: field %d", ErrScanUnknown, number)
146		}
147		if err := checkWireType(field, wireType); err != nil {
148			return 0, err
149		}
150		s.fields++
151		if s.fields > maxScanFields {
152			return 0, ErrScanFields
153		}
154		if spec.oneofs[number] {
155			// Oneof members are exclusive: any second occurrence —
156			// same variant or another — is a duplicate, and the
157			// top level requires exactly one variant.
158			if variantSeen {
159				return 0, fmt.Errorf("%w: oneof field %d", ErrScanDuplicate, number)
160			}
161			variantSeen = true
162		} else if !field.isList {
163			if seen[number] {
164				return 0, fmt.Errorf("%w: field %d", ErrScanDuplicate, number)
165			}
166			seen[number] = true
167		} else {
168			repeated++
169			if repeated > maxScanRepeated {
170				return 0, ErrScanRepeated
171			}
172		}
173		fieldDescriptor := descriptor.Fields().ByNumber(number)
174		n, err := s.scanField(fieldDescriptor, field, wireType, payload[length:], depth)
175		if err != nil {
176			return 0, err
177		}
178		consumed += length + n
179		payload = payload[length+n:]
180	}
181	if depth == 0 && isEnvelopeDescriptor(descriptor) && !variantSeen {
182		return 0, ErrScanEmpty
183	}
184	return consumed, nil
185}
186
187// isEnvelopeDescriptor reports whether the descriptor is a top-level
188// directional envelope requiring exactly one variant. Session, broker, and
189// daemonmux envelopes are separate conversations with disjoint tag spaces;
190// both directions of each require exactly one variant. The daemonmux
191// preamble messages are adapter messages, not envelopes, so they are not
192// listed here.
193func isEnvelopeDescriptor(descriptor protoreflect.MessageDescriptor) bool {
194	switch string(descriptor.FullName()) {
195	case "vev.wire.v1.ClientEnvelope", "vev.wire.v1.ServerEnvelope",
196		"vev.wire.v1.BrokerClientEnvelope", "vev.wire.v1.BrokerServerEnvelope",
197		"vev.wire.v1.MuxClientEnvelope", "vev.wire.v1.MuxServerEnvelope":
198		return true
199	default:
200		return false
201	}
202}
203
204// checkVarintRange rejects a raw varint that cannot be represented by the
205// declared scalar kind. Generated unmarshal truncates the high bits of 32-bit
206// fields, so a hostile 64-bit varint could otherwise alias a legitimate value
207// (for example an in-range enum code, a bool, or a negative int32).
208func checkVarintRange(kind protoreflect.Kind, value uint64) error {
209	switch kind {
210	case protoreflect.BoolKind:
211		// Protobuf treats any nonzero varint as true, so a strict scanner
212		// rejects the non-canonical values above one.
213		if value > 1 {
214			return fmt.Errorf("%w: kind %v", ErrScanVarintRange, kind)
215		}
216	case protoreflect.Uint32Kind, protoreflect.Sint32Kind, protoreflect.EnumKind:
217		if value > math.MaxUint32 {
218			return fmt.Errorf("%w: kind %v", ErrScanVarintRange, kind)
219		}
220	case protoreflect.Int32Kind:
221		// A canonical negative int32 sign-extends into ten bytes, so the
222		// valid encodings are 0..MaxInt32 and the sign-extended range
223		// [MaxUint64-MaxInt32, MaxUint64]. Anything between the two
224		// truncates to a different int32 and is rejected.
225		if value > math.MaxInt32 && value < math.MaxUint64-uint64(math.MaxInt32) {
226			return fmt.Errorf("%w: kind %v", ErrScanVarintRange, kind)
227		}
228	}
229	return nil
230}
231
232func checkWireType(field scanField, wireType protowire.Type) error {
233	switch field.kind {
234	case protoreflect.BoolKind, protoreflect.EnumKind,
235		protoreflect.Int32Kind, protoreflect.Sint32Kind, protoreflect.Uint32Kind,
236		protoreflect.Int64Kind, protoreflect.Sint64Kind, protoreflect.Uint64Kind:
237		if wireType != protowire.VarintType {
238			return fmt.Errorf("%w: want varint", ErrScanWireType)
239		}
240	case protoreflect.Fixed32Kind, protoreflect.Sfixed32Kind, protoreflect.FloatKind:
241		if wireType != protowire.Fixed32Type {
242			return fmt.Errorf("%w: want fixed32", ErrScanWireType)
243		}
244	case protoreflect.Fixed64Kind, protoreflect.Sfixed64Kind, protoreflect.DoubleKind:
245		if wireType != protowire.Fixed64Type {
246			return fmt.Errorf("%w: want fixed64", ErrScanWireType)
247		}
248	case protoreflect.StringKind, protoreflect.BytesKind, protoreflect.MessageKind, protoreflect.GroupKind:
249		if wireType != protowire.BytesType {
250			return fmt.Errorf("%w: want length-delimited", ErrScanWireType)
251		}
252	default:
253		return fmt.Errorf("%w: kind %v", ErrScanWireType, field.kind)
254	}
255	return nil
256}
257
258func (s *envelopeScanner) scanField(descriptor protoreflect.FieldDescriptor, field scanField, wireType protowire.Type, payload []byte, depth int) (int, error) {
259	switch wireType {
260	case protowire.VarintType:
261		value, length := protowire.ConsumeVarint(payload)
262		if length < 0 {
263			return 0, ErrScanTruncated
264		}
265		if err := checkVarintRange(field.kind, value); err != nil {
266			return 0, err
267		}
268		return length, nil
269	case protowire.Fixed32Type:
270		if len(payload) < 4 {
271			return 0, ErrScanTruncated
272		}
273		return 4, nil
274	case protowire.Fixed64Type:
275		if len(payload) < 8 {
276			return 0, ErrScanTruncated
277		}
278		return 8, nil
279	case protowire.BytesType:
280		value, length := protowire.ConsumeBytes(payload)
281		if length < 0 {
282			return 0, ErrScanTruncated
283		}
284		switch {
285		case field.isMessage:
286			if descriptor == nil || descriptor.Message() == nil {
287				return 0, ErrScanNotMessage
288			}
289			if len(value) > maxScanBytesField {
290				return 0, ErrScanLength
291			}
292			if _, err := s.scanMessage(descriptor.Message(), value, depth+1); err != nil {
293				return 0, err
294			}
295			return length, nil
296		case field.kind == protoreflect.StringKind:
297			if len(value) > maxScanStringField {
298				return 0, ErrScanLength
299			}
300			return length, nil
301		default:
302			if len(value) > maxScanBytesField {
303				return 0, ErrScanLength
304			}
305			return length, nil
306		}
307	default:
308		return 0, fmt.Errorf("%w: %d", ErrScanWireType, wireType)
309	}
310}
311
312// validatedUnmarshal scans payload, unmarshals into message, and reports a
313// trailing-garbage-safe result. Callers run semantic validation afterwards.
314func validatedUnmarshal(message proto.Message, payload []byte) error {
315	if err := ScanEnvelope(message, payload); err != nil {
316		return err
317	}
318	options := proto.UnmarshalOptions{DiscardUnknown: false}
319	if err := options.Unmarshal(payload, message); err != nil {
320		return err
321	}
322	return nil
323}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 12

Tags 11

↑↓ moveenter openesc close