Skip to content

Shrink the wc_GetErrorString() lookup table - #11861

Open
michael-membrowse wants to merge 3 commits into
wolfSSL:masterfrom
michael-membrowse:error-string-table
Open

michael-membrowse wants to merge 3 commits into
wolfSSL:masterfrom
michael-membrowse:error-string-table

Conversation

@michael-membrowse

@michael-membrowse michael-membrowse commented Oct 22, 2026 •

Copy link
Copy Markdown
Contributor

wolfCrypt error codes sit in two spans, -97..-300 and -1000..-1999. The switch in wc_GetErrorString() covers both, so compilers that lower it to a lookup table (GCC -O2/-Os, clang) build one table from 0 to the last span 2 code, with an entry for each of the ~800 unused codes, most of them between the spans.

Move span 2 to just below span 1 before the switch, so the table covers one dense range. Output is unchanged for every input. Error code values are not changed; the remapping is internal to the function. On Cortex-M4 with arm-none-eabi-gcc -Os, the table shrinks from 4172 to 1376 bytes and error.o from 12911 to 10135 bytes.

The switch now runs on remapped values, so the enum cast that lets -Wswitch-enum catch a code without a string moves to a new, never called wc_ErrorStringsCheck() that lists every code. error_test() still checks that every code in both spans has a string.

ZD 22676

Copilot AI balanced review requested due to automatic review settings October 22, 2026 12:07
@wolfSSL-Bot

Copy link
Copy Markdown

Can one of the admins verify this patch?

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟢 Approval recommended

The remapping preserves error-string selection and boundary handling, with existing tests covering both spans and unknown-code ranges.

No open issues 🎉

What changed in this PR

This PR reduces the memory footprint of wolfCrypt’s error-string lookup by internally remapping the second error-code span into a compact range, without changing public error codes or returned strings.

Changes:

  • Remaps span 2 before the lookup switch to reduce compiler-generated table size.
  • Adds an unused enum switch to retain compiler warnings for missing error-code cases.
  • Shares the unknown-error string across fallback paths.
File Description
wolfcrypt/​src/​error.c Compacts error-string lookup while preserving enum coverage checks and unknown-code handling.

Review effort: Balanced

@LinuxJedi

Copy link
Copy Markdown
Member

Hey @michael-membrowse, are you able and willing to sign a contributor agreement so that we can get things moving?

Details on how to do it are here, but basically you need to email support@wolfssl.com referencing this pull request and the team will help you out: https://github.com/wolfSSL/wolfssl/blob/master/CONTRIBUTING.md#contributor-agreement-required

@LinuxJedi LinuxJedi self-assigned this Oct 22, 2026
@dgarske

dgarske commented Oct 23, 2026

Copy link
Copy Markdown
Member

Okay to test. Contributor agreement "In progress". Let's see what our CI does with this optimization. Thank you @michael-membrowse

@anhu

anhu commented Oct 27, 2026

Copy link
Copy Markdown
Member

Contributor Agreement has been approved. Please move forward with this review.

@dgarske dgarske self-assigned this Oct 27, 2026
@dgarske
dgarske self-requested a review October 27, 2026 18:59
@dgarske

dgarske commented Oct 27, 2026

Copy link
Copy Markdown
Member

Hi @michael-membrowse I am reviewing this. For some reason I can't mark the PR ready for review, so would you do that when its "ready"? Thank you

@michael-membrowse

Copy link
Copy Markdown
Contributor Author

Hi, @dgarske. I have another PR open (MemBrowse action version bump).
It doesn't let me to set this one to "ready" state because of that.

@dgarske

dgarske commented Oct 27, 2026

Copy link
Copy Markdown
Member

Hi @michael-membrowse ,

Nice find on the sparse range, and the output is provably unchanged. I linked both versions into one binary under different symbol names and compared strcmp output for every int32 value: 0 mismatches. But I do not think the patch is getting the win it describes, and on some targets it is a regression.

