master
go 601 lines 16.1 KB
Raw
1 // Cgroups snapshot codec -- request, response view, builder, dispatch.
2
3 package protocol
4
5 import (
6 "fmt"
7 )
8
9 const (
10 cgroupsReqSize = 4
11 cgroupsRespHdr = 24
12 cgroupsDirEntry = 8
13 cgroupsItemHdr = 32
14 )
15
16 // ---------------------------------------------------------------------------
17 // Cgroups snapshot request (4 bytes)
18 // ---------------------------------------------------------------------------
19
20 // CgroupsRequest is the cgroups snapshot request payload (4 bytes).
21 type CgroupsRequest struct {
22 LayoutVersion uint16
23 Flags uint16
24 }
25
26 // Encode writes the request into buf. Returns 4 on success, 0 if buf is
27 // too small.
28 func (r *CgroupsRequest) Encode(buf []byte) int {
29 if len(buf) < cgroupsReqSize {
30 return 0
31 }
32 ne.PutUint16(buf[0:2], r.LayoutVersion)
33 ne.PutUint16(buf[2:4], r.Flags)
34 return cgroupsReqSize
35 }
36
37 // DecodeCgroupsRequest decodes a cgroups request from buf. Validates
38 // layout_version.
39 func DecodeCgroupsRequest(buf []byte) (CgroupsRequest, error) {
40 if len(buf) < cgroupsReqSize {
41 return CgroupsRequest{}, ErrTruncated
42 }
43 r := CgroupsRequest{
44 LayoutVersion: ne.Uint16(buf[0:2]),
45 Flags: ne.Uint16(buf[2:4]),
46 }
47 if r.LayoutVersion != 1 {
48 return CgroupsRequest{}, ErrBadLayout
49 }
50 // flags must be zero (reserved for future use)
51 if r.Flags != 0 {
52 return CgroupsRequest{}, ErrBadLayout
53 }
54 return r, nil
55 }
56
57 // ---------------------------------------------------------------------------
58 // CStringView - borrowed string view into payload buffer
59 // ---------------------------------------------------------------------------
60
61 // CStringView is a borrowed, zero-copy string view into the payload buffer.
62 // It wraps a byte slice that includes the NUL terminator. The view is
63 // ephemeral and valid only while the underlying payload buffer lives.
64 // Copy immediately via String() if the data is needed later.
65 type CStringView struct {
66 data []byte // includes trailing NUL
67 len uint32 // length excluding NUL
68 }
69
70 // NewCStringView creates a CStringView from a slice that includes the NUL
71 // terminator and the length excluding the NUL.
72 func NewCStringView(data []byte, length uint32) CStringView {
73 return CStringView{data: data, len: length}
74 }
75
76 // Bytes returns the string content as a byte slice (without the NUL).
77 func (v CStringView) Bytes() []byte {
78 return v.data[:v.len]
79 }
80
81 // Len returns the string length excluding the NUL terminator.
82 func (v CStringView) Len() uint32 {
83 return v.len
84 }
85
86 // String returns a copy of the string content. This allocates.
87 func (v CStringView) String() string {
88 return string(v.data[:v.len])
89 }
90
91 // GoString implements fmt.GoStringer for debug output.
92 func (v CStringView) GoString() string {
93 return fmt.Sprintf("CStringView(%q)", v.data[:v.len])
94 }
95
96 // ---------------------------------------------------------------------------
97 // Cgroups snapshot response
98 // ---------------------------------------------------------------------------
99
100 // CgroupsItemView is a per-item view -- ephemeral, borrows the payload
101 // buffer. Valid only while the payload buffer is alive.
102 type CgroupsItemView struct {
103 LayoutVersion uint16
104 Flags uint16
105 Hash uint32
106 Options uint32
107 Enabled uint32
108 Name CStringView
109 Path CStringView
110 }
111
112 // CgroupsResponseView is a full snapshot view -- ephemeral, borrows the
113 // payload buffer. Valid only during the current library call or callback.
114 // Copy immediately if the data is needed later.
115 type CgroupsResponseView struct {
116 LayoutVersion uint16
117 Flags uint16
118 ItemCount uint32
119 SystemdEnabled uint32
120 Generation uint64
121 payload []byte // full payload for item access
122 }
123
124 // DecodeCgroupsResponse decodes the snapshot response header and validates
125 // the item directory. On success, use Item() to access individual items.
126 func DecodeCgroupsResponse(buf []byte) (CgroupsResponseView, error) {
127 if len(buf) < cgroupsRespHdr {
128 return CgroupsResponseView{}, ErrTruncated
129 }
130
131 layoutVersion := ne.Uint16(buf[0:2])
132 flags := ne.Uint16(buf[2:4])
133 itemCount := ne.Uint32(buf[4:8])
134 systemdEnabled := ne.Uint32(buf[8:12])
135 reserved := ne.Uint32(buf[12:16])
136 generation := ne.Uint64(buf[16:24])
137
138 if layoutVersion != 1 {
139 return CgroupsResponseView{}, ErrBadLayout
140 }
141
142 // flags must be zero
143 if flags != 0 {
144 return CgroupsResponseView{}, ErrBadLayout
145 }
146
147 // reserved field must be zero
148 if reserved != 0 {
149 return CgroupsResponseView{}, ErrBadLayout
150 }
151
152 dirSize64 := uint64(itemCount) * uint64(cgroupsDirEntry)
153 dirEnd64 := uint64(cgroupsRespHdr) + dirSize64
154 dirEnd, ok := checkedInt(dirEnd64)
155 if !ok {
156 return CgroupsResponseView{}, ErrBadItemCount
157 }
158 if dirEnd > len(buf) {
159 return CgroupsResponseView{}, ErrTruncated
160 }
161
162 packedAreaLen := len(buf) - dirEnd
163
164 // Validate each directory entry.
165 dirSize, ok := checkedInt(dirSize64)
166 if !ok {
167 return CgroupsResponseView{}, ErrBadItemCount
168 }
169 for i := 0; i < dirSize; i += cgroupsDirEntry {
170 base := cgroupsRespHdr + i
171 off, err := checkedWireU32Int(buf, base)
172 if err != nil {
173 return CgroupsResponseView{}, err
174 }
175 length, err := checkedWireU32Int(buf, base+4)
176 if err != nil {
177 return CgroupsResponseView{}, err
178 }
179
180 if off%Alignment != 0 {
181 return CgroupsResponseView{}, ErrBadAlignment
182 }
183 end, ok := checkedAddInt(off, length)
184 if !ok || end > packedAreaLen {
185 return CgroupsResponseView{}, ErrOutOfBounds
186 }
187 if length < cgroupsItemHdr {
188 return CgroupsResponseView{}, ErrTruncated
189 }
190 }
191
192 return CgroupsResponseView{
193 LayoutVersion: layoutVersion,
194 Flags: flags,
195 ItemCount: itemCount,
196 SystemdEnabled: systemdEnabled,
197 Generation: generation,
198 payload: buf,
199 }, nil
200 }
201
202 // Item accesses the item at index from a decoded snapshot view. Returns an
203 // ephemeral item view.
204 func (v *CgroupsResponseView) Item(index uint32) (CgroupsItemView, error) {
205 if index >= v.ItemCount {
206 return CgroupsItemView{}, ErrOutOfBounds
207 }
208
209 dirStart := cgroupsRespHdr
210 dirSize, ok := checkedInt(uint64(v.ItemCount) * uint64(cgroupsDirEntry))
211 if !ok {
212 return CgroupsItemView{}, ErrBadItemCount
213 }
214 packedAreaStart, ok := checkedAddInt(dirStart, dirSize)
215 if !ok {
216 return CgroupsItemView{}, ErrOutOfBounds
217 }
218
219 dirIndexOff, ok := checkedInt(uint64(index) * uint64(cgroupsDirEntry))
220 if !ok {
221 return CgroupsItemView{}, ErrOutOfBounds
222 }
223 dirBase, ok := checkedAddInt(dirStart, dirIndexOff)
224 if !ok {
225 return CgroupsItemView{}, ErrOutOfBounds
226 }
227 itemOff, err := checkedWireU32Int(v.payload, dirBase)
228 if err != nil {
229 return CgroupsItemView{}, err
230 }
231 itemLen, err := checkedWireU32Int(v.payload, dirBase+4)
232 if err != nil {
233 return CgroupsItemView{}, err
234 }
235
236 itemStart, ok := checkedAddInt(packedAreaStart, itemOff)
237 if !ok {
238 return CgroupsItemView{}, ErrOutOfBounds
239 }
240 itemEnd, ok := checkedAddInt(itemStart, itemLen)
241 if !ok || itemEnd > len(v.payload) {
242 return CgroupsItemView{}, ErrOutOfBounds
243 }
244 item := v.payload[itemStart:itemEnd]
245
246 layoutVersion := ne.Uint16(item[0:2])
247 flags := ne.Uint16(item[2:4])
248 hash := ne.Uint32(item[4:8])
249 options := ne.Uint32(item[8:12])
250 enabled := ne.Uint32(item[12:16])
251
252 nameOff, err := checkedWireU32Int(item, 16)
253 if err != nil {
254 return CgroupsItemView{}, err
255 }
256 nameLen, err := checkedWireU32Int(item, 20)
257 if err != nil {
258 return CgroupsItemView{}, err
259 }
260 nameLen32 := ne.Uint32(item[20:24])
261 pathOff, err := checkedWireU32Int(item, 24)
262 if err != nil {
263 return CgroupsItemView{}, err
264 }
265 pathLen, err := checkedWireU32Int(item, 28)
266 if err != nil {
267 return CgroupsItemView{}, err
268 }
269 pathLen32 := ne.Uint32(item[28:32])
270
271 if layoutVersion != 1 {
272 return CgroupsItemView{}, ErrBadLayout
273 }
274
275 // item flags must be zero
276 if flags != 0 {
277 return CgroupsItemView{}, ErrBadLayout
278 }
279
280 // Validate name string.
281 if nameOff < cgroupsItemHdr {
282 return CgroupsItemView{}, ErrOutOfBounds
283 }
284 nameEnd, ok := checkedAddInt(nameOff, nameLen)
285 if !ok {
286 return CgroupsItemView{}, ErrOutOfBounds
287 }
288 nameNulEnd, ok := checkedAddInt(nameEnd, 1)
289 if !ok || nameNulEnd > itemLen {
290 return CgroupsItemView{}, ErrOutOfBounds
291 }
292 if item[nameEnd] != 0 {
293 return CgroupsItemView{}, ErrMissingNul
294 }
295
296 // Validate path string.
297 if pathOff < cgroupsItemHdr {
298 return CgroupsItemView{}, ErrOutOfBounds
299 }
300 pathEnd, ok := checkedAddInt(pathOff, pathLen)
301 if !ok {
302 return CgroupsItemView{}, ErrOutOfBounds
303 }
304 pathNulEnd, ok := checkedAddInt(pathEnd, 1)
305 if !ok || pathNulEnd > itemLen {
306 return CgroupsItemView{}, ErrOutOfBounds
307 }
308 if item[pathEnd] != 0 {
309 return CgroupsItemView{}, ErrMissingNul
310 }
311
312 // Reject overlapping name and path regions (including NUL)
313 {
314 if overlap(nameOff, nameNulEnd, pathOff, pathNulEnd) {
315 return CgroupsItemView{}, ErrBadLayout
316 }
317 }
318
319 name := NewCStringView(item[nameOff:nameNulEnd], nameLen32)
320 path := NewCStringView(item[pathOff:pathNulEnd], pathLen32)
321
322 return CgroupsItemView{
323 LayoutVersion: layoutVersion,
324 Flags: flags,
325 Hash: hash,
326 Options: options,
327 Enabled: enabled,
328 Name: name,
329 Path: path,
330 }, nil
331 }
332
333 // ---------------------------------------------------------------------------
334 // Cgroups snapshot response builder
335 // ---------------------------------------------------------------------------
336
337 // CgroupsBuilder builds a cgroups snapshot response payload.
338 //
339 // Layout during building (maxItems directory slots reserved):
340 //
341 // [24-byte header space] [maxItems*8 directory] [packed items]
342 //
343 // Layout after Finish (compacted to actual itemCount):
344 //
345 // [24-byte header] [itemCount*8 directory] [packed items]
346 type CgroupsBuilder struct {
347 buf []byte
348 systemdEnabled uint32
349 generation uint64
350 itemCount uint32
351 maxItems uint32
352 dataOffset int // current write position (absolute in buf)
353 }
354
355 // NewCgroupsBuilder initializes a cgroups response builder. buf must be
356 // caller-owned and large enough for the expected snapshot.
357 func NewCgroupsBuilder(buf []byte, maxItems uint32, systemdEnabled uint32, generation uint64) *CgroupsBuilder {
358 minRequired, ok := CgroupsBuilderMinBytes(maxItems)
359 if !ok || len(buf) < minRequired {
360 panic(fmt.Sprintf("CgroupsBuilder buffer too small: need at least %d bytes, got %d",
361 minRequired, len(buf)))
362 }
363 dataOffset := minRequired
364 return &CgroupsBuilder{
365 buf: buf,
366 systemdEnabled: systemdEnabled,
367 generation: generation,
368 maxItems: maxItems,
369 dataOffset: dataOffset,
370 }
371 }
372
373 // CgroupsBuilderMinBytes returns the minimum response buffer required to
374 // reserve directory slots for maxItems before packed item data is appended.
375 func CgroupsBuilderMinBytes(maxItems uint32) (int, bool) {
376 minRequired := uint64(cgroupsRespHdr) + uint64(maxItems)*uint64(cgroupsDirEntry)
377 return checkedInt(minRequired)
378 }
379
380 // SetHeader updates the response header fields written by Finish().
381 func (b *CgroupsBuilder) SetHeader(systemdEnabled uint32, generation uint64) {
382 b.systemdEnabled = systemdEnabled
383 b.generation = generation
384 }
385
386 // EstimateCgroupsMaxItems returns a safe upper bound for the number of
387 // cgroup items that can fit in a response buffer of size bufSize.
388 //
389 // This is an upper bound for builder reservation, not a promise that all of
390 // those items will fit with arbitrary string lengths.
391 func EstimateCgroupsMaxItems(bufSize int) uint32 {
392 if bufSize <= cgroupsRespHdr {
393 return 0
394 }
395
396 minAlignedItem := Align8(cgroupsItemHdr + 2)
397 items := (bufSize - cgroupsRespHdr) / (cgroupsDirEntry + minAlignedItem)
398 items32, ok := checkedU32Int(items)
399 if !ok {
400 return ^uint32(0)
401 }
402 return items32
403 }
404
405 // Add adds one cgroup item. Handles offset bookkeeping, NUL termination,
406 // and alignment.
407 func (b *CgroupsBuilder) Add(hash, options, enabled uint32, name, path []byte) error {
408 if b.itemCount >= b.maxItems {
409 return ErrOverflow
410 }
411
412 itemStart, ok := checkedAlign8(b.dataOffset)
413 if !ok {
414 return ErrOverflow
415 }
416
417 nameSize, ok := checkedAddInt(len(name), 1)
418 if !ok {
419 return ErrOverflow
420 }
421 pathSize, ok := checkedAddInt(len(path), 1)
422 if !ok {
423 return ErrOverflow
424 }
425 itemSize, ok := checkedAddInt(cgroupsItemHdr, nameSize)
426 if !ok {
427 return ErrOverflow
428 }
429 itemSize, ok = checkedAddInt(itemSize, pathSize)
430 if !ok {
431 return ErrOverflow
432 }
433
434 itemEnd, ok := checkedAddInt(itemStart, itemSize)
435 if !ok || itemEnd > len(b.buf) {
436 return ErrOverflow
437 }
438
439 // Zero alignment padding.
440 if itemStart > b.dataOffset {
441 clear(b.buf[b.dataOffset:itemStart])
442 }
443
444 nameLen32, ok := checkedU32Int(len(name))
445 if !ok {
446 return ErrOverflow
447 }
448 pathLen32, ok := checkedU32Int(len(path))
449 if !ok {
450 return ErrOverflow
451 }
452 nameOffset32 := uint32(cgroupsItemHdr)
453 pathOffset, ok := checkedAddInt(cgroupsItemHdr, nameSize)
454 if !ok {
455 return ErrOverflow
456 }
457 pathOffset32, ok := checkedU32Int(pathOffset)
458 if !ok {
459 return ErrOverflow
460 }
461 itemStart32, ok := checkedU32Int(itemStart)
462 if !ok {
463 return ErrOverflow
464 }
465 itemSize32, ok := checkedU32Int(itemSize)
466 if !ok {
467 return ErrOverflow
468 }
469
470 // Write item header.
471 p := itemStart
472 ne.PutUint16(b.buf[p:p+2], 1) // layout_version
473 ne.PutUint16(b.buf[p+2:p+4], 0) // flags
474 ne.PutUint32(b.buf[p+4:p+8], hash)
475 ne.PutUint32(b.buf[p+8:p+12], options)
476 ne.PutUint32(b.buf[p+12:p+16], enabled)
477 ne.PutUint32(b.buf[p+16:p+20], nameOffset32)
478 ne.PutUint32(b.buf[p+20:p+24], nameLen32)
479 ne.PutUint32(b.buf[p+24:p+28], pathOffset32)
480 ne.PutUint32(b.buf[p+28:p+32], pathLen32)
481
482 // Write strings with NUL terminators.
483 ns := p + cgroupsItemHdr
484 copy(b.buf[ns:], name)
485 b.buf[ns+len(name)] = 0
486
487 ps := p + pathOffset
488 copy(b.buf[ps:], path)
489 b.buf[ps+len(path)] = 0
490
491 // Write directory entry (absolute offset stored temporarily).
492 dirEntryOff, ok := checkedInt(uint64(b.itemCount) * uint64(cgroupsDirEntry))
493 if !ok {
494 return ErrOverflow
495 }
496 dirEntry, ok := checkedAddInt(cgroupsRespHdr, dirEntryOff)
497 if !ok {
498 return ErrOverflow
499 }
500 ne.PutUint32(b.buf[dirEntry:dirEntry+4], itemStart32)
501 ne.PutUint32(b.buf[dirEntry+4:dirEntry+8], itemSize32)
502
503 b.dataOffset = itemEnd
504 b.itemCount++
505 return nil
506 }
507
508 // Finish finalizes the builder. Returns the total payload size. The buffer
509 // now contains a complete, decodable cgroups snapshot response payload.
510 func (b *CgroupsBuilder) Finish() int {
511 p := b.buf
512
513 if b.itemCount == 0 {
514 ne.PutUint16(p[0:2], 1)
515 ne.PutUint16(p[2:4], 0)
516 ne.PutUint32(p[4:8], 0)
517 ne.PutUint32(p[8:12], b.systemdEnabled)
518 ne.PutUint32(p[12:16], 0)
519 ne.PutUint64(p[16:24], b.generation)
520 return cgroupsRespHdr
521 }
522
523 dirSize, ok := checkedInt(uint64(b.itemCount) * uint64(cgroupsDirEntry))
524 if !ok {
525 return 0
526 }
527 finalPackedStart, ok := checkedAddInt(cgroupsRespHdr, dirSize)
528 if !ok {
529 return 0
530 }
531
532 // Read the first directory entry to find where packed data begins.
533 firstItemAbs32 := ne.Uint32(p[cgroupsRespHdr : cgroupsRespHdr+4])
534 firstItemAbs, ok := checkedInt(uint64(firstItemAbs32))
535 if !ok {
536 return 0
537 }
538
539 packedDataLen := b.dataOffset - firstItemAbs
540
541 if finalPackedStart < firstItemAbs {
542 packedDataEnd, ok := checkedAddInt(firstItemAbs, packedDataLen)
543 if !ok {
544 return 0
545 }
546 // Shift packed data left.
547 copy(p[finalPackedStart:], p[firstItemAbs:packedDataEnd])
548 }
549
550 // Convert directory entries from absolute to relative offsets.
551 dirBase := cgroupsRespHdr
552 for i := uint32(0); i < b.itemCount; i++ {
553 entryOff, ok := checkedInt(uint64(i) * uint64(cgroupsDirEntry))
554 if !ok {
555 return 0
556 }
557 entry, ok := checkedAddInt(dirBase, entryOff)
558 if !ok {
559 return 0
560 }
561 absOff := ne.Uint32(p[entry : entry+4])
562 if absOff < firstItemAbs32 {
563 return 0
564 }
565 relOff := absOff - firstItemAbs32
566 ne.PutUint32(p[entry:entry+4], relOff)
567 // length stays the same.
568 }
569
570 // Write snapshot header.
571 ne.PutUint16(p[0:2], 1)
572 ne.PutUint16(p[2:4], 0)
573 ne.PutUint32(p[4:8], b.itemCount)
574 ne.PutUint32(p[8:12], b.systemdEnabled)
575 ne.PutUint32(p[12:16], 0)
576 ne.PutUint64(p[16:24], b.generation)
577
578 total, ok := checkedAddInt(finalPackedStart, packedDataLen)
579 if !ok {
580 return 0
581 }
582 return total
583 }
584
585 // DispatchCgroupsSnapshot decodes request, builds response via handler.
586 func DispatchCgroupsSnapshot(req []byte, resp []byte, maxItems uint32,
587 handler func(*CgroupsRequest, *CgroupsBuilder) bool) (int, bool) {
588 request, err := DecodeCgroupsRequest(req)
589 if err != nil {
590 return 0, false
591 }
592 minRequired, ok := CgroupsBuilderMinBytes(maxItems)
593 if !ok || len(resp) < minRequired {
594 return 0, false
595 }
596 builder := NewCgroupsBuilder(resp, maxItems, 0, 0)
597 if !handler(&request, builder) {
598 return 0, false
599 }
600 return builder.Finish(), true
601 }