-
Notifications
You must be signed in to change notification settings - Fork 810
prog: avoid verifier loop of death #1693
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -56,6 +56,12 @@ const minVerifierLogSize = 64 * 1024 | |
| // future, but avoid the unnecessary EINVAL for now. | ||
| const maxVerifierLogSize = math.MaxUint32 >> 2 | ||
|
|
||
| // maxVerifierAttempts is the maximum number of times the verifier will retry | ||
| // loading a program with a growing log buffer before giving up. Since we double | ||
| // the log size on every attempt, this is the absolute maximum number of | ||
| // attempts before the buffer reaches [maxVerifierLogSize]. | ||
| const maxVerifierAttempts = 30 | ||
|
|
||
| // ProgramOptions control loading a program into the kernel. | ||
| type ProgramOptions struct { | ||
| // Bitmap controlling the detail emitted by the kernel's eBPF verifier log. | ||
|
|
@@ -418,59 +424,44 @@ func newProgramWithOptions(spec *ProgramSpec, opts ProgramOptions) (*Program, er | |
| return nil, fmt.Errorf("log level: %w", internal.ErrNotSupportedOnOS) | ||
| } | ||
|
|
||
| // The caller requested a specific verifier log level. Set up the log buffer | ||
| // so that there is a chance of loading the program in a single shot. | ||
| logSize := internal.Between(opts.LogSizeStart, minVerifierLogSize, maxVerifierLogSize) | ||
| var logBuf []byte | ||
| if !opts.LogDisabled && opts.LogLevel != 0 { | ||
| logBuf = make([]byte, logSize) | ||
| attr.LogLevel = opts.LogLevel | ||
| attr.LogSize = uint32(len(logBuf)) | ||
| attr.LogBuf = sys.SlicePointer(logBuf) | ||
| } | ||
|
|
||
| for { | ||
| var fd *sys.FD | ||
| var fd *sys.FD | ||
| if opts.LogDisabled { | ||
| // Loading with logging disabled should never retry. | ||
| fd, err = sys.ProgLoad(attr) | ||
| if err == nil { | ||
| return &Program{unix.ByteSliceToString(logBuf), fd, spec.Name, "", spec.Type}, nil | ||
| return &Program{"", fd, spec.Name, "", spec.Type}, nil | ||
| } | ||
|
|
||
| if opts.LogDisabled { | ||
| break | ||
| } | ||
|
|
||
| if attr.LogTrueSize != 0 && attr.LogSize >= attr.LogTrueSize { | ||
| // The log buffer already has the correct size. | ||
| break | ||
| } else { | ||
| // Only specify log size if log level is also specified. Setting size | ||
| // without level results in EINVAL. Level will be bumped to LogLevelBranch | ||
| // if the first load fails. | ||
| if opts.LogLevel != 0 { | ||
| attr.LogLevel = opts.LogLevel | ||
| attr.LogSize = internal.Between(opts.LogSizeStart, minVerifierLogSize, maxVerifierLogSize) | ||
| } | ||
|
|
||
| if attr.LogSize != 0 && !errors.Is(err, unix.ENOSPC) { | ||
| // Logging is enabled and the error is not ENOSPC, so we can infer | ||
| // that the log buffer is large enough. | ||
| break | ||
| } | ||
| attempts := 1 | ||
| for { | ||
ti-mo marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| if attr.LogLevel != 0 { | ||
| logBuf = make([]byte, attr.LogSize) | ||
| attr.LogBuf = sys.SlicePointer(logBuf) | ||
| } | ||
|
|
||
| if attr.LogLevel == 0 { | ||
| // Logging is not enabled but loading the program failed. Enable | ||
| // basic logging. | ||
| attr.LogLevel = LogLevelBranch | ||
| } | ||
| fd, err = sys.ProgLoad(attr) | ||
| if err == nil { | ||
| return &Program{unix.ByteSliceToString(logBuf), fd, spec.Name, "", spec.Type}, nil | ||
| } | ||
|
|
||
| // Make an educated guess how large the buffer should be by multiplying. | ||
| // Ensure the size doesn't overflow. | ||
| const factor = 2 | ||
| logSize = internal.Between(logSize, minVerifierLogSize, maxVerifierLogSize/factor) | ||
| logSize *= factor | ||
| if !retryLogAttrs(attr, opts.LogSizeStart, err) { | ||
| break | ||
| } | ||
|
|
||
| if attr.LogTrueSize != 0 { | ||
| // The kernel has given us a hint how large the log buffer has to be. | ||
| logSize = attr.LogTrueSize | ||
| if attempts >= maxVerifierAttempts { | ||
| return nil, fmt.Errorf("load program: %w (bug: hit %d verifier attempts)", err, maxVerifierAttempts) | ||
| } | ||
| attempts++ | ||
| } | ||
|
|
||
| logBuf = make([]byte, logSize) | ||
| attr.LogSize = logSize | ||
| attr.LogBuf = sys.SlicePointer(logBuf) | ||
| } | ||
|
|
||
| end := bytes.IndexByte(logBuf, 0) | ||
|
|
@@ -520,6 +511,50 @@ func newProgramWithOptions(spec *ProgramSpec, opts ProgramOptions) (*Program, er | |
| return nil, internal.ErrorWithLog("load program", err, logBuf) | ||
| } | ||
|
|
||
| func retryLogAttrs(attr *sys.ProgLoadAttr, startSize uint32, err error) bool { | ||
| if attr.LogSize == maxVerifierLogSize { | ||
| // Maximum buffer size reached, don't grow or retry. | ||
| return false | ||
| } | ||
|
|
||
| // ENOSPC means the log was enabled on the previous iteration, so we only | ||
| // need to grow the buffer. | ||
| if errors.Is(err, unix.ENOSPC) { | ||
| if attr.LogTrueSize != 0 { | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This doesn't preserve the behaviour that we don't retry if LogSize >= LogTrueSize. I'm also not sure why this only happens on ENOSPC? LogTrueSize should always be populated?
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. LogTrueSize is not available on older kernels
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If the kernel returns ENOSPC, LogSize is smaller than LogTrueSize by definition, and we just need to allocate whatever LogTrueSize specified and retry. I don't think what I wrote here will result in a retry if LogSize > LogTrueSize. What am I missing? |
||
| // Kernel supports LogTrueSize and previous iteration undershot the buffer | ||
| // size. Try again with the given true size. | ||
| attr.LogSize = attr.LogTrueSize | ||
| return true | ||
| } | ||
|
|
||
| // Ensure the size doesn't overflow. | ||
| const factor = 2 | ||
| if attr.LogSize >= maxVerifierLogSize/factor { | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I find this whole factor thing not very intuitive. This is to avoid overflow?
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do you have an alternative suggestion? The intention is for this to keep working if/when maxVerifierLogSize gets bumped to MaxUint32. Currently, we can't rely on a u32 overflow happening, detecting it, and clamping the value to maxVerifierLogSize. Either way, that's more code and more obtuse than this solution. I'll go ahead with the current version for now. |
||
| attr.LogSize = maxVerifierLogSize | ||
| return true | ||
| } | ||
|
|
||
| // Make an educated guess how large the buffer should be by multiplying. Due | ||
| // to int division, this rounds down odd sizes. | ||
| attr.LogSize = internal.Between(attr.LogSize, minVerifierLogSize, maxVerifierLogSize/factor) | ||
| attr.LogSize *= factor | ||
|
|
||
| return true | ||
| } | ||
|
|
||
| if attr.LogLevel == 0 { | ||
| // Loading the program failed, it wasn't a buffer-related error, and the log | ||
| // was disabled the previous iteration. Enable basic logging and retry. | ||
| attr.LogLevel = LogLevelBranch | ||
| attr.LogSize = internal.Between(startSize, minVerifierLogSize, maxVerifierLogSize) | ||
| return true | ||
| } | ||
|
|
||
| // Loading the program failed for a reason other than buffer size and the log | ||
| // was already enabled the previous iteration. Don't retry. | ||
| return false | ||
| } | ||
|
|
||
| // NewProgramFromFD creates a program from a raw fd. | ||
| // | ||
| // You should not use fd after calling this function. | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.