Skip to content

fix: keep the signer's working buffers off the caller's stack - #10

Merged
bonifaido merged 2 commits into
mainfrom
fix/signer-scratch-off-stack
Aug 5, 2026
Merged

fix: keep the signer's working buffers off the caller's stack#10
bonifaido merged 2 commits into
mainfrom
fix/signer-scratch-off-stack

Conversation

@bonifaido

@bonifaido bonifaido commented Aug 4, 2026

Copy link
Copy Markdown
Member

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 the aws_sigv4_signget_canonical_requestget_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!
Task stack:     [0xffff8000830d8000..0xffff8000830dc000]
...
pc : sort_r+0x8/0x220
  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]
  handle_http1_request.isra.0+0x294/0x580 [riptides_pro]
  riptides_sendmsg+0x80c/0x2228 [riptides]
  ...
Kernel panic - not syncing: kernel stack overflow

Growing AWS_SIGV4_CANONICAL_REQUEST_BUF_LEN to 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:

before #9 after #9
aws_sigv4_sign 4528 5552
get_canonical_request 1744 1760
get_canonical_headers 528 1088
panic-path total 6800 8400

Change

The buffers move into a caller-owned aws_sigv4_scratch_t, handed over 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 embedded caller at all.

Measured frame sizes (clang -O2, aarch64):

before after
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.
  • A component with no = 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.scratch is mandatory; aws_sigv4_sign() returns AWS_SIGV4_INVALID_INPUT_ERROR without it. Callers need to supply the struct — see the updated example.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.ko against the companion driver change: both modules compile and the sign_aws_headers frame 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, because key.len is only assigned when = is seen. That is a pre-existing signature-correctness bug and needs its own test vectors.

@bonifaido bonifaido self-assigned this Aug 4, 2026
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
bonifaido force-pushed the fix/signer-scratch-off-stack branch from 1f0b893 to 41faf7c Compare August 4, 2026 13:02
@bonifaido
bonifaido requested review from a team and Copilot August 4, 2026 13:52

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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_t and makes aws_sigv4_params_t.scratch required by aws_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 dereferences sigv4_params->scratch, but it is exposed as a public API in sigv4.h and previously used only local storage. With params.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 assumes sigv4_params->scratch is 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 local query_params array 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.

Comment thread sigv4.c
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
bonifaido marked this pull request as draft August 4, 2026 14:58
@bonifaido
bonifaido marked this pull request as ready for review August 4, 2026 14:58
@bonifaido
bonifaido merged commit 9a58678 into main Aug 5, 2026
1 check passed
@bonifaido
bonifaido deleted the fix/signer-scratch-off-stack branch August 5, 2026 06:49
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants