Skip to content

Commit faa87c7

Browse files
committed
AVR: Bump simavr RAM to 63.5K to fix ML-KEM-1024 stack overflow
The test vectors added in the last commit lead to a stack overflow on AVR for ML-KEM-1024. This commit ports the AVR setup from mldsa-native, including its increase in RAM to nearly 64K. This fixes the AVR test, and also makes future maintenance of the AVR backend across mlkem-native and mldsa-native simpler. - nix/avr: RAMEND 0x81FF -> 0xFFFF, EEPROM E2END 0x3FFF -> 0x7FFF - platform.mk: __stack=0x81FF -> __DATA_REGION_LENGTH__=0xFC00, so .data/.bss grow up from 0x0200 with the stack set at runtime - avr_wrapper.c / init7.S / exec_wrapper.py: place the argc/argv block at the top of RAM and set SP just below it, giving the largest possible stack Keeping this identical (modulo naming) to mldsa-native lets both projects share one AVR baremetal harness, so fixes port across as a prefix diff. Signed-off-by: Hanno Becker <beckphan@amazon.co.uk>
1 parent 1f3f0cd commit faa87c7

10 files changed

Lines changed: 84 additions & 38 deletions

File tree

.github/workflows/baremetal.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ jobs:
1717
matrix:
1818
target:
1919
- runner: ubuntu-latest
20-
name: 'AVR ATmega128RFR2 (modified for 32K RAM)'
20+
name: 'AVR ATmega128RFR2 (modified for 63.5K RAM)'
2121
makefile: test/baremetal/platform/avr/platform.mk
2222
nix-shell: cross-avr
2323
func: true

