Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 43 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
name: test
on:
push:
branches: [main, master]
pull_request:

jobs:
test:
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest]
go-version: ["1.23.x", "1.24.x"]
runs-on: ${{ matrix.os }}
name: test (go ${{ matrix.go-version }})
steps:
- name: Install Go
uses: actions/setup-go@v5
with:
go-version: ${{ matrix.go-version }}

- name: Checkout code
uses: actions/checkout@v4

- name: Cache Go modules
uses: actions/cache@v4
with:
path: |
~/.cache/go-build
~/go/pkg/mod
key: ${{ runner.os }}-go-${{ matrix.go-version }}-${{ hashFiles('**/go.sum') }}
restore-keys: |
${{ runner.os }}-go-${{ matrix.go-version }}-
${{ runner.os }}-go-

- name: Build
run: go build ./...

- name: Vet
run: go vet ./...

- name: Test
run: go test -race -timeout 10m ./...
3 changes: 1 addition & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,7 @@
**Complete binary serialization toolkit for Go.** Code generator for binary parsing and encoding. Golang port of the [original implementation for the Tor project](https://gitweb.torproject.org/trunnel.git) with full bidirectional serialization support.

[![go.dev Reference](https://img.shields.io/badge/doc-reference-007d9b?logo=go&style=flat-square)](https://pkg.go.dev/github.com/katzenpost/trunnel)
[![Build status](https://img.shields.io/travis/katzenpost/trunnel.svg?style=flat-square)](https://travis-ci.org/katzenpost/trunnel)
[![Coverage](https://img.shields.io/coveralls/katzenpost/trunnel.svg?style=flat-square)](https://coveralls.io/r/katzenpost/trunnel)
[![test](https://github.com/katzenpost/trunnel/actions/workflows/test.yml/badge.svg)](https://github.com/katzenpost/trunnel/actions/workflows/test.yml)
[![Go Report Card](https://goreportcard.com/badge/github.com/katzenpost/trunnel?style=flat-square)](https://goreportcard.com/report/github.com/katzenpost/trunnel)

## 🚀 Status
Expand Down
70 changes: 68 additions & 2 deletions gen/decl.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ type generator struct {

func (g *generator) files(pkg string, fs []*ast.File) error {
g.header(pkg)
g.maxParseSize()

if err := g.init(fs); err != nil {
return err
Expand All @@ -43,6 +44,24 @@ func (g *generator) files(pkg string, fs []*ast.File) error {
return nil
}

// defaultMaxParseSize is the default value the generator emits for the
// package-level MaxParseSize variable. 16 MiB is comfortably above any
// realistic single-message payload while remaining far below the kind of
// allocation an adversary would use for a memory-exhaustion attack.
const defaultMaxParseSize = 16 << 20

// maxParseSize emits a package-level MaxParseSize variable that bounds the
// total input size accepted by the top-level Parse... convenience
// constructors. The check applies only at the public entry points; nested
// or in-stream parses via Foo.Parse(data, ...) remain unchecked so that
// recursive struct refs do not re-validate on every step.
func (g *generator) maxParseSize() {
g.printf("// MaxParseSize bounds the total input size accepted by the\n")
g.printf("// top-level Parse... convenience constructors in this package.\n")
g.printf("// Adjust before the first parse call to override the default.\n")
g.printf("var MaxParseSize = %d\n\n", defaultMaxParseSize)
}

func (g *generator) init(fs []*ast.File) (err error) {
g.resolver, err = inspect.NewResolverFiles(fs)
return
Expand Down Expand Up @@ -78,7 +97,17 @@ func (g *generator) structure(s *ast.Struct) {
g.structDecl(s)
g.parse(s)
g.parseConstructor(s)
g.marshalBinary(s)

// The binary encoder, MarshalBinary, and validate methods reference
// context-scoped variables (e.g. flag.Flagval) in their bodies but do
// not take the context as a parameter. Emitting them for a struct
// declared "with context X" produces code that does not compile.
// Until the encoder gains context-parameter support, skip emission
// for context-using structs. The Parse path, which threads contexts
// through its signature, is unaffected.
if len(s.Contexts) == 0 {
g.marshalBinary(s)
}

g.receiver = ""
}
Expand Down Expand Up @@ -120,6 +149,7 @@ func (g *generator) structUnionMemberDecl(m *ast.UnionMember) {
func (g *generator) parseConstructor(s *ast.Struct) {
n := name(s.Name)
g.printf("func Parse%s(data []byte%s) (*%s, error) {\n", n, contextSignature(s.Contexts), n)
g.printf("if len(data) > MaxParseSize { return nil, errors.New(\"input exceeds MaxParseSize\") }\n")
g.printf("%s := new(%s)\n", g.receiver, n)
g.printf("_, err := %s.Parse(data%s)\n", g.receiver, contextArgs(s.Contexts))
g.printf("if err != nil { return nil, err }\n")
Expand Down Expand Up @@ -226,7 +256,27 @@ func (g *generator) parseArray(lhs string, base ast.Type, s ast.LengthConstraint
g.printf("}\n")

case *ast.IDRef:
size := fmt.Sprintf("int(%s)", g.ref(s))
ref := g.ref(s)
// Bound the allocation by the remaining input before make. The
// length field is attacker-controlled and may be a u32 holding
// a value far larger than len(cur); without this check, the
// make() runs first and can be coerced into a multi-gigabyte
// allocation. The comparison is unsigned so that on 32-bit
// platforms a u32 length above MaxInt32 (which sign-extends to
// a negative int) does not silently pass an int comparison and
// then panic in runtime.makeslice.
//
// For multi-byte element types the actual allocation is
// size*elemBytes; dividing len(cur) by elemBytes keeps the
// check tight so the allocation is bounded by len(cur), not by
// len(cur)*elemBytes. For single-byte elements (u8/char) the
// bound is the same and the simpler form reads better.
if eb := elementByteSize(base); eb > 1 {
g.printf("if uint64(%s) > uint64(len(%s))/%d { return nil, errors.New(\"data too short\") }\n", ref, g.data, eb)
} else {
g.printf("if uint64(%s) > uint64(len(%s)) { return nil, errors.New(\"data too short\") }\n", ref, g.data)
}
size := fmt.Sprintf("int(%s)", ref)
g.printf("%s = make([]%s, %s)\n", lhs, g.tipe(base), size)
g.printf("for idx := 0; idx < %s; idx++ {\n", size)
g.parseType(lhs+"[idx]", base)
Expand Down Expand Up @@ -316,6 +366,22 @@ func (g *generator) assertEnd() {
g.printf("if len(%s) > 0 { return nil, errors.New(\"trailing data disallowed\") }\n", g.data)
}

// elementByteSize returns the byte size of a single element of base, used
// by parseArray to bound the allocation tightly against the remaining
// input. For variable-sized members (struct refs and the like) we
// conservatively return 1, which still bounds the allocation by
// O(len(cur)) without claiming a tighter ratio than we can prove.
func elementByteSize(base ast.Type) int {
switch t := base.(type) {
case *ast.IntType:
return int(t.Size) / 8
case *ast.CharType:
return 1
default:
return 1
}
}

func (g *generator) integer(i ast.Integer) string {
x, err := g.resolver.Integer(i)
if err != nil {
Expand Down
17 changes: 12 additions & 5 deletions gen/files_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,16 +32,23 @@ func TestFilesBuild(t *testing.T) {
}

func Build(t *testing.T, filenames []string) {
srcs := [][]byte{}
// Match how the CLI actually invokes the generator: parse all files
// in the group into a single []*ast.File, then call Marshallers once
// to emit one Go source file for the whole package. Calling
// Marshallers per-file would emit one source per input file with
// duplicate package-level declarations (e.g. MaxParseSize), which
// is not a usage pattern the generator supports.
asts := make([]*ast.File, 0, len(filenames))
for _, filename := range filenames {
f, err := parse.File(filename)
require.NoError(t, err)
src, err := Marshallers("pkg", []*ast.File{f})
require.NoError(t, err)
srcs = append(srcs, src)
asts = append(asts, f)
}

output, err := test.Build(srcs)
src, err := Marshallers("pkg", asts)
require.NoError(t, err)

output, err := test.Build([][]byte{src})
if err != nil {
t.Fatal(string(output))
}
Expand Down
27 changes: 27 additions & 0 deletions gen/tests/color/gen-marshallers.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

28 changes: 28 additions & 0 deletions gen/tests/constant/gen-marshallers.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

40 changes: 40 additions & 0 deletions gen/tests/constraint/gen-marshallers.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading