close
Skip to content

earlgrey: Add static stack size verification tooling and firmware tests - #424

Open
anthonychen1251 wants to merge 2 commits into
OpenPRoT:earlgrey-hwefrom
anthonychen1251:stack-static-check
Open

earlgrey: Add static stack size verification tooling and firmware tests#424
anthonychen1251 wants to merge 2 commits into
OpenPRoT:earlgrey-hwefrom
anthonychen1251:stack-static-check

Conversation

@anthonychen1251

@anthonychen1251 anthonychen1251 commented Aug 18, 2026

Copy link
Copy Markdown

This PR introduces an automated, compile-time static stack size analysis tooling for pw_kernel userspace processes on OpenTitan earlgrey firmware (hwe and transport).

In pw_kernel, each process has a fixed static RAM allocation (ram_size_bytes) declared in system.json5 and enforced by hardware PMP regions.

Previously, determining the required RAM for a new or modified process required guesswork. This tooling reconstructs the caller-to-callee call graph, computes the worst-case cumulative stack depth for each process protection domain, and enforces safety margins at compile time.

  • Known Limitations
    Dynamic Indirect Jumps (jalr): The static call-graph reconstructor parses disassembly to identify direct or PC-relative calls (such as jal or auipc + jalr ). However, dynamic indirect calls—including dynamic trait objects (&dyn Trait) using vtables or standard function pointers (fn())—retrieve target addresses at runtime via CPU registers (jalr rs1). Since these lack static symbol labels, they remain unresolvable through pure disassembly and are excluded from the cumulative depth calculation.

When running bazel test //target/earlgrey/firmware/transport:stack_size_transport, the tool produces a detailed report:

================================================================================
  STATIC STACK SIZE & CALL GRAPH VERIFICATION REPORT: App 'transport'
================================================================================
Binary: target/earlgrey/firmware/transport/transport
Config: target/earlgrey/firmware/transport/system.json5
Headroom buffer: 0 bytes

WARNING: Unresolved indirect jump(s) detected in reachable call paths:
  ┌─ Process: flash_server                                 
  ├── Function: _ZN44_$LT$u32$u20$as$u20$zfmt..format..Format$GT$3fmt17h473a47ead3eb0cb0E (2 indirect jump(s): a0, a2)
  ┌─ Process: logmgr
  ├── Function: _ZN4core3fmt5write17hfa250156eaa22698E (5 indirect jump(s): a3, a4, a3)
  ┌─ Process: platform
  ├── Function: _ZN4core3fmt5write17hfa250156eaa22698E (5 indirect jump(s): a3, a4, a3)
  ┌─ Process: sysmgr      
  ├── Function: _ZN4core3fmt5write17hfa250156eaa22698E (5 indirect jump(s): a3, a4, a3)
  ├── Function: _ZN4zfmt6format8fmt_uint17hdc28bddb54f49ae5E (2 indirect jump(s): a0, a2)                                                                                                                                                      
  ┌─ Process: updatemgr                                    
  ├── Function: _ZN44_$LT$u32$u20$as$u20$zfmt..format..Format$GT$3fmt17hb528abe7c7e1dd22E (2 indirect jump(s): a0, a2)
  ┌─ Process: usbmgr                                                                                                                                                                                                                           
  ├── Function: _ZN4zfmt6format8fmt_uint17hdb038c641794a7dbE (2 indirect jump(s): a0, a2)
  ├── Function: _ZN69_$LT$usbmgr..MyDescriptors$u20$as$u20$usb_stack..DescriptorSource$GT$10get_string17h622354532458fa85E (1 indirect jump(s): a3)
  ├── Function: _ZN6usbmgr13_inner_usbmgr17he8d7aaba7a3a4c07E (3 indirect jump(s): a0, a0, a0)                                                                                                                                                 
  Note: Dynamic dispatch (e.g. &dyn Trait, fn() pointers) cannot be statically traced.                         
        Callee frames beyond these calls are not included in cumulative stack depth. 

