-
Notifications
You must be signed in to change notification settings - Fork 1.4k
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
server+util: Limit max request sizes, prealloc request buffers #6868
server+util: Limit max request sizes, prealloc request buffers #6868
Conversation
✅ Deploy Preview for openpolicyagent ready!
To edit notification comments on pull requests, go to your Netlify site configuration. |
1 GB gzipped input POST benchmarkSummary:
CLI command for request: Results from
|
ℹ️ I discovered after putting up the draft PR that using a |
88acc1a
to
2f79bcb
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This looks great @philipaconrad! Thanks for the detailed issue description. Few initial thoughts 👇
return r, nil, err | ||
} | ||
rawBody, err = io.ReadAll(plaintextBody) | ||
rawBody, err = util.ReadMaybeCompressedBody(r) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This would be backwards-incompatible. It would be good to avoid this.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Probably we can define a new method.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@ashutosh-narkar Could you elaborate on the backwards-incompatible bits?
As far as I can tell, the updated util.ReadMaybeCompressedBody
method produces the same end result types as before, and only generates new errors if the gzip payload is larger than the configured size limit. 🤔
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The changes look fine to me. Few more comments and we can then get this in. Thanks!
return r, nil, err | ||
} | ||
rawBody, err = io.ReadAll(plaintextBody) | ||
rawBody, err = util.ReadMaybeCompressedBody(r) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Probably we can define a new method.
Locust Max RPS benchmarks (plain, gzip)Summary:
CLI command for each benchmark:
Benchmarking scripts: Results from
|
ℹ️ @ashutosh-narkar I'm going to squash down my changes and rebase off If you have suggestions/edits for the Changelog notes, I'm happy to amend them as needed! 🙂 |
618906d
to
91d12c5
Compare
91d12c5
to
db57672
Compare
|
db57672
to
01cecba
Compare
This commit introduces a few major changes: - (Breaking change) Limits now exist for maximum request body sizes. - Buffers are preallocated for reading request bodies. - Buffers are preallocated for decompressing request bodies. - Gzip decoder instances are reused in a `sync.Pool` across requests. The effect on garbage collection is dramatically fewer GC pauses, giving a roughly 9% RPS improvement in load tests with gzipped request bodies. For larger request sizes, the number of GC pauses is dramatically reduced, although the peak pause time may increase by a few percent. Implementation notes: - The DecodingLimits handler enforces the max request body size both through a Content-Length check, and a MaxBytesReader wrapper around the payload. - The DecodingLimits handler passes the gzip payload size limit down using a context key. Signed-off-by: Philip Conrad <[email protected]>
01cecba
to
6aa1f51
Compare
This commit fixes a request handling bug introduced in open-policy-agent#6868, which caused OPA to treat all incoming chunked requests as if they had zero-length request bodies. The fix detects cases where the request body size is unknown in the DecodingLimits handler, and propagates a request context key down to the `util.ReadMaybeCompressedBody` function, allowing it to correctly select between using the original `io.ReadAll` style for chunked requests, or the newer preallocated buffers approach (for requests of known size). This change has a small, but barely visible performance impact for large requests (<5% increase in GC pauses for a 1GB request JSON blob), and minimal, if any, effect on RPS under load. Fixes: open-policy-agent#6904 Signed-off-by: Philip Conrad <[email protected]>
This commit fixes a request handling bug introduced in open-policy-agent#6868, which caused OPA to treat all incoming chunked requests as if they had zero-length request bodies. The fix detects cases where the request body size is unknown in the DecodingLimits handler, and propagates a request context key down to the `util.ReadMaybeCompressedBody` function, allowing it to correctly select between using the original `io.ReadAll` style for chunked requests, or the newer preallocated buffers approach (for requests of known size). This change has a small, but barely visible performance impact for large requests (<5% increase in GC pauses for a 1GB request JSON blob), and minimal, if any, effect on RPS under load. Fixes: open-policy-agent#6904 Signed-off-by: Philip Conrad <[email protected]>
This commit fixes a request handling bug introduced in open-policy-agent#6868, which caused OPA to treat all incoming chunked requests as if they had zero-length request bodies. The fix detects cases where the request body size is unknown in the DecodingLimits handler, and propagates a request context key down to the `util.ReadMaybeCompressedBody` function, allowing it to correctly select between using the original `io.ReadAll` style for chunked requests, or the newer preallocated buffers approach (for requests of known size). This change has a small, but barely visible performance impact for large requests (<5% increase in GC pauses for a 1GB request JSON blob), and minimal, if any, effect on RPS under load. Fixes: open-policy-agent#6904 Signed-off-by: Philip Conrad <[email protected]>
This commit fixes a request handling bug introduced in #6868, which caused OPA to treat all incoming chunked requests as if they had zero-length request bodies. The fix detects cases where the request body size is unknown in the DecodingLimits handler, and propagates a request context key down to the `util.ReadMaybeCompressedBody` function, allowing it to correctly select between using the original `io.ReadAll` style for chunked requests, or the newer preallocated buffers approach (for requests of known size). This change has a small, but barely visible performance impact for large requests (<5% increase in GC pauses for a 1GB request JSON blob), and minimal, if any, effect on RPS under load. Fixes: #6904 Signed-off-by: Philip Conrad <[email protected]>
…ent#6906) This commit fixes a request handling bug introduced in open-policy-agent#6868, which caused OPA to treat all incoming chunked requests as if they had zero-length request bodies. The fix detects cases where the request body size is unknown in the DecodingLimits handler, and propagates a request context key down to the `util.ReadMaybeCompressedBody` function, allowing it to correctly select between using the original `io.ReadAll` style for chunked requests, or the newer preallocated buffers approach (for requests of known size). This change has a small, but barely visible performance impact for large requests (<5% increase in GC pauses for a 1GB request JSON blob), and minimal, if any, effect on RPS under load. Fixes: open-policy-agent#6904 Signed-off-by: Philip Conrad <[email protected]> (cherry picked from commit ee9ab0b)
This commit fixes a request handling bug introduced in #6868, which caused OPA to treat all incoming chunked requests as if they had zero-length request bodies. The fix detects cases where the request body size is unknown in the DecodingLimits handler, and propagates a request context key down to the `util.ReadMaybeCompressedBody` function, allowing it to correctly select between using the original `io.ReadAll` style for chunked requests, or the newer preallocated buffers approach (for requests of known size). This change has a small, but barely visible performance impact for large requests (<5% increase in GC pauses for a 1GB request JSON blob), and minimal, if any, effect on RPS under load. Fixes: #6904 Signed-off-by: Philip Conrad <[email protected]> (cherry picked from commit ee9ab0b)
Why the changes in this PR are needed?
In a recent bugfix PR, I discovered some performance issues around how request bodies are handled in OPA, particularly when the request is gzipped. This PR is a follow-up to that one, and changes how OPA reads request bodies.
The main performance benefits of this PR come from reducing the workload of the Golang GC, and also by removing redundant
Read
operations on the request body.What are the changes in this PR?
This PR preallocates the entire buffer for a request body (and also for its decompressed payload) when possible. This reduces max RSS for large requests by around 5-10%, and reduces GC overhead by 33-66% for large requests, while not penalizing small (<1kb) requests.
The changes are structured as follows:
docs/
: Updated for the new config options.plugins/server/decoding
: New always-on plugin, modeled after theserver/encoding
plugin.server/server.go
: Integrates the new request handler + uses the request body reading function.util/read_gzip_body.go
: Allocs whole buffers up-front now, and checks gzip blob sizes before decompressing.Other small changes were needed in a variety of spots to account for new method signatures, and so on.
New configuration options:
server.decoding
This PR introduces two new server config fields under
server.decoding
. Here's an example configuration file:These fields keep in line with the naming scheme from
server.encoding
, but I'm open to other ideas! 😅Defending against huge requests
On
main
currently, an attacker can tie up a huge amount of resources for an OPA instance with a large, malicious request. Gzipped payloads amplify the resources that can ultimately be tied up. (Example: a 1 GB JSON object with repetitive values can compress down to 5 MB or smaller with gzip.)In this PR, buffers are allocated all-at-once, which is hazardous-- an attacker could submit a huge request, and cause OPA to experience an out of memory (OOM) error just from allocating the buffers for the request.
In the case of gzip payloads, the raw payload size may be small, but with a forged gzip size trailer field, the attack could cause the buffer allocation logic to believe that the gzip blob will expand to up to 4 GB in size when decompressed! This makes preallocating the decompression buffer a dangerous thing to do without some sort of validation beforehand.
In this PR, the proposed solution is validating the size of incoming request bodies against server-configured limits before allocating any buffers. That forces attackers in both scenarios to have to work a bit harder to do anything nefarious.
Where are the new validation checks?
The defenses against huge requests happen in 2x places:
DecodingLimits
handler in the OPA HTTP server. This allows us to reject those requests before they can significantly tie up an OPA server's resources.util.ReadMaybeCompressedBody
function (used whenever we want to read the request body contents), which checks the gzip blob's 4-byte size trailer field against the server's configured size limits.Notes to assist PR review:
I've moved benchmark sets into comments in the PR discussion thread, since this top PR description was getting a bit crowded. 😅
Scripts used:
summarize-gctrace.awk
geninput.py
Further comments:
Remaining TODO items:
Things I'd love reviewer comments/opinions on: