blob: bb2367c985a53a309e76556d4226423d5c632d5f [file]
// Copyright 2026 The Fuchsia Authors
//
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file or at
// https://opensource.org/licenses/MIT
#include <lib/debuglog.h>
#include <lib/fit/defer.h>
#include <lib/io.h>
#include <lib/kconcurrent/chainlock_transaction.h>
#include <lib/unittest/unittest.h>
#include <platform.h>
#include <stdio.h>
#include <zircon/errors.h>
#include <zircon/time.h>
#include <zircon/types.h>
#include <kernel/thread.h>
#include <ktl/algorithm.h>
#include <ktl/atomic.h>
#include <ktl/string_view.h>
#include <platform/debug.h>
namespace {
// Wait until |thread| has blocked.
void WaitUntilBlocked(Thread* thread) {
zx_duration_mono_t wait_duration = ZX_USEC(1);
while (true) {
{
SingleChainLockGuard guard{IrqSaveOption, thread->get_lock(), CLT_TAG("WaitUntilBlocked")};
if (thread->state() == THREAD_BLOCKED) {
return;
}
}
Thread::Current::SleepRelative(wait_duration);
wait_duration = ktl::min(wait_duration * 2, ZX_MSEC(10));
}
}
struct WriterState {
ktl::atomic<bool> done{false};
ktl::atomic<zx_status_t> status{ZX_ERR_INTERNAL};
};
constexpr ktl::string_view kWriterMsg =
"blocking write issued while the debug UART was suspended\n";
// Note that we must use `dlog_serial_write` (or its interruptible variant, the
// same path used by `zx_debug_write`) rather than calling
// `platform_dputs_thread` directly. The UART driver does not support multiple
// concurrent blocking writers (only one waiter is woken per TX interrupt), so
// all blocking writers must be serialized with the debuglog thread via the
// debuglog serial lock.
int NonInterruptibleWriter(void* arg) {
auto* state = static_cast<WriterState*>(arg);
dlog_serial_write(kWriterMsg);
state->status.store(ZX_OK, ktl::memory_order_relaxed);
state->done.store(true, ktl::memory_order_relaxed);
return 0;
}
int InterruptibleWriter(void* arg) {
auto* state = static_cast<WriterState*>(arg);
state->status.store(dlog_serial_write_interruptible(kWriterMsg), ktl::memory_order_relaxed);
state->done.store(true, ktl::memory_order_relaxed);
return 0;
}
// Verify that while the debug UART is suspended (a) blocking writers block
// until it's resumed, (b) non-blocking writers and polling reads do not hang,
// and (c) suspend/resume report BAD_STATE when called in the wrong state.
bool uart_suspend_resume_test() {
BEGIN_TEST;
zx_status_t status = platform_serial_suspend();
if (status == ZX_ERR_NOT_SUPPORTED) {
printf("debug UART suspend not supported, skipping\n");
END_TEST;
}
ASSERT_OK(status);
// No matter how the test exits, make sure the UART is resumed and that the
// writer thread (if any) has terminated before |state| goes out of scope.
WriterState state;
Thread* writer = nullptr;
auto cleanup = fit::defer([&writer]() {
if (platform_serial_is_suspended()) {
platform_serial_resume();
}
if (writer != nullptr) {
writer->Join(nullptr, ZX_TIME_INFINITE);
}
});
EXPECT_TRUE(platform_serial_is_suspended());
EXPECT_EQ(ZX_ERR_BAD_STATE, platform_serial_suspend());
// None of these may block or touch the hardware.
serial_write("this non-blocking output should be dropped\n");
platform_pputc('x');
char c;
EXPECT_EQ(-1, platform_pgetc(&c));
// Start a thread to perform a blocking write. It should not complete until
// the UART has been resumed.
writer = Thread::Create("uart suspend writer", NonInterruptibleWriter, &state, DEFAULT_PRIORITY);
ASSERT_NONNULL(writer);
writer->Resume();
WaitUntilBlocked(writer);
EXPECT_FALSE(state.done.load(ktl::memory_order_relaxed));
ASSERT_OK(platform_serial_resume());
EXPECT_FALSE(platform_serial_is_suspended());
EXPECT_EQ(ZX_ERR_BAD_STATE, platform_serial_resume());
ASSERT_OK(writer->Join(nullptr, ZX_TIME_INFINITE));
writer = nullptr;
EXPECT_TRUE(state.done.load(ktl::memory_order_relaxed));
EXPECT_OK(state.status.load(ktl::memory_order_relaxed));
END_TEST;
}
// Verify that a thread blocked in an interruptible write (as performed by
// `zx_debug_write`) waiting for the debug UART to be resumed can be killed.
bool uart_suspend_interruptible_write_kill_test() {
BEGIN_TEST;
zx_status_t status = platform_serial_suspend();
if (status == ZX_ERR_NOT_SUPPORTED) {
printf("debug UART suspend not supported, skipping\n");
END_TEST;
}
ASSERT_OK(status);
WriterState state;
Thread* writer = nullptr;
auto cleanup = fit::defer([&writer]() {
if (writer != nullptr) {
writer->Kill();
writer->Join(nullptr, ZX_TIME_INFINITE);
}
if (platform_serial_is_suspended()) {
platform_serial_resume();
}
});
writer = Thread::Create("uart suspend interruptible writer", InterruptibleWriter, &state,
DEFAULT_PRIORITY);
ASSERT_NONNULL(writer);
writer->Resume();
WaitUntilBlocked(writer);
EXPECT_FALSE(state.done.load(ktl::memory_order_relaxed));
// Kill the writer while the UART is still suspended. It should terminate
// without writing anything.
writer->Kill();
ASSERT_OK(writer->Join(nullptr, ZX_TIME_INFINITE));
writer = nullptr;
EXPECT_TRUE(state.done.load(ktl::memory_order_relaxed));
EXPECT_EQ(ZX_ERR_INTERNAL_INTR_KILLED, state.status.load(ktl::memory_order_relaxed));
EXPECT_TRUE(platform_serial_is_suspended());
ASSERT_OK(platform_serial_resume());
END_TEST;
}
// Verify that suspending a thread blocked in an interruptible write (as
// performed by `zx_debug_write`) interrupts the wait with
// ZX_ERR_INTERNAL_INTR_RETRY without writing anything.
bool uart_suspend_interruptible_write_thread_suspend_test() {
BEGIN_TEST;
zx_status_t status = platform_serial_suspend();
if (status == ZX_ERR_NOT_SUPPORTED) {
printf("debug UART suspend not supported, skipping\n");
END_TEST;
}
ASSERT_OK(status);
WriterState state;
Thread* writer = nullptr;
auto cleanup = fit::defer([&writer]() {
if (platform_serial_is_suspended()) {
platform_serial_resume();
}
if (writer != nullptr) {
writer->Join(nullptr, ZX_TIME_INFINITE);
}
});
writer = Thread::Create("uart suspend interruptible writer", InterruptibleWriter, &state,
DEFAULT_PRIORITY);
ASSERT_NONNULL(writer);
writer->Resume();
WaitUntilBlocked(writer);
EXPECT_FALSE(state.done.load(ktl::memory_order_relaxed));
// Ask the writer to suspend while the UART is still suspended. This should
// interrupt the wait with ZX_ERR_INTERNAL_INTR_RETRY without writing
// anything.
ASSERT_OK(writer->Suspend());
ASSERT_OK(writer->Join(nullptr, ZX_TIME_INFINITE));
writer = nullptr;
EXPECT_TRUE(state.done.load(ktl::memory_order_relaxed));
EXPECT_EQ(ZX_ERR_INTERNAL_INTR_RETRY, state.status.load(ktl::memory_order_relaxed));
EXPECT_TRUE(platform_serial_is_suspended());
ASSERT_OK(platform_serial_resume());
END_TEST;
}
} // namespace
UNITTEST_START_TESTCASE(platform_debug_tests)
UNITTEST("suspend_resume", uart_suspend_resume_test)
UNITTEST("suspend_interruptible_write_kill", uart_suspend_interruptible_write_kill_test)
UNITTEST("suspend_interruptible_write_thread_suspend",
uart_suspend_interruptible_write_thread_suspend_test)
UNITTEST_END_TESTCASE(platform_debug_tests, "platform_debug", "platform debug UART tests")