blob: a496d262d035e4df6dcdedc3201eca31411b31a0 [file] [log] [blame]
// Copyright 2019 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/unittest/unittest.h>
#include <lib/zircon-internal/macros.h>
#include <zircon/types.h>
#include <ktl/string_view.h>
#include <ktl/unique_ptr.h>
#include "debuglog_internal.h"
#include <ktl/enforce.h>
namespace {
struct DLogOutputTest final : public DLog {
AutounsignalEvent output;
// Buffer to hold last rendered log - size = dlog max data + additional buffer for log header.
char output_log_buffer[DLOG_MAX_DATA + 128] TA_GUARDED(output_lock);
ktl::string_view last_output_log TA_GUARDED(output_lock);
DECLARE_LOCK(DLogOutputTest, Mutex) output_lock;
protected:
void OutputLogMessage(ktl::string_view log) override {
{
Guard<Mutex> output_guard(&output_lock);
memcpy(output_log_buffer, log.data(), log.length());
last_output_log = {output_log_buffer, log.length()};
}
output.Signal();
}
};
} // namespace
struct DebuglogTests {
static bool log_format() {
BEGIN_TEST;
fbl::AllocChecker ac;
ktl::unique_ptr<DLog> log = ktl::make_unique<DLog>(&ac);
ASSERT_TRUE(ac.check(), "");
char msg[] = "Hello World";
log->Write(DEBUGLOG_WARNING, 0, {msg, sizeof(msg)});
// log->data_[0] is not necessarily aligned as a dlog_header would be so copy rather than
// access them directly.
dlog_header header{};
static_assert(sizeof(log->data_) >= sizeof(header));
memcpy(&header, log->data_, sizeof(header));
EXPECT_EQ(DEBUGLOG_WARNING, header.severity);
EXPECT_EQ(0, header.flags);
EXPECT_EQ(ZX_KOID_INVALID, header.pid);
EXPECT_NE(ZX_KOID_INVALID, header.tid);
ASSERT_EQ(12, header.datalen);
uint8_t* msg_bytes = log->data_ + sizeof(dlog_header);
EXPECT_BYTES_EQ(reinterpret_cast<uint8_t*>(msg), msg_bytes, sizeof(msg));
END_TEST;
}
static bool log_wrap() {
BEGIN_TEST;
fbl::AllocChecker ac;
ktl::unique_ptr<DLog> log = ktl::make_unique<DLog>(&ac);
ASSERT_TRUE(ac.check(), "");
char msg[] = "Hello World";
const size_t kTruncateTarget =
kDLogHeaderFifoAlignment + 1; // An unaligned length somewhere in the middle of msg.
const size_t pad = DLOG_ALIGN_TRUNC(DLOG_SIZE - sizeof(dlog_header) - kTruncateTarget);
// We need to trigger some writes, but we don't care what we write, so we just need a buffer
// large enough to be copied from. Doesn't much matter what's in it.
char ignored_buffer[DLOG_MAX_DATA]{0};
for (size_t to_write = pad; to_write;) {
size_t write = to_write - sizeof(dlog_header);
write = write > DLOG_MAX_DATA ? DLOG_MAX_DATA : write;
log->Write(DEBUGLOG_WARNING, 0, {ignored_buffer, write});
to_write -= write + sizeof(dlog_header);
}
EXPECT_EQ(pad, log->head_);
log->Write(DEBUGLOG_WARNING, 0, {msg, sizeof(msg)});
// log->data_[pad] is not necessarily aligned as a dlog_header would be so copy rather than
// access them directly.
dlog_header header{};
static_assert(sizeof(log->data_) - pad >= sizeof(header));
memcpy(&header, log->data_ + pad, sizeof(header));
EXPECT_EQ(DEBUGLOG_WARNING, header.severity);
EXPECT_EQ(0, header.flags);
EXPECT_EQ(ZX_KOID_INVALID, header.pid);
EXPECT_NE(ZX_KOID_INVALID, header.tid);
ASSERT_EQ(sizeof(msg), header.datalen);
uint8_t* msg_bytes = log->data_ + pad + sizeof(dlog_header);
const size_t kTruncatedSize = DLOG_SIZE - pad - sizeof(dlog_header);
EXPECT_BYTES_EQ(reinterpret_cast<uint8_t*>(&msg[0]), msg_bytes, kTruncatedSize);
EXPECT_BYTES_EQ(reinterpret_cast<uint8_t*>(&msg[8]), log->data_, sizeof(msg) - kTruncatedSize);
END_TEST;
}
// Read a record from the debuglog and verify its fields.
static bool log_reader_read() {
BEGIN_TEST;
fbl::AllocChecker ac;
ktl::unique_ptr<DLog> log = ktl::make_unique<DLog>(&ac);
ASSERT_TRUE(ac.check());
const zx_time_t now = current_time();
char msg[] = "Message!";
ASSERT_EQ(ZX_OK, log->Write(DEBUGLOG_WARNING, 0, {msg, sizeof(msg)}));
DlogReader reader;
reader.Initialize(nullptr, nullptr, log.get());
size_t got;
dlog_record_t rec{};
ASSERT_EQ(ZX_OK, reader.Read(0, &rec, &got));
ASSERT_EQ(sizeof(dlog_header) + sizeof(msg), got);
EXPECT_EQ(0u, rec.hdr.preamble);
// Sequence should start at 0.
EXPECT_EQ(0u, rec.hdr.sequence);
EXPECT_EQ(sizeof(msg), rec.hdr.datalen);
EXPECT_EQ(DEBUGLOG_WARNING, rec.hdr.severity);
EXPECT_EQ(0, rec.hdr.flags);
EXPECT_GE(rec.hdr.timestamp, now);
EXPECT_EQ(0ull, rec.hdr.pid);
EXPECT_EQ(Thread::Current::Get()->tid(), rec.hdr.tid);
reader.Disconnect();
END_TEST;
}
// Write to the log, exceeding its capacity and see that data is lost.
static bool log_reader_dataloss() {
BEGIN_TEST;
fbl::AllocChecker ac;
ktl::unique_ptr<DLog> log = ktl::make_unique<DLog>(&ac);
ASSERT_TRUE(ac.check(), "");
DlogReader reader;
reader.Initialize(nullptr, nullptr, log.get());
char msg[] = "Hello World";
uint64_t num_written = 0;
log->Write(DEBUGLOG_WARNING, 0, {msg, sizeof(msg)});
num_written++;
dlog_record_t rec{};
size_t got;
ASSERT_EQ(ZX_OK, reader.Read(0, &rec, &got));
ASSERT_EQ(sizeof(dlog_header) + sizeof(msg), got);
EXPECT_EQ(DEBUGLOG_WARNING, rec.hdr.severity);
EXPECT_EQ(0u, rec.hdr.preamble);
EXPECT_EQ(0, rec.hdr.flags);
EXPECT_EQ(ZX_KOID_INVALID, rec.hdr.pid);
EXPECT_NE(ZX_KOID_INVALID, rec.hdr.tid);
ASSERT_EQ(sizeof(msg), rec.hdr.datalen);
EXPECT_BYTES_EQ(reinterpret_cast<uint8_t*>(msg), reinterpret_cast<uint8_t*>(rec.data),
sizeof(msg));
for (size_t i = 0; i < DLOG_SIZE; i += sizeof(dlog_header) + sizeof(msg)) {
log->Write(DEBUGLOG_WARNING, 0, {msg, sizeof(msg)});
num_written++;
}
uint64_t num_read = 0;
zx_status_t status;
uint64_t expected_sequence = 0;
uint64_t dropped = 0;
while ((status = reader.Read(0, &rec, &got)) == ZX_OK) {
num_read++;
ASSERT_EQ(sizeof(dlog_header) + sizeof(msg), got);
EXPECT_EQ(DEBUGLOG_WARNING, rec.hdr.severity);
EXPECT_EQ(0u, rec.hdr.preamble);
EXPECT_EQ(0, rec.hdr.flags);
EXPECT_EQ(ZX_KOID_INVALID, rec.hdr.pid);
EXPECT_NE(ZX_KOID_INVALID, rec.hdr.tid);
ASSERT_EQ(sizeof(msg), rec.hdr.datalen);
EXPECT_BYTES_EQ(reinterpret_cast<uint8_t*>(msg), reinterpret_cast<uint8_t*>(rec.data),
sizeof(msg));
dropped += (rec.hdr.sequence - expected_sequence);
expected_sequence = rec.hdr.sequence + 1;
}
EXPECT_EQ(ZX_ERR_SHOULD_WAIT, status);
// See that we read fewer records than we wrote.
EXPECT_LT(num_read, num_written);
EXPECT_EQ(0, rec.hdr.flags);
EXPECT_EQ(ZX_KOID_INVALID, rec.hdr.pid);
EXPECT_NE(ZX_KOID_INVALID, rec.hdr.tid);
ASSERT_EQ(sizeof(msg), rec.hdr.datalen);
EXPECT_BYTES_EQ(reinterpret_cast<uint8_t*>(msg), reinterpret_cast<uint8_t*>(rec.data),
sizeof(msg));
// See that all messages are acconted for. That is, the number dropped matches the differnce.
EXPECT_EQ(dropped, num_written - num_read);
reader.Disconnect();
END_TEST;
}
// Verify that logs written are output correctly by the DumperThread.
static bool log_dumper_test() {
BEGIN_TEST;
fbl::AllocChecker ac;
ktl::unique_ptr<DLogOutputTest> log = ktl::make_unique<DLogOutputTest>(&ac);
ASSERT_TRUE(ac.check());
// Start the dumper thread.
log->StartThreads();
// This is the size of the header.
const size_t min_output_size = []() {
dlog_header_t empty_hdr;
memset(&empty_hdr, 0, sizeof(empty_hdr));
return DLog::FormatHeader({}, empty_hdr);
}();
// A small helper which will count the number of occurrences of |msg| in
// |str|.
auto CountOccurences = [min_output_size](const ktl::string_view& str,
const ktl::string_view& msg) -> size_t {
size_t count = 0;
size_t offset = min_output_size;
while ((offset = str.find(msg, offset)) != str.npos) {
++count;
++offset;
}
return count;
};
// Helper function to verify that a message is output correctly.
auto write_log_and_check_output = [&log, &all_ok, min_output_size,
&CountOccurences](char* msg) -> bool {
auto len = strlen(msg);
ASSERT_OK(log->Write(DEBUGLOG_INFO, 0, {msg, len}));
// Wait for the log to get output by the DumperThread.
ASSERT_OK(log->output.Wait());
Guard<Mutex> output_guard(&log->output_lock);
// Length of output log is at least header length + message length.
EXPECT_GE(log->last_output_log.length(), min_output_size + len);
ASSERT_GT(log->last_output_log.length(), 0lu);
// Check that the log ends with a newline.
EXPECT_EQ('\n', log->last_output_log[log->last_output_log.length() - 1]);
// Check that the log contains the message (if not empty) after the header.
if (len) {
ASSERT_EQ(1u, CountOccurences(log->last_output_log, msg));
}
return all_ok;
};
// Check that a simple message appears in the log dump.
char msg0[] = "Hello World!\n";
EXPECT_TRUE(write_log_and_check_output(msg0));
// Check that a message without newline appears in the log dump and contains newline.
char msg1[] = "Hello!";
EXPECT_TRUE(write_log_and_check_output(msg1));
// Check that a message containing only newline in the log dump.
char msg2[] = "\n";
EXPECT_TRUE(write_log_and_check_output(msg2));
// Check that an empty log still gets rendered and contains newline.
char msg3[] = "";
EXPECT_TRUE(write_log_and_check_output(msg3));
log->Shutdown(ZX_TIME_INFINITE);
END_TEST;
}
static bool render_to_crashlog() {
BEGIN_TEST;
fbl::AllocChecker ac;
ktl::unique_ptr<DLog> log = ktl::make_unique<DLog>(&ac);
ASSERT_TRUE(ac.check());
// Define our test message and figure out a expected minimum size for the
// message when rendered. While we are not 100% sure how large the header
// for a given rendered record will be, we we know that a record with a
// timestamp, pid, and tid of zero will have the smallest rendered header
// possible. Note that our test message does not end with a newline, but we
// expect rendering to add one to each record which does not end with a
// newline.
const ktl::string_view msg{"Message!"};
const size_t min_rendered_size = [&msg]() {
dlog_header_t empty_hdr;
memset(&empty_hdr, 0, sizeof(empty_hdr));
return DLog::FormatHeader({}, empty_hdr) + msg.size() + 1;
}();
// Allocate two targets for rendering data into, one normal sized, and one
// small. Using a dedicated small buffer (instead of simply artificially
// limiting the length of a span wrapped around the large buffer) should
// help ASAN in detecting any buffer overflows.
constexpr size_t kLargeRenderBufferSize = 1024;
ktl::unique_ptr<char[]> large_storage{new (&ac) char[kLargeRenderBufferSize]};
ASSERT_TRUE(ac.check());
ASSERT_LE(min_rendered_size, kLargeRenderBufferSize);
constexpr size_t kSmallRenderBufferSize = 1;
ktl::unique_ptr<char[]> small_storage{new (&ac) char[kSmallRenderBufferSize]};
ASSERT_TRUE(ac.check());
ktl::span<char> large_target{large_storage.get(), kLargeRenderBufferSize};
ktl::span<char> small_target{small_storage.get(), kSmallRenderBufferSize};
// A small helper which will count the number of occurrences of |msg| in
// |str|.
auto CountOccurences = [](const ktl::string_view& str, const ktl::string_view& msg) -> size_t {
size_t count = 0;
size_t offset = 0;
while ((offset = str.find(msg, offset)) != str.npos) {
++count;
++offset;
}
return count;
};
// A small helper which renders a debuglog into a span provided, and gives
// back a string view of the result.
auto DoRender = [](const DLog& log, ktl::span<char> target) -> ktl::string_view {
return ktl::string_view{target.data(), log.RenderToCrashlog(target)};
};
// Rendering from an empty log should produce nothing.
ktl::string_view rendered;
rendered = DoRender(*log, large_target);
ASSERT_EQ(0u, rendered.size());
// Add record to the log, then render the log and verify that it contains at
// least a minimum number of expected bytes, and exactly one occurrence of
// the test string in the buffer.
ASSERT_EQ(ZX_OK, log->Write(DEBUGLOG_WARNING, 0, msg));
rendered = DoRender(*log, large_target);
ASSERT_GE(rendered.size(), min_rendered_size);
ASSERT_EQ(1u, CountOccurences(rendered, msg));
// Attempting to render into an empty target from a log with valid records
// in should produce no data.
rendered = DoRender(*log, {});
ASSERT_EQ(0u, rendered.size());
// Attempting to render into target with is non-empty, but does not have
// enough space to hold a rendered record, should also produce no data.
rendered = DoRender(*log, small_target);
ASSERT_EQ(0u, rendered.size());
// Add two more instances of the test message and re-validate
ASSERT_EQ(ZX_OK, log->Write(DEBUGLOG_WARNING, 0, msg));
ASSERT_EQ(ZX_OK, log->Write(DEBUGLOG_WARNING, 0, msg));
rendered = DoRender(*log, large_target);
ASSERT_GE(rendered.size(), min_rendered_size * 3);
ASSERT_EQ(3u, CountOccurences(rendered, msg));
// Figure out how many instances of our message _should_ fit in the dlog
// buffer, then add that many, forcing the log to wrap in the process. Note
// that records are always padded out to a 4 bytes boundary.
const size_t kRecordSize = (msg.size() + sizeof(dlog_header_t) + 0x3) & ~0x3;
const size_t kRecordCount = DLOG_SIZE / kRecordSize;
for (size_t i = 0; i < kRecordCount; ++i) {
ASSERT_EQ(ZX_OK, log->Write(DEBUGLOG_WARNING, 0, msg));
}
// It is not clear exactly how many records should fit into our render
// buffer at this point, but the number should be more than 3.
rendered = DoRender(*log, large_target);
ASSERT_GT(rendered.size(), min_rendered_size * 3);
ASSERT_GT(CountOccurences(rendered, msg), 3u);
END_TEST;
}
// This is a regression test introduced after discovering https://fxbug.dev/42071359. It
// attempt to make sure that the internal methods ReadRecord and
// ReassembleFromOffset return proper results, even when headers and payload
// span the break in the ring buffer.
static bool read_record() {
BEGIN_TEST;
fbl::AllocChecker ac;
ktl::unique_ptr<DLog> log = ktl::make_unique<DLog>(&ac);
ASSERT_TRUE(ac.check());
const char kPayloadData[] = {
0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b,
0x0c, 0x0d, 0x0e, 0x0f, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16,
0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f, 0x20,
};
const ktl::string_view kPayload{kPayloadData, ktl::size(kPayloadData)};
const size_t kPayloadSize = sizeof(kPayloadData);
const size_t kTotalRecordSize = sizeof(dlog_header_t) + kPayloadSize;
constexpr uint8_t kTestSeverity = 0xba;
constexpr uint8_t kTestFlags = 0xad;
const uint64_t our_tid = Thread::Current::Get()->tid();
static_assert(
(kPayloadSize % 4) == 0,
"Payload data must be a multiple of 4 bytes in order to be able to align its end exactly "
"with the end of the ring buffer");
constexpr ktl::array kOffsets = {
static_cast<size_t>(0), // Start of the buffer
DLOG_SIZE - kTotalRecordSize, // Aligned with end
DLOG_SIZE - kTotalRecordSize + (kPayloadSize / 2), // Splitting the payload
DLOG_SIZE - (sizeof(dlog_header_t) / 2), // Splitting the header
};
for (const size_t offset : kOffsets) {
Guard<MonitoredSpinLock, IrqSave> guard{&log->lock_, SOURCE_TAG};
// Set up the logs ring buffer so that it looks like it is empty, but
// ready to write the next record starting at the test vector offset.
// Also, zero out our entire ring buffer just so we are starting from a
// known state with each vector.
ASSERT_TRUE((offset % 4) == 0); // Records must start on 4 byte boundaries.
ASSERT_LT(offset, DLOG_SIZE);
log->head_ = log->tail_ = offset;
memset(log->data_, 0, sizeof(log->data_));
// Go ahead and write our record, then confirm that head and tail end up
// where we expect them to. Note that these pointers are always absolute
// pointers, to compute offsets from them, one needs to & with the
// DLOG_MASK.
zx_status_t status = ZX_ERR_INTERNAL;
const zx_time_t before_ts = current_time();
guard.CallUnlocked([&log, &status, kPayload]() {
status = log->Write(kTestSeverity, kTestFlags, kPayload);
});
const zx_time_t after_ts = current_time();
ASSERT_OK(status);
EXPECT_EQ(offset, log->tail_);
EXPECT_EQ(offset + kTotalRecordSize, log->head_);
// Reassemble the header from the offset and make sure it looks right.
dlog_header_t hdr;
memset(&hdr, 0, sizeof(hdr));
log->ReassembleFromOffset(offset, {reinterpret_cast<uint8_t*>(&hdr), sizeof(hdr)});
EXPECT_EQ(DLOG_HDR_GET_FIFOLEN(hdr.preamble), kTotalRecordSize);
EXPECT_EQ(DLOG_HDR_GET_READLEN(hdr.preamble), kTotalRecordSize);
EXPECT_EQ(kPayloadSize, hdr.datalen);
EXPECT_GE(hdr.timestamp, before_ts);
EXPECT_LE(hdr.timestamp, after_ts);
EXPECT_EQ(kTestSeverity, hdr.severity);
EXPECT_EQ(kTestFlags, hdr.flags);
EXPECT_EQ(0u, hdr.pid); // Kernel PID is always 0
EXPECT_EQ(our_tid, hdr.tid);
EXPECT_EQ(log->sequence_count_ - 1u, hdr.sequence);
// Now attempt to read the record info at the same offset. We should get
// the same header base, and regions which exist completely within the
// ring buffer, and have the test vector pattern in them.
const zx::result<DLog::Record> maybe_record = log->ReadRecord(offset);
ASSERT_TRUE(maybe_record.is_ok());
const DLog::Record& record = maybe_record.value();
const uintptr_t log_data = reinterpret_cast<uintptr_t>(log->data_);
EXPECT_BYTES_EQ(reinterpret_cast<const uint8_t*>(&hdr),
reinterpret_cast<const uint8_t*>(&record.hdr), sizeof(hdr));
EXPECT_GT(record.region1.size(), 0u);
EXPECT_LE(record.region1.size(), DLOG_MAX_RECORD);
ASSERT_GE(reinterpret_cast<uintptr_t>(record.region1.data()), log_data);
ASSERT_LE(reinterpret_cast<uintptr_t>(record.region1.data()) + record.region1.size(),
log_data + DLOG_SIZE);
if (record.region2.size() > 0u) {
EXPECT_LE(record.region2.size(), DLOG_MAX_RECORD);
ASSERT_GE(reinterpret_cast<uintptr_t>(record.region2.data()), log_data);
ASSERT_LE(reinterpret_cast<uintptr_t>(record.region2.data()) + record.region2.size(),
log_data + DLOG_SIZE);
}
ASSERT_EQ(kPayloadSize, record.region1.size() + record.region2.size());
EXPECT_BYTES_EQ(reinterpret_cast<const uint8_t*>(kPayloadData),
reinterpret_cast<const uint8_t*>(record.region1.data()),
record.region1.size());
EXPECT_BYTES_EQ(reinterpret_cast<const uint8_t*>(kPayloadData) + record.region1.size(),
reinterpret_cast<const uint8_t*>(record.region2.data()),
record.region2.size());
EXPECT_FALSE(record.ends_with_newline);
}
END_TEST;
}
// Test that write fails with an error once the |DLog| has been shutdown.
static bool shutdown() {
BEGIN_TEST;
fbl::AllocChecker ac;
ktl::unique_ptr<DLog> log = ktl::make_unique<DLog>(&ac);
ASSERT_TRUE(ac.check());
// Write one message and see that it succeeds.
char msg[] = "Message!";
ASSERT_EQ(ZX_OK, log->Write(DEBUGLOG_WARNING, 0, {msg, sizeof(msg)}));
// Now ask the DLog to shutdown, write another, see that it fails.
log->Shutdown(0);
ASSERT_EQ(ZX_ERR_BAD_STATE, log->Write(DEBUGLOG_WARNING, 0, msg));
// See that there is only one message in the DLog.
DlogReader reader;
reader.Initialize(nullptr, nullptr, log.get());
size_t got;
dlog_record_t rec{};
ASSERT_EQ(ZX_OK, reader.Read(0, &rec, &got));
ASSERT_EQ(sizeof(dlog_header) + sizeof(msg), got);
ASSERT_EQ(ZX_ERR_SHOULD_WAIT, reader.Read(0, &rec, &got));
reader.Disconnect();
END_TEST;
}
};
#define DEBUGLOG_UNITTEST(fname) UNITTEST(#fname, fname)
UNITTEST_START_TESTCASE(debuglog_tests)
DEBUGLOG_UNITTEST(DebuglogTests::log_format)
DEBUGLOG_UNITTEST(DebuglogTests::log_wrap)
DEBUGLOG_UNITTEST(DebuglogTests::log_reader_read)
DEBUGLOG_UNITTEST(DebuglogTests::log_reader_dataloss)
DEBUGLOG_UNITTEST(DebuglogTests::log_dumper_test)
DEBUGLOG_UNITTEST(DebuglogTests::render_to_crashlog)
DEBUGLOG_UNITTEST(DebuglogTests::read_record)
DEBUGLOG_UNITTEST(DebuglogTests::shutdown)
UNITTEST_END_TESTCASE(debuglog_tests, "debuglog_tests", "Debuglog test")