Skip to content

Commit d4330d3

Browse files
authored
Hw-based RTOS stack overflow detection (#4218)
* Respect the stack guard offset in thread stacks * Use the hardware watchpoint to protect rtos thread stacks * Use watchpoints for stack overflow detection
1 parent bbcda39 commit d4330d3

File tree

4 files changed

+103
-28
lines changed

4 files changed

+103
-28
lines changed

esp-rtos/esp_config.yml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,3 +8,13 @@ options:
88
constraints:
99
- type:
1010
validator: positive_integer
11+
12+
- name: sw-task-overflow-detection
13+
description: 'Enable software-based stack overflow detection. The stack guard value and offset is based on esp-hal configuration.'
14+
default:
15+
- value: false
16+
17+
- name: hw-task-overflow-detection
18+
description: 'Enable hardware-based stack overflow detection. The stack watermark is based on the esp-hal stack-guard-offset configuration.'
19+
default:
20+
- value: true

esp-rtos/src/lib.rs

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -222,20 +222,23 @@ pub fn start_with_idle_hook(
222222
SCHEDULER.with(move |scheduler| {
223223
scheduler.setup(TimeDriver::new(timer.timer()), idle_hook);
224224

225-
// Allocate the default task. The stack bottom is set to the stack guard's address as the
226-
// rest of the stack is unusable anyway.
225+
// Allocate the default task.
227226

228227
unsafe extern "C" {
229228
static _stack_start_cpu0: u32;
230-
static __stack_chk_guard: u32;
229+
static _stack_end_cpu0: u32;
231230
}
232231
let stack_top = &raw const _stack_start_cpu0;
233-
let stack_bottom = (&raw const __stack_chk_guard).cast::<MaybeUninit<u32>>();
232+
let stack_bottom = (&raw const _stack_end_cpu0).cast::<MaybeUninit<u32>>();
234233
let stack_slice = core::ptr::slice_from_raw_parts_mut(
235234
stack_bottom.cast_mut(),
236235
stack_top as usize - stack_bottom as usize,
237236
);
238-
task::allocate_main_task(scheduler, stack_slice);
237+
task::allocate_main_task(
238+
scheduler,
239+
stack_slice,
240+
esp_config::esp_config_int!(usize, "ESP_HAL_CONFIG_STACK_GUARD_OFFSET"),
241+
);
239242

240243
task::setup_multitasking(
241244
#[cfg(riscv)]
@@ -317,7 +320,14 @@ pub fn start_second_core_with_stack_guard_offset<const STACK_SIZE: usize>(
317320
scheduler.time_driver.is_some(),
318321
"The scheduler must be started on the first core first."
319322
);
320-
task::allocate_main_task(scheduler, ptrs.stack);
323+
task::allocate_main_task(
324+
scheduler,
325+
ptrs.stack,
326+
stack_guard_offset.unwrap_or(esp_config::esp_config_int!(
327+
usize,
328+
"ESP_HAL_CONFIG_STACK_GUARD_OFFSET"
329+
)),
330+
);
321331
task::yield_task();
322332
trace!("Second core scheduler initialized");
323333
});

esp-rtos/src/scheduler.rs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,8 @@ impl CpuSchedulerState {
6666
thread_semaphore: None,
6767
state: TaskState::Ready,
6868
stack: core::ptr::slice_from_raw_parts_mut(core::ptr::null_mut(), 0),
69+
#[cfg(any(hw_task_overflow_detection, sw_task_overflow_detection))]
70+
stack_guard: core::ptr::null_mut(),
6971
current_queue: None,
7072
priority: 0,
7173
#[cfg(multi_core)]
@@ -274,6 +276,8 @@ impl SchedulerState {
274276
let next_context = if let Some(next) = next_task {
275277
priority = Some(next.priority(&mut self.run_queue));
276278

279+
unsafe { next.as_ref().set_up_stack_watchpoint() };
280+
277281
unsafe { &raw mut (*next.as_ptr()).cpu_context }
278282
} else {
279283
// If there is no next task, set up and return to the idle hook.
@@ -284,6 +288,9 @@ impl SchedulerState {
284288
let idle_sp = if current_context.is_null()
285289
|| current_context == &raw mut self.per_cpu[current_cpu].main_task.cpu_context
286290
{
291+
// We're using the current task's stack, for which the watchpoint is already set
292+
// up.
293+
287294
let current_sp;
288295
cfg_if::cfg_if! {
289296
if #[cfg(xtensa)] {
@@ -294,6 +301,11 @@ impl SchedulerState {
294301
}
295302
current_sp
296303
} else {
304+
// We're using the main task's stack.
305+
self.per_cpu[current_cpu]
306+
.main_task
307+
.set_up_stack_watchpoint();
308+
297309
cfg_if::cfg_if! {
298310
if #[cfg(xtensa)] {
299311
self.per_cpu[current_cpu].main_task.cpu_context.A1

esp-rtos/src/task/mod.rs

Lines changed: 65 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -315,6 +315,8 @@ pub(crate) struct Task {
315315
pub thread_semaphore: Option<Semaphore>,
316316
pub state: TaskState,
317317
pub stack: *mut [MaybeUninit<u32>],
318+
#[cfg(any(hw_task_overflow_detection, sw_task_overflow_detection))]
319+
pub stack_guard: *mut u32,
318320
pub priority: usize,
319321
#[cfg(multi_core)]
320322
pub pinned_to: Option<Cpu>,
@@ -342,6 +344,7 @@ pub(crate) struct Task {
342344
pub(crate) heap_allocated: bool,
343345
}
344346

347+
#[cfg(sw_task_overflow_detection)]
345348
const STACK_CANARY: u32 =
346349
const { esp_config::esp_config_int!(u32, "ESP_HAL_CONFIG_STACK_GUARD_VALUE") };
347350

@@ -366,14 +369,17 @@ impl Task {
366369
name, task_fn, param, task_stack_size, priority, pinned_to
367370
);
368371

369-
let extra_stack = if cfg!(debug_build) {
370-
// This is a lot, but debug builds fail in different ways without.
371-
6 * 1024
372+
// Make sure the stack guard doesn't eat into the stack size.
373+
let extra_stack = if cfg!(any(hw_task_overflow_detection, sw_task_overflow_detection)) {
374+
4 + esp_config::esp_config_int!(usize, "ESP_HAL_CONFIG_STACK_GUARD_OFFSET")
372375
} else {
373-
// Make sure the stack guard doesn't eat into the stack size.
374-
4
376+
0
375377
};
376378

379+
#[cfg(debug_build)]
380+
// This is a lot, but debug builds fail in different ways without.
381+
let extra_stack = extra_stack.max(6 * 1024);
382+
377383
let task_stack_size = task_stack_size + extra_stack;
378384

379385
// Make sure stack size is also aligned to 16 bytes.
@@ -393,17 +399,21 @@ impl Task {
393399

394400
let stack_bottom = stack.cast::<MaybeUninit<u32>>();
395401
let stack_len_bytes = layout.size();
396-
unsafe { stack_bottom.write(MaybeUninit::new(STACK_CANARY)) };
402+
403+
let stack_guard_offset =
404+
esp_config::esp_config_int!(usize, "ESP_HAL_CONFIG_STACK_GUARD_OFFSET");
397405

398406
let stack_words = core::ptr::slice_from_raw_parts_mut(stack_bottom, stack_len_bytes / 4);
399407
let stack_top = unsafe { stack_bottom.add(stack_words.len()).cast() };
400408

401-
Task {
409+
let mut task = Task {
402410
cpu_context: new_task_context(task_fn, param, stack_top),
403411
#[cfg(feature = "esp-radio")]
404412
thread_semaphore: None,
405413
state: TaskState::Ready,
406414
stack: stack_words,
415+
#[cfg(any(hw_task_overflow_detection, sw_task_overflow_detection))]
416+
stack_guard: stack_words.cast(),
407417
current_queue: None,
408418
priority,
409419
#[cfg(multi_core)]
@@ -418,19 +428,49 @@ impl Task {
418428

419429
#[cfg(feature = "alloc")]
420430
heap_allocated: false,
431+
};
432+
433+
task.set_up_stack_guard(stack_guard_offset);
434+
435+
task
436+
}
437+
438+
fn set_up_stack_guard(&mut self, offset: usize) {
439+
let stack_bottom = self.stack.cast::<MaybeUninit<u32>>();
440+
let stack_guard = unsafe { stack_bottom.byte_add(offset) };
441+
442+
#[cfg(sw_task_overflow_detection)]
443+
unsafe {
444+
// avoid touching the main stack's canary on the first core
445+
if stack_guard.read().assume_init() != STACK_CANARY {
446+
stack_guard.write(MaybeUninit::new(STACK_CANARY));
447+
}
448+
}
449+
450+
#[cfg(any(hw_task_overflow_detection, sw_task_overflow_detection))]
451+
{
452+
self.stack_guard = stack_guard.cast();
421453
}
422454
}
423455

424456
pub(crate) fn ensure_no_stack_overflow(&self) {
457+
#[cfg(sw_task_overflow_detection)]
425458
assert_eq!(
426459
// This cast is safe to do from MaybeUninit<u32> because this is the word we've written
427460
// during initialization.
428-
unsafe { self.stack.cast::<u32>().read() },
461+
unsafe { self.stack_guard.read() },
429462
STACK_CANARY,
430463
"Stack overflow detected in {:?}",
431464
self as *const Task
432465
);
433466
}
467+
468+
pub(crate) fn set_up_stack_watchpoint(&self) {
469+
#[cfg(hw_task_overflow_detection)]
470+
unsafe {
471+
esp_hal::debugger::set_stack_watchpoint(self.stack_guard as usize);
472+
}
473+
}
434474
}
435475

436476
impl Drop for Task {
@@ -451,18 +491,20 @@ impl Drop for Task {
451491
}
452492
}
453493

454-
pub(super) fn allocate_main_task(scheduler: &mut SchedulerState, stack: *mut [MaybeUninit<u32>]) {
494+
pub(super) fn allocate_main_task(
495+
scheduler: &mut SchedulerState,
496+
stack: *mut [MaybeUninit<u32>],
497+
stack_guard_offset: usize,
498+
) {
455499
let cpu = Cpu::current();
456500
let current_cpu = cpu as usize;
457501

458-
unsafe {
459-
// avoid touching the main stack's canary on the first core
460-
if stack.cast::<MaybeUninit<u32>>().read().assume_init() != STACK_CANARY {
461-
stack
462-
.cast::<MaybeUninit<u32>>()
463-
.write(MaybeUninit::new(STACK_CANARY));
464-
}
465-
}
502+
debug_assert!(
503+
!scheduler.per_cpu[current_cpu].initialized,
504+
"Tried to allocate main task multiple times"
505+
);
506+
507+
scheduler.per_cpu[current_cpu].initialized = true;
466508

467509
// Reset main task properties. The rest should be cleared when the task is deleted.
468510
scheduler.per_cpu[current_cpu].main_task.priority = 0;
@@ -473,12 +515,13 @@ pub(super) fn allocate_main_task(scheduler: &mut SchedulerState, stack: *mut [Ma
473515
scheduler.per_cpu[current_cpu].main_task.pinned_to = Some(cpu);
474516
}
475517

476-
debug_assert!(
477-
!scheduler.per_cpu[current_cpu].initialized,
478-
"Tried to allocate main task multiple times"
479-
);
518+
scheduler.per_cpu[current_cpu]
519+
.main_task
520+
.set_up_stack_guard(stack_guard_offset);
480521

481-
scheduler.per_cpu[current_cpu].initialized = true;
522+
scheduler.per_cpu[current_cpu]
523+
.main_task
524+
.set_up_stack_watchpoint();
482525

483526
// This is slightly questionable as we don't ensure SchedulerState is pinned, but it's always
484527
// part of a static object so taking the pointer is fine.

0 commit comments

Comments
 (0)