Skip to content

Commit f2b2f6d

Browse files
committed
program, btf: probe correct log buffer size
The kernel exposes a log of operations to aid debugging a program or BTF load. Until recently there was no way to know the size of that buffer, so the library forces the user to specify a buffer size. From a cursory survey on sourcegraph.com it seems that users either hardcode a large fixed buffer or probe the correct size by doubling the buffer size when hitting ENOSPC. Since commit 47a71c1f9af0 ("bpf: Add log_true_size output field to return necessary log buffer size") the kernel does provide a hint to user space. Move probing of the correct log buffer size into the library. On recent (>= 6.4) kernels this is guaranteed to work in a single call. On older kernels we use the doubling strategy employed by cilium and tetragon. Signed-off-by: Lorenz Bauer <[email protected]>
1 parent 12aca10 commit f2b2f6d

File tree

6 files changed

+113
-143
lines changed

6 files changed

+113
-143
lines changed

btf/btf_test.go

-4
Original file line numberDiff line numberDiff line change
@@ -316,10 +316,6 @@ func TestVerifierError(t *testing.T) {
316316
if !errors.As(err, &ve) {
317317
t.Fatalf("expected a VerifierError, got: %v", err)
318318
}
319-
320-
if ve.Truncated {
321-
t.Fatalf("expected non-truncated verifier log: %v", err)
322-
}
323319
}
324320

