Skip to content

Commit cc169dd

Browse files
authored
stdio hardening + new mutex API (#1224)
* * Harden stdio_usb and stdio in general against deadlocks which could otherwise result from doing printfs from within IRQs * Add a test for the above * Add mutex_try_enter_block_until API. * Make best_effort_wfe_or_timeout not use alarms if called from within IRQ
1 parent 5b46799 commit cc169dd

File tree

9 files changed

+138
-34
lines changed

9 files changed

+138
-34
lines changed

src/common/pico_sync/include/pico/mutex.h

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,20 @@ void recursive_mutex_enter_blocking(recursive_mutex_t *mtx);
114114
*/
115115
bool mutex_try_enter(mutex_t *mtx, uint32_t *owner_out);
116116

117+
/*! \brief Attempt to take ownership of a mutex until the specified time
118+
* \ingroup mutex
119+
*
120+
* If the mutex wasn't owned, this method will immediately claim the mutex for the caller and return true.
121+
* If the mutex is owned by the caller, this method will immediately return false,
122+
* If the mutex is owned by someone else, this method will try to claim it until the specified time, returning
123+
* true if it succeeds, or false on timeout
124+
*
125+
* \param mtx Pointer to mutex structure
126+
* \param until The time after which to return if the caller cannot be granted ownership of the mutex
127+
* \return true if mutex now owned, false otherwise
128+
*/
129+
bool mutex_try_enter_block_until(mutex_t *mtx, absolute_time_t until);
130+
117131
/*! \brief Attempt to take ownership of a recursive mutex
118132
* \ingroup mutex
119133
*

src/common/pico_sync/mutex.c

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,17 @@ bool __time_critical_func(mutex_try_enter)(mutex_t *mtx, uint32_t *owner_out) {
8080
return entered;
8181
}
8282

83+
bool __time_critical_func(mutex_try_enter_block_until)(mutex_t *mtx, absolute_time_t until) {
84+
// not using lock_owner_id_t to avoid backwards incompatibility change to mutex_try_enter API
85+
static_assert(sizeof(lock_owner_id_t) <= 4, "");
86+
uint32_t owner;
87+
if (!mutex_try_enter(mtx, &owner)) {
88+
if ((lock_owner_id_t)owner == lock_get_caller_owner_id()) return false; // deadlock, so we can never own it
89+
return mutex_enter_block_until(mtx, until);
90+
}
91+
return true;
92+
}
93+
8394
bool __time_critical_func(recursive_mutex_try_enter)(recursive_mutex_t *mtx, uint32_t *owner_out) {
8495
bool entered;
8596
lock_owner_id_t caller = lock_get_caller_owner_id();

src/common/pico_time/time.c

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -431,16 +431,21 @@ void sleep_ms(uint32_t ms) {
431431

432432
bool best_effort_wfe_or_timeout(absolute_time_t timeout_timestamp) {
433433
#if !PICO_TIME_DEFAULT_ALARM_POOL_DISABLED
434-
alarm_id_t id;
435-
id = add_alarm_at(timeout_timestamp, sleep_until_callback, NULL, false);
436-
if (id <= 0) {
434+
if (__get_current_exception()) {
437435
tight_loop_contents();
438436
return time_reached(timeout_timestamp);
439437
} else {
440-
__wfe();
441-
// we need to clean up if it wasn't us that caused the wfe; if it was this will be a noop.
442-
cancel_alarm(id);
443-
return time_reached(timeout_timestamp);
438+
alarm_id_t id;
439+
id = add_alarm_at(timeout_timestamp, sleep_until_callback, NULL, false);
440+
if (id <= 0) {
441+
tight_loop_contents();
442+
return time_reached(timeout_timestamp);
443+
} else {
444+
__wfe();
445+
// we need to clean up if it wasn't us that caused the wfe; if it was this will be a noop.
446+
cancel_alarm(id);
447+
return time_reached(timeout_timestamp);
448+
}
444449
}
445450
#else
446451
tight_loop_contents();

src/rp2_common/pico_stdio/include/pico/stdio.h

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,11 @@
3636
#define PICO_STDIO_STACK_BUFFER_SIZE 128
3737
#endif
3838

39+
// PICO_CONFIG: PICO_STDIO_DEADLOCK_TIMEOUT_MS, Time after which to assume stdio_usb is deadlocked by use in IRQ and give up, type=int, default=1000, group=pico_stdio
40+
#ifndef PICO_STDIO_DEADLOCK_TIMEOUT_MS
41+
#define PICO_STDIO_DEADLOCK_TIMEOUT_MS 1000
42+
#endif
43+
3944
#ifdef __cplusplus
4045
extern "C" {
4146
#endif

src/rp2_common/pico_stdio/stdio.c

Lines changed: 1 addition & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -43,18 +43,7 @@ static stdio_driver_t *filter;
4343
auto_init_mutex(print_mutex);
4444

4545
bool stdout_serialize_begin(void) {
46-
lock_owner_id_t caller = lock_get_caller_owner_id();
47-
// not using lock_owner_id_t to avoid backwards incompatibility change to mutex_try_enter API
48-
static_assert(sizeof(lock_owner_id_t) <= 4, "");
49-
uint32_t owner;
50-
if (!mutex_try_enter(&print_mutex, &owner)) {
51-
if (owner == (uint32_t)caller) {
52-
return false;
53-
}
54-
// we are not a nested call, so lets wait
55-
mutex_enter_blocking(&print_mutex);
56-
}
57-
return true;
46+
return mutex_try_enter_block_until(&print_mutex, make_timeout_time_ms(PICO_STDIO_DEADLOCK_TIMEOUT_MS));
5847
}
5948

6049
void stdout_serialize_end(void) {

src/rp2_common/pico_stdio_usb/stdio_usb.c

Lines changed: 21 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -93,10 +93,8 @@ static void usb_irq(void) {
9393

9494
static void stdio_usb_out_chars(const char *buf, int length) {
9595
static uint64_t last_avail_time;
96-
uint32_t owner;
97-
if (!mutex_try_enter(&stdio_usb_mutex, &owner)) {
98-
if (owner == get_core_num()) return; // would deadlock otherwise
99-
mutex_enter_blocking(&stdio_usb_mutex);
96+
if (!mutex_try_enter_block_until(&stdio_usb_mutex, make_timeout_time_ms(PICO_STDIO_DEADLOCK_TIMEOUT_MS))) {
97+
return;
10098
}
10199
if (stdio_usb_connected()) {
102100
for (int i = 0; i < length;) {
@@ -126,20 +124,28 @@ static void stdio_usb_out_chars(const char *buf, int length) {
126124
}
127125

128126
int stdio_usb_in_chars(char *buf, int length) {
129-
uint32_t owner;
130-
if (!mutex_try_enter(&stdio_usb_mutex, &owner)) {
131-
if (owner == get_core_num()) return PICO_ERROR_NO_DATA; // would deadlock otherwise
132-
mutex_enter_blocking(&stdio_usb_mutex);
133-
}
127+
// note we perform this check outside the lock, to try and prevent possible deadlock conditions
128+
// with printf in IRQs (which we will escape through timeouts elsewhere, but that would be less graceful).
129+
//
130+
// these are just checks of state, so we can call them while not holding the lock.
131+
// they may be wrong, but only if we are in the middle of a tud_task call, in which case at worst
132+
// we will mistakenly think we have data available when we do not (we will check again), or
133+
// tud_task will complete running and we will check the right values the next time.
134+
//
134135
int rc = PICO_ERROR_NO_DATA;
135136
if (stdio_usb_connected() && tud_cdc_available()) {
136-
int count = (int) tud_cdc_read(buf, (uint32_t) length);
137-
rc = count ? count : PICO_ERROR_NO_DATA;
138-
} else {
139-
// because our mutex use may starve out the background task, run tud_task here (we own the mutex)
140-
tud_task();
137+
if (!mutex_try_enter_block_until(&stdio_usb_mutex, make_timeout_time_ms(PICO_STDIO_DEADLOCK_TIMEOUT_MS))) {
138+
return PICO_ERROR_NO_DATA; // would deadlock otherwise
139+
}
140+
if (stdio_usb_connected() && tud_cdc_available()) {
141+
int count = (int) tud_cdc_read(buf, (uint32_t) length);
142+
rc = count ? count : PICO_ERROR_NO_DATA;
143+
} else {
144+
// because our mutex use may starve out the background task, run tud_task here (we own the mutex)
145+
tud_task();
146+
}
147+
mutex_exit(&stdio_usb_mutex);
141148
}
142-
mutex_exit(&stdio_usb_mutex);
143149
return rc;
144150
}
145151

test/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
add_subdirectory(pico_test)
22

33
add_subdirectory(pico_stdlib_test)
4+
add_subdirectory(pico_stdio_test)
45
add_subdirectory(pico_time_test)
56
add_subdirectory(pico_divider_test)
67
if (PICO_ON_DEVICE)
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
if (NOT PICO_TIME_NO_ALARM_SUPPORT)
2+
add_executable(pico_stdio_test_uart pico_stdio_test.c)
3+
target_link_libraries(pico_stdio_test_uart PRIVATE pico_stdlib pico_test pico_multicore)
4+
pico_add_extra_outputs(pico_stdio_test_uart)
5+
pico_enable_stdio_uart(pico_stdio_test_uart 1)
6+
pico_enable_stdio_usb(pico_stdio_test_uart 0)
7+
8+
add_executable(pico_stdio_test_usb pico_stdio_test.c)
9+
target_link_libraries(pico_stdio_test_usb PRIVATE pico_stdlib pico_test pico_multicore)
10+
target_compile_definitions(pico_stdio_test_usb PRIVATE
11+
PICO_STDIO_USB_CONNECT_WAIT_TIMEOUT_MS=-1) # wait for USB connect
12+
13+
pico_add_extra_outputs(pico_stdio_test_usb)
14+
pico_enable_stdio_uart(pico_stdio_test_usb 0)
15+
pico_enable_stdio_usb(pico_stdio_test_usb 1)
16+
endif()
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
/**
2+
* Copyright (c) 2023 Raspberry Pi (Trading) Ltd.
3+
*
4+
* SPDX-License-Identifier: BSD-3-Clause
5+
*/
6+
7+
#include <stdio.h>
8+
#include "pico/stdlib.h"
9+
#include "pico/multicore.h"
10+
#include "pico/test.h"
11+
12+
PICOTEST_MODULE_NAME("pico_stdio_test", "pico_stdio test harness");
13+
14+
static volatile bool deadlock_test_done;
15+
static void deadlock_test_core1(void) {
16+
busy_wait_ms(250);
17+
for(int i=0;i<1000;i++) {
18+
if (deadlock_test_done) break;
19+
printf("Hello from core 1 - %d\n", i);
20+
busy_wait_ms(23);
21+
}
22+
}
23+
24+
static volatile bool deadlock_test_irq_called;
25+
26+
static int64_t deadlock_test_alarm(alarm_id_t id, void *param) {
27+
static int foo;
28+
printf("Here is a printf from the IRQ %d\n", ++foo);
29+
deadlock_test_irq_called = true;
30+
return 100;
31+
}
32+
33+
int main() {
34+
stdio_init_all();
35+
36+
for(int i=0;i<10;i++) {
37+
printf("Hello %d\n", i);
38+
}
39+
printf("pico_stdio_test begins\n");
40+
PICOTEST_START();
41+
42+
// Check default config has valid data in it
43+
PICOTEST_START_SECTION("STDIO deadlock test");
44+
multicore_launch_core1(deadlock_test_core1);
45+
alarm_id_t alarm_id = add_alarm_in_ms(500, deadlock_test_alarm, NULL, false);
46+
PICOTEST_CHECK(getchar_timeout_us(2000000) < 0, "someone pressed a key!");
47+
48+
deadlock_test_done = true;
49+
cancel_alarm(alarm_id);
50+
51+
sleep_ms(100);
52+
PICOTEST_CHECK(deadlock_test_irq_called, "deadlock_test_irq was not called");
53+
PICOTEST_END_SECTION();
54+
55+
PICOTEST_END_TEST();
56+
}
57+

0 commit comments

Comments
 (0)