Process Allocation Budgets:
  - flash_server   : RAM =   4096 B | Max Allowed Stack =   4096 B
  - logmgr         : RAM =   4096 B | Max Allowed Stack =   4096 B
  - platform       : RAM =   4096 B | Max Allowed Stack =   4096 B
  - sysmgr         : RAM =   4096 B | Max Allowed Stack =   4096 B
  - updatemgr      : RAM =   4096 B | Max Allowed Stack =   4096 B
  - usbmgr         : RAM =  16384 B | Max Allowed Stack =  16384 B
  - [shared/common]: Min Process Budget =   4096 B

  ┌─ Process: logmgr (Peak Stack:  3504 B | Margin:   592 B / 4096 B)                                 
  ├── _entry_logmgr (16 B)            
  ├── _ZN6logmgr13_inner_logmgr17hb0e2687dbe804973E (2992 B)                                                        
  ├── _ZN6logmgr15service_uart_tx17haec1968fbeeef0a0E (448 B)                                                          
  └── _ZN9util_zfmt6buffer18LogBuffer$LT$_$GT$15next_frame_size17hb5cfe96c90154884E (48 B)                                                                                                                                                     
                                                                                                                       
  ┌─ Process: platform (Peak Stack:   656 B | Margin:  3440 B / 4096 B)                                                
  ├── _entry_platform (16 B)       
  ├── _ZN8platform15_inner_platform17hafea2982faf8e10fE (256 B)                                                        
  ├── _ZN4zfmt6output11binary_send17h70931ea870ecb1a5E (368 B)                                                                                                                                                                                 
  ├── memcpy (0 B)                                         
  └── _ZN17compiler_builtins3mem6memcpy17h6e18710bad54b5dfE (16 B)                                                     
                                                                                                                                                                                                                                               
  ┌─ Process: sysmgr (Peak Stack:  2544 B | Margin:  1552 B / 4096 B)
  ├── _entry_sysmgr (16 B)                           
  ├── _ZN6sysmgr13_inner_sysmgr17ha57e7cacbc68199fE (1456 B)                                                           
  ├── _ZN22earlgrey_sysmgr_server12SysmgrServer10handle_one17hfbbc53502d3e6ae7E (448 B)                                
  ├── _ZN4sha26sha25611compress25617h8ec77f7fe100458aE (576 B)                                                         
  └── _ZN4sha26sha2564soft8schedule17h12deaa7b8c96a277E (48 B)      
                                                           
  ┌─ Process: updatemgr (Peak Stack:  2928 B | Margin:  1168 B / 4096 B)                                               
  ├── _entry_updatemgr (16 B)                                                                                          
  ├── _ZN9updatemgr16_inner_updatemgr17hb4d9637b35ce00b3E (2352 B)     
  ├── _ZN4zfmt6output9text_send17hb01a01403afd64f7E (176 B)                                                            
  ├── _ZN4zfmt6output11binary_send17hbc0ebf3dc00068bfE (368 B)                                                         
  ├── memcpy (0 B)                                                                                                     
  └── _ZN17compiler_builtins3mem6memcpy17h6e18710bad54b5dfE (16 B)        
                                                                                                                       
  ┌─ Process: usbmgr (Peak Stack:  7056 B | Margin:  9328 B / 16384 B)
  ├── _entry_usbmgr (16 B)                                                                                             
  ├── _ZN6usbmgr13_inner_usbmgr17he8d7aaba7a3a4c07E (4912 B)
  ├── _ZN91_$LT$usbmgr..dfu..EarlgreyDfuHandler$LT$IPC$GT$$u20$as$u20$protocol_usb_dfu..DfuHandler$GT$6upload17h3db78fd8d29c7cf0E (1552 B)                                  
  ├── _ZN4zfmt6output9text_send17hd9ef02f9e4180e04E (176 B)                                                            
  ├── _ZN4zfmt6output11binary_send17h6f64ba04ee6f8cbfE (352 B)                                                                                                                                                                                 
  └── _ZN72_$LT$util_zfmt..IpcLogger$LT$IPC$GT$$u20$as$u20$zfmt..logger..Logger$GT$13send_vectored17h98cbb7746313bff0E (48 B)                                                                                                                  
                                                                                                                       
Peak Individual Function Frame by Process:                                                                             
  - flash_server   : Peak =  2224 B (Margin:  1872 B) | Function: _ZN12flash_server19_inner_flash_server17hef961fd204c07fc6E                                                                                                                   
  - logmgr         : Peak =  2992 B (Margin:  1104 B) | Function: _ZN6logmgr13_inner_logmgr17hb0e2687dbe804973E        
  - platform       : Peak =   256 B (Margin:  3840 B) | Function: _ZN8platform15_inner_platform17hafea2982faf8e10fE    
  - sysmgr         : Peak =  1456 B (Margin:  2640 B) | Function: _ZN6sysmgr13_inner_sysmgr17ha57e7cacbc68199fE        
  - updatemgr      : Peak =  2352 B (Margin:  1744 B) | Function: _ZN9updatemgr16_inner_updatemgr17hb4d9637b35ce00b3E  
  - usbmgr         : Peak =  4912 B (Margin: 11472 B) | Function: _ZN6usbmgr13_inner_usbmgr17he8d7aaba7a3a4c07E
  - [shared/common]: Peak =  1552 B (Margin:  2544 B) | Function: _ZN91_$LT$usbmgr..dfu..EarlgreyDfuHandler$LT$IPC$GT$$u20$as$u20$protocol_usb_dfu..DfuHandler$GT$6upload17h3db78fd8d29c7cf0E  

--------------------------------------------------------------------------------
PASSED: All cumulative call stacks and function frames conform to allocated process budgets.
================================================================================

Add static stack size and cumulative call-stack verification tooling
to inspect Rust ELF binaries against process memory budgets declared
in system.json5.

Includes:
- MODULE.bazel: expose hermetic @llvm_toolchain for llvm-readobj and llvm-objdump.
- check_stack_sizes.py: parses system.json5 allocations, extracts
  function stack frame sizes via llvm-readobj, constructs directed
  call graphs via llvm-objdump, computes worst-case cumulative
  call paths for userspace processes, and detects/warns on recursive
  cycles and unresolved reachable indirect jumps.
- stack_size_test.bzl: defines rust_stack_size_test rule and macro using hermetic llvm tools.
- BUILD.bazel: exposes tooling targets.

Signed-off-by: Anthony Chen <antchen@google.com>
…ize allocations

Add rust_stack_size_test to HWE and Transport firmware targets to verify
that userspace process cumulative call paths conform to system.json5
memory allocations.

Also adjust updatemgr ram_size_bytes from 8192 to 4096 and
kernel_stack_size_bytes from 4096 to 2048 in transport system.json5
based on static stack analysis findings.

Signed-off-by: Anthony Chen <antchen@google.com>
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.

1 participant