325321
func TestGuessBTFByteOrder(t *testing.T) {

btf/handle.go

+44-14
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,8 @@ func NewHandle(b *Builder) (*Handle, error) {
4141
//
4242
// Returns an error wrapping ErrNotSupported if the kernel doesn't support BTF.
4343
func NewHandleFromRawBTF(btf []byte) (*Handle, error) {
44+
const minLogSize = 64 * 1024
45+
4446
if uint64(len(btf)) > math.MaxUint32 {
4547
return nil, errors.New("BTF exceeds the maximum size")
4648
}
@@ -50,26 +52,54 @@ func NewHandleFromRawBTF(btf []byte) (*Handle, error) {
5052
BtfSize: uint32(len(btf)),
5153
}
5254

53-
fd, err := sys.BtfLoad(attr)
54-
if err == nil {
55-
return &Handle{fd, attr.BtfSize, false}, nil
55+
var (
56+
logBuf []byte
57+
err error
58+
)
59+
for {
60+
var fd *sys.FD
61+
fd, err = sys.BtfLoad(attr)
62+
if err == nil {
63+
return &Handle{fd, attr.BtfSize, false}, nil
64+
}
65+
66+
if attr.BtfLogTrueSize != 0 && attr.BtfLogSize >= attr.BtfLogTrueSize {
67+
// The log buffer already has the correct size.
68+
break
69+
}
70+
71+
if attr.BtfLogSize != 0 && !errors.Is(err, unix.ENOSPC) {
72+
// Up until at least kernel 6.0, the BTF verifier does not return ENOSPC
73+
// if there are other verification errors. ENOSPC is only returned when
74+
// the BTF blob is correct, a log was requested, and the provided buffer
75+
// is too small. We're therefore not sure whether we got the full
76+
// log or not.
77+
break
78+
}
79+
80+
// Make an educated guess how large the buffer should be. Start
81+
// at a reasonable minimum and then double the size.
82+
logSize := uint32(max(len(logBuf)*2, minLogSize))
83+
if int(logSize) < len(logBuf) {
84+
return nil, errors.New("overflow while probing log buffer size")
85+
}
86+
87+
if attr.BtfLogTrueSize != 0 {
88+
// The kernel has given us a hint how large the log buffer has to be.
89+
logSize = attr.BtfLogTrueSize
90+
}
91+
92+
logBuf = make([]byte, logSize)
93+
attr.BtfLogSize = logSize
94+
attr.BtfLogBuf = sys.NewSlicePointer(logBuf)
95+
attr.BtfLogLevel = 1
5696
}
5797

5898
if err := haveBTF(); err != nil {
5999
return nil, err
60100
}
61101

62-
logBuf := make([]byte, 64*1024)
63-
attr.BtfLogBuf = sys.NewSlicePointer(logBuf)
64-
attr.BtfLogSize = uint32(len(logBuf))
65-
attr.BtfLogLevel = 1
66-
67-
// Up until at least kernel 6.0, the BTF verifier does not return ENOSPC
68-
// if there are other verification errors. ENOSPC is only returned when
69-
// the BTF blob is correct, a log was requested, and the provided buffer
70-
// is too small.
71-
_, ve := sys.BtfLoad(attr)
72-
return nil, internal.ErrorWithLog("load btf", err, logBuf, errors.Is(ve, unix.ENOSPC))
102+
return nil, internal.ErrorWithLog("load btf", err, logBuf)
73103
}
74104

75105
// NewHandleFromID returns the BTF handle for a given id.

internal/errors.go

+5-24
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import (
1212
//
1313
// The default error output is a summary of the full log. The latter can be
1414
// accessed via VerifierError.Log or by formatting the error, see Format.
15-
func ErrorWithLog(source string, err error, log []byte, truncated bool) *VerifierError {
15+
func ErrorWithLog(source string, err error, log []byte) *VerifierError {
1616
const whitespace = "\t\r\v\n "
1717

1818
// Convert verifier log C string by truncating it on the first 0 byte
@@ -23,7 +23,7 @@ func ErrorWithLog(source string, err error, log []byte, truncated bool) *Verifie
2323

2424
log = bytes.Trim(log, whitespace)
2525
if len(log) == 0 {
26-
return &VerifierError{source, err, nil, truncated}
26+
return &VerifierError{source, err, nil}
2727
}
2828

2929
logLines := bytes.Split(log, []byte{'\n'})
@@ -34,7 +34,7 @@ func ErrorWithLog(source string, err error, log []byte, truncated bool) *Verifie
3434
lines = append(lines, string(bytes.TrimRight(line, whitespace)))
3535
}
3636

37-
return &VerifierError{source, err, lines, truncated}
37+
return &VerifierError{source, err, lines}
3838
}
3939

4040
// VerifierError includes information from the eBPF verifier.
@@ -46,8 +46,6 @@ type VerifierError struct {
4646
Cause error
4747
// The verifier output split into lines.
4848
Log []string
49-
// Whether the log output is truncated, based on several heuristics.
50-
Truncated bool
5149
}
5250

5351
func (le *VerifierError) Unwrap() error {
@@ -70,7 +68,7 @@ func (le *VerifierError) Error() string {
7068
}
7169

7270
lines := log[n-1:]
73-
if n >= 2 && (includePreviousLine(log[n-1]) || le.Truncated) {
71+
if n >= 2 && includePreviousLine(log[n-1]) {
7472
// Add one more line of context if it aids understanding the error.
7573
lines = log[n-2:]
7674
}
@@ -81,22 +79,9 @@ func (le *VerifierError) Error() string {
8179
}
8280

8381
omitted := len(le.Log) - len(lines)
84-
if omitted == 0 && !le.Truncated {
85-
return b.String()
86-
}
87-
88-
b.WriteString(" (")
89-
if le.Truncated {
90-
b.WriteString("truncated")
91-
}
92-
9382
if omitted > 0 {
94-
if le.Truncated {
95-
b.WriteString(", ")
96-
}
97-
fmt.Fprintf(&b, "%d line(s) omitted", omitted)
83+
fmt.Fprintf(&b, " (%d line(s) omitted)", omitted)
9884
}
99-
b.WriteString(")")
10085

10186
return b.String()
10287
}
@@ -188,10 +173,6 @@ func (le *VerifierError) Format(f fmt.State, verb rune) {
188173
}
189174
}
190175

191-
if le.Truncated {
192-
fmt.Fprintf(f, "\n\t(truncated)")
193-
}
194-
195176
default:
196177
fmt.Fprintf(f, "%%!%c(BADVERB)", verb)
197178
}

internal/errors_test.go

+7-10
Original file line numberDiff line numberDiff line change
@@ -20,36 +20,33 @@ func TestVerifierErrorWhitespace(t *testing.T) {
2020
0, 0, // trailing NUL bytes
2121
)
2222

23-
err := ErrorWithLog("frob", errors.New("test"), b, false)
23+
err := ErrorWithLog("frob", errors.New("test"), b)
2424
qt.Assert(t, qt.Equals(err.Error(), "frob: test: unreachable insn 28"))
2525

2626
for _, log := range [][]byte{
2727
nil,
2828
[]byte("\x00"),
2929
[]byte(" "),
3030
} {
31-
err = ErrorWithLog("frob", errors.New("test"), log, false)
31+
err = ErrorWithLog("frob", errors.New("test"), log)
3232
qt.Assert(t, qt.Equals(err.Error(), "frob: test"), qt.Commentf("empty log %q has incorrect format", log))
3333
}
3434
}
3535

3636
func TestVerifierErrorWrapping(t *testing.T) {
37-
ve := ErrorWithLog("frob", unix.ENOENT, nil, false)
37+
ve := ErrorWithLog("frob", unix.ENOENT, nil)
3838
qt.Assert(t, qt.ErrorIs(ve, unix.ENOENT), qt.Commentf("should wrap provided error"))
39-
qt.Assert(t, qt.IsFalse(ve.Truncated), qt.Commentf("verifier log should not be marked as truncated"))
4039

41-
ve = ErrorWithLog("frob", unix.EINVAL, nil, true)
40+
ve = ErrorWithLog("frob", unix.EINVAL, nil)
4241
qt.Assert(t, qt.ErrorIs(ve, unix.EINVAL), qt.Commentf("should wrap provided error"))
43-
qt.Assert(t, qt.IsTrue(ve.Truncated), qt.Commentf("verifier log should be marked as truncated"))
4442

45-
ve = ErrorWithLog("frob", unix.EINVAL, []byte("foo"), false)
43+
ve = ErrorWithLog("frob", unix.EINVAL, []byte("foo"))
4644
qt.Assert(t, qt.ErrorIs(ve, unix.EINVAL), qt.Commentf("should wrap provided error"))
4745
qt.Assert(t, qt.StringContains(ve.Error(), "foo"), qt.Commentf("verifier log should appear in error string"))
4846

49-
ve = ErrorWithLog("frob", unix.ENOSPC, []byte("foo"), true)
47+
ve = ErrorWithLog("frob", unix.ENOSPC, []byte("foo"))
5048
qt.Assert(t, qt.ErrorIs(ve, unix.ENOSPC), qt.Commentf("should wrap provided error"))
5149
qt.Assert(t, qt.StringContains(ve.Error(), "foo"), qt.Commentf("verifier log should appear in error string"))
52-
qt.Assert(t, qt.IsTrue(ve.Truncated), qt.Commentf("verifier log should be marked truncated"))
5350
}
5451

5552
func TestVerifierErrorSummary(t *testing.T) {
@@ -84,5 +81,5 @@ func readErrorFromFile(tb testing.TB, file string) *VerifierError {
8481
tb.Fatal("Read file:", err)
8582
}
8683

87-
return ErrorWithLog("file", unix.EINVAL, contents, false)
84+
return ErrorWithLog("file", unix.EINVAL, contents)
8885
}

prog.go

+47-49
Original file line numberDiff line numberDiff line change
@@ -46,13 +46,9 @@ const (
4646
outputPad = 256 + 2
4747
)
4848

49-
// DefaultVerifierLogSize is the default number of bytes allocated for the
49+
// minVerifierLogSize is the default number of bytes allocated for the
5050
// verifier log.
51-
const DefaultVerifierLogSize = 64 * 1024
52-
53-
// maxVerifierLogSize is the maximum size of verifier log buffer the kernel
54-
// will accept before returning EINVAL.
55-
const maxVerifierLogSize = math.MaxUint32 >> 2
51+
const minVerifierLogSize = 64 * 1024
5652

5753
// ProgramOptions control loading a program into the kernel.
5854
type ProgramOptions struct {
@@ -73,17 +69,6 @@ type ProgramOptions struct {
7369
// attempt at loading the program.
7470
LogLevel LogLevel
7571

76-
// Controls the output buffer size for the verifier log, in bytes. See the
77-
// documentation on ProgramOptions.LogLevel for details about how this value
78-
// is used.
79-
//
80-
// If this value is set too low to fit the verifier log, the resulting
81-
// [ebpf.VerifierError]'s Truncated flag will be true, and the error string
82-
// will also contain a hint to that effect.
83-
//
84-
// Defaults to DefaultVerifierLogSize.
85-
LogSize int
86-
8772
// Disables the verifier log completely, regardless of other options.
8873
LogDisabled bool
8974

@@ -262,10 +247,6 @@ func newProgramWithOptions(spec *ProgramSpec, opts ProgramOptions) (*Program, er
262247
return nil, fmt.Errorf("can't load %s program on %s", spec.ByteOrder, internal.NativeEndian)
263248
}
264249

265-
if opts.LogSize < 0 {
266-
return nil, errors.New("ProgramOptions.LogSize must be a positive value; disable verifier logs using ProgramOptions.LogDisabled")
267-
}
268-
269250
// Kernels before 5.0 (6c4fc209fcf9 "bpf: remove useless version check for prog load")
270251
// require the version field to be set to the value of the KERNEL_VERSION
271252
// macro for kprobe-type programs.
@@ -404,37 +385,59 @@ func newProgramWithOptions(spec *ProgramSpec, opts ProgramOptions) (*Program, er
404385
}
405386
}
406387

407-
if opts.LogSize == 0 {
408-
opts.LogSize = DefaultVerifierLogSize
409-
}
410-
411-
// The caller requested a specific verifier log level. Set up the log buffer.
388+
// The caller requested a specific verifier log level. Set up the log buffer
389+
// so that there is a chance of loading the program in a single shot.
412390
var logBuf []byte
413391
if !opts.LogDisabled && opts.LogLevel != 0 {
414-
logBuf = make([]byte, opts.LogSize)
392+
logBuf = make([]byte, minVerifierLogSize)
415393
attr.LogLevel = opts.LogLevel
416394
attr.LogSize = uint32(len(logBuf))
417395
attr.LogBuf = sys.NewSlicePointer(logBuf)
418396
}
419397

420-
fd, err := sys.ProgLoad(attr)
421-
if err == nil {
422-
return &Program{unix.ByteSliceToString(logBuf), fd, spec.Name, "", spec.Type}, nil
423-
}
398+
for {
399+
var fd *sys.FD
400+
fd, err = sys.ProgLoad(attr)
401+
if err == nil {
402+
return &Program{unix.ByteSliceToString(logBuf), fd, spec.Name, "", spec.Type}, nil
403+
}
424404

425-
// An error occurred loading the program, but the caller did not explicitly
426-
// enable the verifier log. Re-run with branch-level verifier logs enabled to
427-
// obtain more info. Preserve the original error to return it to the caller.
428-
// An undersized log buffer will result in ENOSPC regardless of the underlying
429-
// cause.
430-
var err2 error
431-
if !opts.LogDisabled && opts.LogLevel == 0 {
432-
logBuf = make([]byte, opts.LogSize)
433-
attr.LogLevel = LogLevelBranch
434-
attr.LogSize = uint32(len(logBuf))
435-
attr.LogBuf = sys.NewSlicePointer(logBuf)
405+
if opts.LogDisabled {
406+
break
407+
}
436408

437-
_, err2 = sys.ProgLoad(attr)
409+
if attr.LogTrueSize != 0 && attr.LogSize >= attr.LogTrueSize {
410+
// The log buffer already has the correct size.
411+
break
412+
}
413+
414+
if attr.LogSize != 0 && !errors.Is(err, unix.ENOSPC) {
415+
// Logging is enabled and the error is not ENOSPC, so we can infer
416+
// that the log buffer is large enough.
417+
break
418+
}
419+
420+
if attr.LogLevel == 0 {
421+
// Logging is not enabled but loading the program failed. Enable
422+
// basic logging.
423+
attr.LogLevel = LogLevelBranch
424+
}
425+
426+
// Make an educated guess how large the buffer should be. Start
427+
// at minVerifierLogSize and then double the size.
428+
logSize := uint32(max(len(logBuf)*2, minVerifierLogSize))
429+
if int(logSize) < len(logBuf) {
430+
return nil, errors.New("overflow while probing log buffer size")
431+
}
432+
433+
if attr.LogTrueSize != 0 {
434+
// The kernel has given us a hint how large the log buffer has to be.
435+
logSize = attr.LogTrueSize
436+
}
437+
438+
logBuf = make([]byte, logSize)
439+
attr.LogSize = logSize
440+
attr.LogBuf = sys.NewSlicePointer(logBuf)
438441
}
439442

440443
end := bytes.IndexByte(logBuf, 0)
@@ -452,10 +455,6 @@ func newProgramWithOptions(spec *ProgramSpec, opts ProgramOptions) (*Program, er
452455
}
453456

454457
case errors.Is(err, unix.EINVAL):
455-
if opts.LogSize > maxVerifierLogSize {
456-
return nil, fmt.Errorf("load program: %w (ProgramOptions.LogSize exceeds maximum value of %d)", err, maxVerifierLogSize)
457-
}
458-
459458
if bytes.Contains(tail, coreBadCall) {
460459
err = errBadRelocation
461460
break
@@ -479,8 +478,7 @@ func newProgramWithOptions(spec *ProgramSpec, opts ProgramOptions) (*Program, er
479478
}
480479
}
481480

482-
truncated := errors.Is(err, unix.ENOSPC) || errors.Is(err2, unix.ENOSPC)
483-
return nil, internal.ErrorWithLog("load program", err, logBuf, truncated)
481+
return nil, internal.ErrorWithLog("load program", err, logBuf)
484482
}
485483

486484
// NewProgramFromFD creates a program from a raw fd.

0 commit comments

Comments
 (0)