nix/avr/default.nix

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,9 @@ let
77
# Patched simavr with increased RAM and fixed UART output
88
simavr-patched = pkgs.simavr.overrideAttrs (oldAttrs: {
99
patches = (oldAttrs.patches or [ ]) ++ [
10-
./simavr-32kb-ram.patch
10+
./simavr-64kb-ram.patch
1111
./simavr-uart-output-fix.patch
12-
./simavr-16k-eeprom.patch
12+
./simavr-32k-eeprom.patch
1313
# Exit-code commands (SIMAVR_CMD_EXIT_CODE_*), not yet in a release
1414
(pkgs.fetchpatch {
1515
url = "https://github.com/buserror/simavr/commit/c9354b32e057e409c2fbc9454e26db3b3103c26a.patch";
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,4 +6,4 @@ index 1234567..abcdefg 100644
66
+++ b/simavr/cores/avr/iom128rfr2.h
77
@@ -1,1 +1,1 @@
88
-#define E2END (0xFFF)
9-
+#define E2END (0x3FFF)
9+
+#define E2END (0x7FFF)
Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,8 @@
88
#define RAMSTART (0x200)
99
-#define RAMSIZE (0x4000)
1010
-#define RAMEND (0x41FF)
11-
+#define RAMSIZE (0x8000)
12-
+#define RAMEND (0x81FF)
11+
+#define RAMSIZE (0xFE00)
12+
+#define RAMEND (0xFFFF)
1313
#define XRAMSTART (0x0000)
1414
#define XRAMSIZE (0x0000)
1515
#define XRAMEND RAMEND

test/baremetal/platform/avr/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,4 +6,4 @@ This directory provides a baremetal build and test environment for AVR MCUs, usi
66

77
This is primarily a vehicle to test that mlkem-native builds and is functionally correct in 16-bit C implementations. For actual practical use on 16-bit MCUs, stack usage would need to be reduced.
88

9-
**Note:** We currently need 32K of RAM, more than any MCU supported by `simavr`; we therefore use a patched version of `simavr` where Atmega128rfr2 is given 32K of RAM. To test this, you must work in the `nix .#avr` shell specified in nix flake.
9+
**Note:** We currently need close to the full 64K data address space of the AVR architecture, more than any MCU supported by `simavr`; we therefore use a patched version of `simavr` where Atmega128rfr2 is given 63.5K of RAM (0x0200-0xFFFF). To test this, you must work in the `nix .#cross-avr` shell specified in nix flake.

test/baremetal/platform/avr/avr_wrapper.c

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,17 @@
1414
/* Register for sending commands (e.g. exit codes) to simavr */
1515
AVR_MCU_SIMAVR_COMMAND(&GPIOR0);
1616

17-
#define RAM_BASE 0x2000
17+
/* The argc/argv block is placed at the top of RAM, just below 16 bytes of
18+
* scratch stack used during startup. The exec wrapper chooses the base
19+
* address RAM_TOP - blocksize and stores it in the first two bytes of
20+
* EEPROM, followed by the block itself. The stack grows downwards from
21+
* just below the block, so binaries with little argument data
22+
* automatically get the largest possible stack. */
23+
#define RAM_TOP 0xFFF0
24+
25+
/* Base address of the argc/argv block, read from EEPROM. Used by the
26+
* argc/argv register setup in init7.S. */
27+
uint16_t mlk_argv_base;
1828

1929
static int uart_putchar(char c, FILE *stream)
2030
{
@@ -27,11 +37,22 @@ static int uart_putchar(char c, FILE *stream)
2737
/* Set up stdout stream for avr-libc printf */
2838
static FILE mystdout = FDEV_SETUP_STREAM(uart_putchar, NULL, _FDEV_SETUP_WRITE);
2939

30-
/* Init6 function - copy EEPROM to RAM */
40+
/* Init6 function - copy argc/argv block from EEPROM to top of RAM and
41+
* point the stack just below it. The scratch stack above RAM_TOP is used
42+
* for the eeprom_read_* calls; the default stack location (RAMEND of the
43+
* unpatched MCU) may lie inside .data/.bss and must not be used. */
3144
void setup_args(void) __attribute__((naked, section(".init6"), used));
3245
void setup_args(void)
3346
{
34-
eeprom_read_block((void *)RAM_BASE, (void *)0x0000, 0x4000);
47+
uint16_t base;
48+
SPH = 0xFF;
49+
SPL = 0xFF;
50+
base = eeprom_read_word((const uint16_t *)0);
51+
eeprom_read_block((void *)base, (const void *)2, (size_t)(RAM_TOP - base));
52+
mlk_argv_base = base;
53+
base--;
54+
SPH = (uint8_t)(base >> 8);
55+
SPL = (uint8_t)(base & 0xFF);
3556
}
3657

3758
/* This is run as part of the init sequence, after setting up the stack

test/baremetal/platform/avr/exec_wrapper.py

Lines changed: 32 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,14 @@
77
import os
88
import tempfile
99

10+
# The argc/argv block is placed at the top of RAM, just below 16 bytes of
11+
# scratch stack used during startup (see avr_wrapper.c). The stack grows
12+
# downwards from just below the block.
13+
RAM_TOP = 0xFFF0
14+
15+
# Patched EEPROM size (see nix/avr/simavr-32k-eeprom.patch)
16+
EEPROM_SIZE = 0x8000
17+
1018

1119
def intel_hex_line(addr, data):
1220
"""Generate Intel HEX format line"""
@@ -25,6 +33,10 @@ def intel_hex_line(addr, data):
2533
def create_eeprom_hex(args, output_file):
2634
"""
2735
Create EEPROM hex file from command line arguments.
36+
37+
EEPROM layout: 2 bytes block base address (little-endian), followed by
38+
the argc/argv block to be copied to that address:
39+
argc (2 bytes) + argv array (len(args) * 2 bytes) + packed strings.
2840
"""
2941
# First arg should be binary name (strip path)
3042
args = [os.path.basename(args[0])] + args[1:]
@@ -38,15 +50,23 @@ def create_eeprom_hex(args, output_file):
3850
strings_data.extend(arg.encode("utf-8"))
3951
strings_data.append(0x00) # Null terminator
4052

41-
# Step 2: Calculate where strings will be in RAM
42-
# Layout: argc (2 bytes) + argv array (len(args) * 2 bytes) + strings
53+
# Step 2: Calculate where the block will be in RAM
4354
argc_size = 2
4455
argv_size = len(args) * 2
45-
strings_ram_base = 0x2000 + argc_size + argv_size
56+
block_size = argc_size + argv_size + len(strings_data)
57+
if block_size + 2 > EEPROM_SIZE:
58+
print(
59+
f"Error: argument block of {block_size} bytes does not fit in EEPROM",
60+
file=sys.stderr,
61+
)
62+
sys.exit(1)
63+
base = RAM_TOP - block_size
64+
strings_ram_base = base + argc_size + argv_size
4665

47-
# Step 3: Build data starting with argc
66+
# Step 3: Build EEPROM data: block base address, then argc
4867
data = bytearray()
49-
data.extend([len(args) & 0xFF, (len(args) >> 8) & 0xFF]) # argc (little-endian)
68+
data.extend([base & 0xFF, (base >> 8) & 0xFF])
69+
data.extend([len(args) & 0xFF, (len(args) >> 8) & 0xFF])
5070

5171
# Step 4: Build argv array with pointers to RAM addresses
5272
for offset in string_offsets:
@@ -96,7 +116,7 @@ def main():
96116

97117
# Run with simavr - enable UART output
98118
# Note that we use a patched version of simavr where atmega128rfr2
99-
# has 32K of RAM. This is purely for testing purposes.
119+
# has 63.5K of RAM. This is purely for testing purposes.
100120
cmd = [
101121
"simavr",
102122
"-m",
@@ -126,12 +146,12 @@ def main():
126146
filtered_lines.append(clean_line)
127147
output = "\n".join(filtered_lines)
128148

129-
# simavr does not propagate the guest exit code (returncode is always
130-
# 0), so a guest-side failure would otherwise be reported as success.
131-
# As a stopgap until simavr's exit-code mechanism is backported (#1728),
132-
# scan the output for "ERROR" and fail instead. This catches both
133-
# the UBSan-trap abort() in avr_wrapper.c and the CHECK(...) macros in
134-
# the tests, which print "ERROR (file,line)" on failure.
149+
# The guest exit code is propagated via simavr's exit-code commands
150+
# (see avr_wrapper.c). As an additional safety net, scan the output
151+
# for "ERROR" and fail: this catches failures where the firmware
152+
# stops without reaching exit(), and both the UBSan-trap abort() in
153+
# avr_wrapper.c and the CHECK(...) macros in the tests print
154+
# "ERROR (file,line)" on failure.
135155
#
136156
# On failure, write to stderr, otherwise stdout.
137157
if "ERROR" in output:

test/baremetal/platform/avr/init7.S

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,16 +7,18 @@
77
/* AVR calling convention for main(int argc, char** argv):
88
* - argc (int, 16-bit) goes in r24:r25 (r24=low, r25=high)
99
* - argv (char**, 16-bit pointer) goes in r22:r23 (r22=low, r23=high)
10+
*
11+
* The argc/argv block was copied to mlk_argv_base by setup_args (init6):
12+
* argc at mlk_argv_base, argv array at mlk_argv_base + 2.
1013
*/
1114

12-
/* Load argc from 0x2000 */
13-
ldi r30, 0x00
14-
ldi r31, 0x20
15+
lds r30, mlk_argv_base
16+
lds r31, mlk_argv_base+1
1517
ld r24, Z
1618
ldi r25, 0
1719

18-
/* Set argv = 0x2002 */
19-
ldi r22, lo8(0x2002)
20-
ldi r23, hi8(0x2002)
20+
movw r22, r30
21+
subi r22, lo8(-2)
22+
sbci r23, hi8(-2)
2123

2224
/* Fall through to init8 - no ret instruction */

test/baremetal/platform/avr/platform.mk

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,10 @@ CROSS_PREFIX=avr-
77
CC=gcc
88

99
# AVR target configuration
10-
# We would need ATMega256rfr2 with 32K RAM, but it's not supported by simavr.
11-
# We instead use ATMega128rfr1 with 16K RAM, but modify its specification in
12-
# simavr to bump the RAM to 32K.
13-
# Once simavr supports ATMega256rfr2, or once mlkem-native has [an option for]
14-
# lower stack usage, this should be changed.
10+
# ML-KEM-1024 (together with the RAM-resident test vectors) needs more than
11+
# the 16K RAM of any MCU supported by simavr, so we use ATMega128rfr2 with its
12+
# RAM specification in simavr bumped from 16K to the maximal 63.5K
13+
# (0x0200-0xFFFF).
1514
AVR_MCU ?= atmega128rfr2
1615
AVR_FREQ ?= 16000000UL
1716

@@ -35,12 +34,16 @@ CFLAGS += \
3534

3635
CFLAGS += $(CFLAGS_EXTRA)
3736

38-
# Non-standard stack end: 0x81FF = 0x200 + 8K instead of default 0x41FF
37+
# Memory layout (data address space 0x0200-0xFFFF):
38+
# - .data/.bss grow upwards from 0x0200 (data region length raised from the
39+
# default 16K to the full RAM size)
40+
# - the argc/argv block sits at the top of RAM, with the stack growing
41+
# downwards from just below it (set up at runtime, see avr_wrapper.c)
3942
LDFLAGS += \
4043
-mmcu=$(AVR_MCU) \
4144
-Wl,--gc-sections \
4245
-Wl,--relax \
43-
-Wl,--defsym=__stack=0x81FF \
46+
-Wl,--defsym=__DATA_REGION_LENGTH__=0xFC00 \
4447
-Wl,--undefined=_simavr_command_register \
4548
-Wl,--section-start=.mmcu=0x910000 \
4649
-lprintf_min

test/src/test_mlkem.c

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -30,9 +30,9 @@
3030

3131
#if !defined(MLK_CONFIG_NO_KEYPAIR_API) && \
3232
!defined(MLK_CONFIG_NO_ENCAPS_API) && !defined(MLK_CONFIG_NO_DECAPS_API)
33-
/* Force out-of-line so the ~6.4KB of stack buffers below stay in short-lived
34-
* frames and don't stack up in main() -- crucial on memory-constrained
35-
* targets such as AVR (32K RAM). */
33+
/* Forbid inlining so stack buffers below stay in short-lived
34+
* frames and don't stack up in main(). This helps testing on
35+
* memory-constrained targets such as AVR. */
3636
static MLK_NOINLINE int test_keys_core(uint8_t pk[CRYPTO_PUBLICKEYBYTES],
3737
uint8_t sk[CRYPTO_SECRETKEYBYTES],
3838
uint8_t ct[CRYPTO_CIPHERTEXTBYTES],

0 commit comments

Comments
 (0)