Skip to content

[AVR] core::fmt prints out-of-bounds memory on microcontrollers with more than 128KB of program memory #149223

Description

@meithecatte

The reproducer is available ready-to-go at https://codeberg.org/mei-b/farbix, commit 26684d7b9339112b9b4fc08ca7c0d973341ce929. The code itself is as follows:

#![no_std]
#![no_main]

use arduino_hal::{Peripherals, port};

panic_serial::impl_panic_handler!(
  arduino_hal::usart::Usart<
    arduino_hal::pac::USART0,
    port::Pin<port::mode::Input, arduino_hal::hal::port::PE0>,
    port::Pin<port::mode::Output, arduino_hal::hal::port::PE1>
  >
);

#[inline(never)]
fn meow1() {
    meow2()
}

#[inline(never)]
fn meow2() {
    panic!("{}", 2137)
}

#[arduino_hal::entry]
fn main() -> ! {
    let dp = Peripherals::take().unwrap();
    let pins = arduino_hal::pins!(dp);
    let serial = arduino_hal::default_serial!(dp, pins, 57600);
    let serial = share_serial_port_with_panic(serial);
    meow1(); // if bug doesn't reproduce on a newer compiler, try calling
             // meow2 directly instead
    loop {}
}

I compile this with the following settings in .cargo/config.toml:

[build]
target = "avr-none"
rustflags = ["-C", "target-cpu=atmega2560"]

[unstable]
build-std = ["core"]

The dependencies involved are as follows:

[package]
name = "farbix"
version = "0.1.0"
edition = "2024"

[dependencies]

[dependencies.arduino-hal]
git = "https://github.com/Rahix/avr-hal"
features = ["arduino-mega2560"]
# main branch at the time of writing
rev = "6de651a2303ad1f392fc628675af2372632df4fb"

[dependencies.panic-serial]
git = "https://github.com/nilclass/panic-serial"
rev = "409d81c6f5d36d2fc73e4df0e0b0a09e10989b94" # PR #1
features = ["full"]

[profile.release]
panic = "abort"
lto = true
opt-level = "s"

Note that these particular dependencies aren't central to the issue – it's just the easiest way I knew of for creating a self-contained example for what's inherently bare-metal target.

To run this example without physical hardware, clone and compile https://github.com/buserror/simavr, and then run something along the lines of:

cargo build --release && ~/src/simavr/simavr/run_avr -m atmega2560 target/avr-none/release/farbix.elf

Expected result:

Panic at src/main.rs:21:5: 2137

Actual result:

Panic at src/main.rs:21:5: �./home/mei/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/core/src/fmt/num.rs...
Panic at ........"...Y.......................C.�.00010203040506070809101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899called
`Option::unwrap()` on a `None` valueE.n.^[garbage continues]

Meta

rustc --version --verbose:

rustc 1.93.0-nightly (53732d5e0 2025-11-20)
binary: rustc
commit-hash: 53732d5e076329a62f71d3c6901886ce8a71e812
commit-date: 2025-11-20
host: x86_64-unknown-linux-gnu
release: 1.93.0-nightly
LLVM version: 21.1.5

Diagnosis

The breakage bisects to #148789. Moreover, the issue only occurs on the atmega2560, and doesn't reproduce on e.g. the atmega1280. The bug occurs because:

  • the flash memory of the ATmega2560 is big enough that a 16-bit program counter is not enough. Because of this, the return addresses on the stack are 3 bytes long:
    • Compare Section 7.6 "Stack Pointer" of the ATmega640/V-1280/V-1281/V-2560/V-2561/V datasheet

      The Stack Pointer is decremented by one when data is pushed onto the Stack with the PUSH instruction, and it is decremented by two for ATmega640/1280/1281 and three for ATmega2560/2561 when the return address is pushed onto the Stack with subroutine call or interrupt.

  • on AVR, LLVM ignores any alignment requirements passed to alloca, and the generated code never concerns itself with stack alignment
    • normally, this is fine, because the AVR instruction set doesn't have any instructions that would care about the alignment of memory addresses
  • however, the aforementioned Rust PR New format_args!() and fmt::Arguments implementation #148789 has reworked the formatting machinery to use the low bit of a pointer to core::fmt::rt::Argument as a tag.
    • because the Arguments are allocated on the stack, a misaligned pointer is embedded in the fmt::Arguments, which then gets interpreted as the string literal case – printing kilobytes of garbage onto the serial port

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    A-LLVMArea: Code generation parts specific to LLVM. Both correctness bugs and optimization-related issues.C-bugCategory: This is a bug.O-AVRTarget: AVR processors (ATtiny, ATmega, etc.)T-compilerRelevant to the compiler team, which will review and decide on the PR/issue.

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions