Skip to content

Commit 148c76c

Browse files
committed
internal: make VerifierError more succinct
Instead of dumping sometimes megabytes of log lines, summarise verifier errors. Very short logs are shown in full, while longer longs only show the last few lines by default. internal/errors_test.go:55: invalid argument: JIT doesn't support bpf-to-bpf calls (1 line(s) omitted) internal/errors_test.go:60: invalid argument: [52] STRUCT task_struct size=7744 vlen=218: cpus_mask type_id=109 bitfield_size=0 bits_offset=7744 Invalid member (27 line(s) omitted) internal/errors_test.go:64: invalid argument: [11] FUNC helper_func2 type_id=10 vlen != 0 (24 line(s) omitted) internal/errors_test.go:69: invalid argument: str_off: 3166088: str_len: 228 (truncated, 6 line(s) omitted) Using %(+|-)v users can print the first or last n log lines. Individual fields of VerifierError are also exported.
1 parent 544f053 commit 148c76c

File tree

9 files changed

+291
-42
lines changed

9 files changed

+291
-42
lines changed

btf/btf.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -698,9 +698,9 @@ func NewHandle(spec *Spec) (*Handle, error) {
698698
attr.BtfLogBuf = sys.NewSlicePointer(logBuf)
699699
attr.BtfLogSize = uint32(len(logBuf))
700700
attr.BtfLogLevel = 1
701-
_, logErr := sys.BtfLoad(attr)
702701
// NB: The syscall will never return ENOSPC as of 5.18-rc4.
703-
return nil, internal.ErrorWithLog(err, logBuf, logErr)
702+
_, _ = sys.BtfLoad(attr)
703+
return nil, internal.ErrorWithLog(err, logBuf)
704704
}
705705

706706
return &Handle{spec.Copy(), fd}, nil

internal/errors.go

Lines changed: 158 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -2,46 +2,186 @@ package internal
22

33
import (
44
"bytes"
5-
"errors"
65
"fmt"
7-
8-
"github.com/cilium/ebpf/internal/unix"
6+
"io"
7+
"strings"
98
)
109