error.o size (.text + .rodata + .rodata.str1.1), same user_settings.h for every column, arm-none-eabi-gcc 13.2.1 / gcc 13.3.0 / clang 18.1.3:

target / flags master PR delta
cortex-m4 gcc -Os 12913 11429 -1484
cortex-m0+ gcc -Os 12917 11449 -1468
cortex-a9 -marm gcc -Os 12933 13137 +204
cortex-a7 -marm gcc -Os 12933 13137 +204
powerpc gcc -Os 12961 13177 +216
aarch64 gcc -Os 12505 12469 -36
x86_64 gcc -Os 12166 12151 -15
x86_64 gcc -O2 15470 12722 -2748
x86_64 clang -O2 14900 10171 -4729

The table does not shrink from 4172 to 1376 on GCC 13, it disappears, and is replaced by more code. On master, tree-switch-conversion lowers the whole function to one indexed load from a CSWTCH array of string pointers (60 bytes of .text plus the 4172-byte array). On this branch the pre-switch if that rewrites error makes that pass bail out:

Bailing out - no common successor to all case label target blocks found

so the switch falls through to switchlower, which emits a jump table plus one block per case. That is 2748 bytes of .text on Thumb-2 -- cheaper than the 4172-byte array, so a net win there -- but 4456 bytes in ARM mode, which is more than the array it replaced. Hence the +204 on cortex-a7/a9 and +216 on 32-bit PowerPC.

Moving the switch into its own function, so the pass sees a body that is nothing but the switch, recovers it:

static const char* wc_GetErrorStringIdx(int error)
{
    switch (error) {
    /* ... body unchanged ... */
    default:
        return wc_unknown_error_str;
    }
}

WOLFSSL_ABI
const char* wc_GetErrorString(int error)
{
    if ((error <= WC_SPAN2_FIRST_E) && (error >= WC_SPAN2_MIN_CODE_E)) {
        error = WC_SPAN2_TO_IDX(error);
    }
    else if (error < WC_SPAN1_MIN_CODE_E) {
        return wc_unknown_error_str;
    }
    return wc_GetErrorStringIdx(error);
}

With that, Cortex-M4 -Os becomes 80 bytes of .text plus a 1376-byte .rodata table (exactly the table the description promises) for -2773 instead of -1484, and the ARM-mode and PowerPC regressions become savings of about 2.7 KB each. It costs ~30 bytes versus this branch on x86_64 gcc and is identical on clang. That probably also explains the 10135 in the description: it is what you get when switch conversion survives, which it does on some GCC versions and not on 13.2.

Thanks,
David Garske, wolfSSL

@michael-membrowse

michael-membrowse commented Oct 28, 2026 •

Copy link
Copy Markdown
Contributor Author

Hi @dgarske,

Thanks for your detailed review.
My measurement came from using an older gcc, which produced a different result than yours.

I applied your suggestion of moving the switch to a separate function.

here are the updated results with your suggestion:

target / flags master PR delta
cortex-m4 gcc -Os 12911 10135 −2776
cortex-m0+ gcc -Os 12915 10155 −2760
cortex-a9 -marm gcc -Os 12933 10173 −2760
cortex-a7 -marm gcc -Os 12933 10173 −2760
powerpc gcc -Os 12961 10201 −2760
aarch64 gcc -Os 12505 12469 −36
x86_64 gcc -Os 12166 12151 −15
x86_64 gcc -O2 15462 12752 −2710
x86_64 clang -O2 14898 10156 −4742

@michael-membrowse
michael-membrowse marked this pull request as ready for review October 28, 2026 15:50
@github-actions

Copy link
Copy Markdown

retest this please

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 28, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ⚠️ Failed 2026-10-28T15:53:35.799071Z
We couldn’t complete this review. Comment @codex review to try again.
f5d8a6f Draft marked ready
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@dgarske

dgarske commented Oct 28, 2026

