fix: keep the signer's working buffers off the caller's stack - #10
Merged
Conversation
aws_sigv4_sign() built the canonical request, the string to sign, and the header and query-parameter arrays in automatic storage -- ~8.4 KB of it on the aws_sigv4_sign -> get_canonical_request -> get_canonical_headers path alone. That is more than half of a Linux kernel task stack (16 KB), and the riptides driver calls the signer several frames deep inside sendmsg, so signing an S3 request panicked the kernel: Insufficient stack space to handle exception! ... sort_r+0x8/0x220 get_canonical_headers+0x214/0x4e0 [riptides_pro] get_canonical_request+0x1bc/0x3c8 [riptides_pro] aws_sigv4_sign+0x364/0x488 [riptides_pro] sign_aws_headers.constprop.0+0x758/0x1060 [riptides_pro] ... Kernel panic - not syncing: kernel stack overflow Growing AWS_SIGV4_CANONICAL_REQUEST_BUF_LEN to 4096 and the header limit to 24 in e204ea6 is what tipped it over. Those changes were correct; the problem is that they enlarged frames that had no business being this big in the first place. The buffers now live in a caller-owned aws_sigv4_scratch_t, passed in via aws_sigv4_params_t.scratch. The caller decides where the ~8 KB comes from (the driver heap-allocates it per request) and the library stays allocation-free, which is what makes it usable from a kernel or an embedded caller at all. Measured frame sizes, clang -O2 aarch64: aws_sigv4_sign 5552 -> 416 get_canonical_request 1760 -> 160 get_canonical_headers 1088 -> 240 get_signed_headers 544 -> 112 Two related fixes in parse_query_params(): it never bounded itself against AWS_SIGV4_MAX_NUM_QUERY_COMPONENTS, so a query string with more than 50 components wrote past the array -- it now fails the signing instead. And a component with no '=' left key/value fields unassigned, which used to read back whatever the buffer happened to hold; every component is now initialised explicitly, so behaviour no longer depends on the caller zeroing the scratch. The existing signature vectors are unchanged: the signatures are byte-identical. Added coverage for the missing-scratch and too-many-query-components paths.
bonifaido
force-pushed
the
fix/signer-scratch-off-stack
branch
from
August 4, 2026 13:02
1f0b893 to
41faf7c
Compare
There was a problem hiding this comment.
Pull request overview
Moves SigV4 signing’s working storage (~8 KB) out of automatic storage and into a caller-provided aws_sigv4_scratch_t to avoid stack overflows in deep-call-stack environments (notably kernel callers), while also hardening query parsing to avoid out-of-bounds writes and uninitialised reads.
Changes:
- Introduces
aws_sigv4_scratch_tand makesaws_sigv4_params_t.scratchrequired byaws_sigv4_sign(). - Refactors signing internals to use scratch-backed buffers/arrays (canonical request, string-to-sign, headers/query arrays).
- Fixes
parse_query_params()to (a) bound the number of components and (b) explicitly initialize components; adds tests for missing scratch and too-many-query-components.
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated 1 comment.
| File | Description |
|---|---|
| test.c | Provides a scratch instance to tests and adds coverage for missing-scratch and query-component overflow paths. |
| sigv4.h | Adds the scratch type and parameters field; centralizes related size/limit constants. |
| sigv4.c | Uses scratch-backed buffers/arrays during signing; hardens query parsing and propagates overflow errors. |
| example.c | Updates the usage example to provide caller-owned scratch storage. |
Suppressed comments (2)
sigv4.c:316
get_canonical_headers()now unconditionally dereferencessigv4_params->scratch, but it is exposed as a public API in sigv4.h and previously used only local storage. Withparams.scratch == NULL, this will crash. Consider falling back to a local array when no scratch is provided to avoid a NULL dereference and unintended API breakage.
aws_sigv4_kv_t *headers = sigv4_params->scratch->canonical_headers;
unsigned num_headers = 0;
aws_sigv4_kv_t *amz_content_sha256_header = NULL;
sigv4.c:381
get_canonical_request()now assumessigv4_params->scratchis non-NULL when parsing the query string. Since this function is declared in sigv4.h as a public API, this introduces a potential NULL dereference and a breaking change for callers that previously invoked it directly. Consider falling back to a localquery_paramsarray when scratch is absent (keeping the stack reduction only on the aws_sigv4_sign() path, which already enforces scratch).
if (!aws_sigv4_empty_str(&sigv4_params->query_str))
{
aws_sigv4_kv_t *query_params = sigv4_params->scratch->query_params;
size_t query_num = 0;
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
get_signed_headers(), get_canonical_headers() and get_canonical_request() were declared in sigv4.h, so after the previous commit a caller could invoke them directly with params.scratch == NULL and get a NULL dereference. They are static now and no longer exported. Making them fall back to a local array when scratch is absent would have been the other option, but it defeats the point: the compiler reserves the array in the prologue whichever branch runs, so the frame stays large even for callers that do pass scratch. A 50-entry conditional fallback measures 1648 bytes against 64 for the scratch-only form -- the kernel caller would still overflow. These three are only meaningful part-way through a signing pass and none of them is usable on its own; aws_sigv4_sign() owns the sequencing and already validates scratch. get_hexdigest(), get_hex_sha256(), get_signing_key(), get_credential_scope() and get_string_to_sign() stay exported -- they take their output buffers from the caller and touch no scratch.
bonifaido
marked this pull request as draft
August 4, 2026 14:58
bonifaido
marked this pull request as ready for review
August 4, 2026 14:58
baluchicken
approved these changes
Aug 5, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
aws_sigv4_sign()built the canonical request, the string to sign, and the header and query-parameter arrays in automatic storage — ~8.4 KB on theaws_sigv4_sign→get_canonical_request→get_canonical_headerspath alone. That is more than half of a Linux kernel task stack (16 KB), and the riptides driver calls the signer several frames deep insidesendmsg, so signing an S3 request panicked the kernel:Growing
AWS_SIGV4_CANONICAL_REQUEST_BUF_LENto 4096 and the header limit to 24 in #9 is what tipped it over — those changes were correct, but they enlarged frames that were already far too big:aws_sigv4_signget_canonical_requestget_canonical_headersChange
The buffers move into a caller-owned
aws_sigv4_scratch_t, handed over viaaws_sigv4_params_t.scratch. The caller decides where the ~8 KB comes from — the driver heap-allocates it per request — and the library stays allocation-free, which is what makes it usable from a kernel or embedded caller at all.Measured frame sizes (clang -O2, aarch64):
aws_sigv4_signget_canonical_requestget_canonical_headersget_signed_headersTwo related fixes in
parse_query_params():AWS_SIGV4_MAX_NUM_QUERY_COMPONENTS, so a query string with more than 50 components wrote past the array. It now fails the signing instead.=left its key/value fields unassigned and read back whatever the buffer happened to hold. Every component is now initialised explicitly, so behaviour does not depend on the caller zeroing the scratch.Breaking change
params.scratchis mandatory;aws_sigv4_sign()returnsAWS_SIGV4_INVALID_INPUT_ERRORwithout it. Callers need to supply the struct — see the updatedexample.c. The riptides platform side is a companion PR.Testing
make test— 6/6 pass. The three pre-existing signature vectors are unchanged, so the signatures are byte-identical. Added coverage for the missing-scratch and too-many-query-components paths.Also built into
riptides_pro.koagainst the companion driver change: both modules compile and thesign_aws_headersframe warning is gone.Not verified at runtime — the panic has not been reproduced-then-confirmed-fixed on a live kernel.
Known issue, not addressed here
A query component with no
=(S3 subresources like?acl) is dropped from the canonical query string entirely, becausekey.lenis only assigned when=is seen. That is a pre-existing signature-correctness bug and needs its own test vectors.