11-
// ErrorWithLog returns an error that includes logs from the
12-
// kernel verifier.
10+
// ErrorWithLog returns an error which includes logs from the kernel verifier.
11+
//
12+
// The default error output is a summary of the full log. The latter can be
13+
// accessed via VerifierError.Log or by formatting the error, see Format.
1314
//
14-
// logErr should be the error returned by the syscall that generated
15-
// the log. It is used to check for truncation of the output.
16-
func ErrorWithLog(err error, log []byte, logErr error) error {
15+
// A set of heuristics is used to determine whether the log has been truncated.
16+
func ErrorWithLog(err error, log []byte) *VerifierError {
17+
const whitespace = "\t\r\v\n "
18+
1719
// Convert verifier log C string by truncating it on the first 0 byte
1820
// and trimming trailing whitespace before interpreting as a Go string.
21+
truncated := false
1922
if i := bytes.IndexByte(log, 0); i != -1 {
23+
if i == len(log)-1 && !bytes.HasSuffix(log[:i], []byte{'\n'}) {
24+
// The null byte is at the end of the buffer and it's not preceded
25+
// by a newline character. Most likely the buffer was too short.
26+
truncated = true
27+
}
28+
2029
log = log[:i]
30+
} else if len(log) > 0 {
31+
// No null byte? Dodgy!
32+
truncated = true
2133
}
22-
logStr := string(bytes.Trim(log, "\t\r\n "))
2334

24-
if errors.Is(logErr, unix.ENOSPC) {
25-
logStr += " (truncated...)"
35+
log = bytes.Trim(log, whitespace)
36+
logLines := bytes.Split(log, []byte{'\n'})
37+
lines := make([]string, 0, len(logLines))
38+
for _, line := range logLines {
39+
// Don't remove leading white space on individual lines. We rely on it
40+
// when outputting logs.
41+
lines = append(lines, string(bytes.TrimRight(line, whitespace)))
2642
}
2743

28-
return &VerifierError{err, logStr}
44+
return &VerifierError{err, lines, truncated}
2945
}
3046

3147
// VerifierError includes information from the eBPF verifier.
48+
//
49+
// It summarises the log output, see Format if you want to output the full contents.
3250
type VerifierError struct {
33-
cause error
34-
log string
51+
// The error which caused this error.
52+
Cause error
53+
// The verifier output split into lines.
54+
Log []string
55+
// Whether the log output is truncated, based on several heuristics.
56+
Truncated bool
3557
}
3658

3759
func (le *VerifierError) Unwrap() error {
38-
return le.cause
60+
return le.Cause
3961
}
4062

4163
func (le *VerifierError) Error() string {
42-
if le.log == "" {
43-
return le.cause.Error()
64+
log := le.Log
65+
if n := len(log); n > 0 && strings.HasPrefix(log[n-1], "processed ") {
66+
// Get rid of "processed 39 insns (limit 1000000) ..." from summary.
67+
log = log[:n-1]
68+
}
69+
70+
output := 0
71+
switch n := len(log); n {
72+
case 0:
73+
return le.Cause.Error()
74+
75+
case 1, 2:
76+
output = n
77+
78+
default:
79+
output = 1
80+
if strings.HasPrefix(log[n-1], "\t") || le.Truncated {
81+
// Add one more line of context if the last line starts with a tab,
82+
// or if it has been truncated.
83+
// For example:
84+
// [13] STRUCT drm_rect size=16 vlen=4
85+
// \tx1 type_id=2
86+
output++
87+
}
88+
}
89+
90+
var b strings.Builder
91+
fmt.Fprintf(&b, "%s: ", le.Cause.Error())
92+
93+
lines := log[len(log)-output:]
94+
for i, line := range lines {
95+
b.WriteString(strings.TrimSpace(line))
96+
if i != len(lines)-1 {
97+
b.WriteString(": ")
98+
}
99+
}
100+
101+
omitted := len(le.Log) - len(lines)
102+
if omitted == 0 && !le.Truncated {
103+
return b.String()
104+
}
105+
106+
b.WriteString(" (")
107+
if le.Truncated {
108+
b.WriteString("truncated")
109+
}
110+
111+
if omitted > 0 {
112+
if le.Truncated {
113+
b.WriteString(", ")
114+
}
115+
fmt.Fprintf(&b, "%d line(s) omitted", omitted)
44116
}
117+
b.WriteString(")")
118+
119+
return b.String()
120+
}
121+
122+
// Format the error.
123+
//
124+
// Understood verbs are %s and %v, which are equivalent to calling Error(). %v
125+
// allows outputting additional information using the following flags:
126+
//
127+
// + Output the first <width> lines, or all lines if no width is given.
128+
// - Output the last <width> lines, or all lines if no width is given.
129+
//
130+
// Use width to specify how many lines to output. Use the '-' flag to output
131+
// lines from the end of the log instead of the beginning.
132+
func (le *VerifierError) Format(f fmt.State, verb rune) {
133+
switch verb {
134+
case 's':
135+
_, _ = io.WriteString(f, le.Error())
136+
137+
case 'v':
138+
n, haveWidth := f.Width()
139+
if !haveWidth || n > len(le.Log) {
140+
n = len(le.Log)
141+
}
142+
143+
if !f.Flag('+') && !f.Flag('-') {
144+
if haveWidth {
145+
_, _ = io.WriteString(f, "%!v(BADWIDTH)")
146+
return
147+
}
45148

46-
return fmt.Sprintf("%s: %s", le.cause, le.log)
149+
_, _ = io.WriteString(f, le.Error())
150+
return
151+
}
152+
153+
if f.Flag('+') && f.Flag('-') {
154+
_, _ = io.WriteString(f, "%!v(BADFLAG)")
155+
return
156+
}
157+
158+
fmt.Fprintf(f, "%s:", le.Cause.Error())
159+
160+
omitted := len(le.Log) - n
161+
lines := le.Log[:n]
162+
if f.Flag('-') {
163+
// Print last instead of first lines.
164+
lines = le.Log[len(le.Log)-n:]
165+
if omitted > 0 {
166+
fmt.Fprintf(f, "\n\t(%d line(s) omitted)", omitted)
167+
}
168+
}
169+
170+
for _, line := range lines {
171+
fmt.Fprintf(f, "\n\t%s", line)
172+
}
173+
174+
if !f.Flag('-') {
175+
if omitted > 0 {
176+
fmt.Fprintf(f, "\n\t(%d line(s) omitted)", omitted)
177+
}
178+
}
179+
180+
if le.Truncated {
181+
fmt.Fprintf(f, "\n\t(truncated)")
182+
}
183+
184+
default:
185+
fmt.Fprintf(f, "%%!%c(BADVERB)", verb)
186+
}
47187
}

internal/errors_test.go

Lines changed: 88 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,16 @@ package internal
22

33
import (
44
"errors"
5+
"fmt"
6+
"os"
7+
"syscall"
58
"testing"
69

710
"github.com/cilium/ebpf/internal/unix"
11+
qt "github.com/frankban/quicktest"
812
)
913

10-
func TestErrorWithLog(t *testing.T) {
14+
func TestVerifierErrorWhitespace(t *testing.T) {
1115
b := []byte("unreachable insn 28")
1216
b = append(b,
1317
0xa, // \n
@@ -17,9 +21,9 @@ func TestErrorWithLog(t *testing.T) {
1721
0, 0, // trailing NUL bytes
1822
)
1923

20-
err := ErrorWithLog(errors.New("test"), b, unix.ENOSPC)
24+
err := ErrorWithLog(errors.New("test"), b)
2125

22-
want := "test: unreachable insn 28 (truncated...)"
26+
want := "test: unreachable insn 28"
2327
got := err.Error()
2428

2529
t.Log(got)
@@ -28,3 +32,84 @@ func TestErrorWithLog(t *testing.T) {
2832
t.Fatalf("\nwant: %s\ngot: %s", want, got)
2933
}
3034
}
35+
36+
func TestVerifierError(t *testing.T) {
37+
for _, test := range []struct {
38+
name string
39+
log string
40+
}{
41+
{"missing null", "foo"},
42+
{"missing newline before null", "foo\x00"},
43+
} {
44+
t.Run("truncate "+test.name, func(t *testing.T) {
45+
ve := ErrorWithLog(syscall.ENOENT, []byte(test.log))
46+
qt.Assert(t, ve, qt.IsNotNil, qt.Commentf("should return error"))
47+
qt.Assert(t, ve.Truncated, qt.IsTrue, qt.Commentf("should be truncated"))
48+
})
49+
}
50+
51+
ve := ErrorWithLog(syscall.ENOENT, nil)
52+
qt.Assert(t, ve, qt.IsNotNil, qt.Commentf("should return error without log or logErr"))
53+
54+
errno524 := readErrorFromFile(t, "testdata/errno524.log")
55+
t.Log(errno524)
56+
qt.Assert(t, errno524.Error(), qt.Contains, "JIT doesn't support bpf-to-bpf calls")
57+
qt.Assert(t, errno524.Error(), qt.Not(qt.Contains), "processed 39 insns")
58+
59+
invalidMember := readErrorFromFile(t, "testdata/invalid-member.log")
60+
t.Log(invalidMember)
61+
qt.Assert(t, invalidMember.Error(), qt.Contains, "STRUCT task_struct size=7744 vlen=218: cpus_mask type_id=109 bitfield_size=0 bits_offset=7744 Invalid member")
62+
63+
issue43 := readErrorFromFile(t, "testdata/issue-43.log")
64+
t.Log(issue43)
65+
qt.Assert(t, issue43.Error(), qt.Contains, "[11] FUNC helper_func2 type_id=10 vlen != 0")
66+
qt.Assert(t, issue43.Error(), qt.Not(qt.Contains), "[10] FUNC_PROTO (anon) return=3 args=(3 arg)")
67+
68+
truncated := readErrorFromFile(t, "testdata/truncated.log")
69+
t.Log(truncated)
70+
qt.Assert(t, truncated.Truncated, qt.IsTrue)
71+
qt.Assert(t, truncated.Error(), qt.Contains, "str_off: 3166088: str_len: 228")
72+
}
73+
74+
func ExampleVerifierError() {
75+
err := &VerifierError{
76+
syscall.ENOSPC,
77+
[]string{"first", "second", "third"},
78+
false,
79+
}
80+
81+
fmt.Printf("With %%s: %s\n", err)
82+
err.Truncated = true
83+
fmt.Printf("With %%v and a truncated log: %v\n", err)
84+
fmt.Printf("All log lines: %+v\n", err)
85+
fmt.Printf("First line: %+1v\n", err)
86+
fmt.Printf("Last two lines: %-2v\n", err)
87+
88+
// Output: With %s: no space left on device: third (2 line(s) omitted)
89+
// With %v and a truncated log: no space left on device: second: third (truncated, 1 line(s) omitted)
90+
// All log lines: no space left on device:
91+
// first
92+
// second
93+
// third
94+
// (truncated)
95+
// First line: no space left on device:
96+
// first
97+
// (2 line(s) omitted)
98+
// (truncated)
99+
// Last two lines: no space left on device:
100+
// (1 line(s) omitted)
101+
// second
102+
// third
103+
// (truncated)
104+
}
105+
106+
func readErrorFromFile(tb testing.TB, file string) *VerifierError {
107+
tb.Helper()
108+
109+
contents, err := os.ReadFile(file)
110+
if err != nil {
111+
tb.Fatal("Read file:", err)
112+
}
113+
114+
return ErrorWithLog(unix.EINVAL, contents)
115+
}

internal/testdata/errno524.log

136 Bytes
Binary file not shown.

internal/testdata/invalid-member.log

915 Bytes
Binary file not shown.

internal/testdata/issue-43.log

789 Bytes
Binary file not shown.

internal/testdata/truncated.log

108 Bytes
Binary file not shown.

0 commit comments

Comments
 (0)