Copy link
Copy Markdown
Member

Jenkins retest this please. FIPS test "cmd" missing (not related to this PR)

@github-actions

github-actions Bot commented Oct 28, 2026 •

Copy link
Copy Markdown

MemBrowse Memory Report · Explore what changed →

gcc-arm-cortex-m4

  • FLASH: -2,796 B (-1.3%, 207,152 B / 262,144 B, total: 79% used)

gcc-arm-cortex-m4-crypto-only

  • FLASH: -2,796 B (-1.5%, 179,795 B / 262,144 B, total: 69% used)

gcc-arm-cortex-m4-openssl-compat

  • FLASH: .rodata -2,768 B (-0.3%, 793,436 B / 1,048,576 B, total: 76% used)

gcc-arm-cortex-m4-pkcs7

  • FLASH: .text +64 B (-1.2%, 220,820 B / 262,144 B, total: 84% used)

gcc-arm-cortex-m4-pq

  • FLASH: .rodata -2,772 B, .text +64 B (-0.9%, 309,008 B / 1,048,576 B, total: 29% used)

gcc-arm-cortex-m4-rsa-only

  • FLASH: .rodata -2,768 B (-0.8%, 340,360 B / 1,048,576 B, total: 32% used)

gcc-arm-cortex-m4-tls13

  • FLASH: .text +64 B (-1.1%, 246,498 B / 262,144 B, total: 94% used)

gcc-arm-cortex-m7

  • FLASH: -2,796 B (-1.3%, 207,152 B / 262,144 B, total: 79% used)

gcc-arm-cortex-m7-pq

  • FLASH: .rodata -2,772 B (-0.9%, 309,904 B / 1,048,576 B, total: 30% used)

gcc-arm-cortex-m7-tls13

  • FLASH: -2,796 B (-1.1%, 246,498 B / 262,144 B, total: 94% used)

stm32-sim-stm32h753

  • FLASH: .text -2,732 B (-1.3%, 202,988 B / 2,097,152 B, total: 10% used)

zephyr-frdm_rw612

  • FLASH: log_const_area -8 B, rodata -8 B, text +48 B (+0.0%, 282,496 B / 67,108,864 B, total: 0% used)

No memory changes detected for:


Catch memory regressions in CI with MemBrowse. Free for open source.

Comment thread wolfcrypt/src/error.c Outdated
Comment thread wolfcrypt/src/error.c Outdated
Comment thread wolfcrypt/src/error.c
wolfCrypt error codes sit in two spans, -97..-300 and -1000..-1999. The
switch in wc_GetErrorString() covers both, so compilers that lower it to a
lookup table (GCC -O2/-Os, clang) build one table from 0 to the last span 2
code, with an entry for each of the ~800 unused codes, most of them between
the spans.

Move span 2 to just below span 1 before the switch, so the table covers one
dense range. Output is unchanged for every input. Error code values are not
changed; the remapping is internal to the function. On Cortex-M4 with
arm-none-eabi-gcc -Os, the table shrinks from 4172 to 1376 bytes and
error.o from 12911 to 10135 bytes.

The switch now runs on remapped values, so the enum cast that lets
-Wswitch-enum catch a code without a string moves to a new, never called
wc_ErrorStringsCheck() that lists every code. error_test() still checks
that every code in both spans has a string.
GCC 12 and later stop turning the switch into a lookup table when the
span 2 remap comes before it in the same function. They emit a jump
table and one block per case instead, which saves less on Thumb and
grows the code in ARM mode and on 32-bit PowerPC.

Move the switch into wc_GetErrorStringIdx(), so it compiles to the
small table again on every compiler measured. Output is unchanged.
Add a static assert that span 2 starts below span 1, which the remap
relies on. Move wc_ErrorStringsCheck() below wc_GetErrorStringIdx(), so
a -Wswitch-enum warning for a new code points past the switch that also
needs its string. Fix a comment that named the wrong function.
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.

6 participants