Skip to content

Commit

Permalink
logging: fix delegating lock sink races
Browse files Browse the repository at this point in the history
This fixes two different issues:
1) Previously there was no locking around log sink replacement,
   so it was possibles for a link sink to get removed by one
   thread while getting written to be another thread.
2) Even with locking, the base class destructor pattern would
   do the swap after the derived class was destroyed, leading to
   undefined behavior.

This was easy to reproduce in cx_limit_integration_test but is
an issue anywhere the log expectations are used or death test
stderr workaround for coverage which has been removed because
it no longer logs to a file.

Fixes #11841

Signed-off-by: Matt Klein <mklein@lyft.com>
  • Loading branch information
mattklein123 committed Jul 26, 2020
1 parent defea7e commit 1155250
Show file tree
Hide file tree
Showing 25 changed files with 121 additions and 107 deletions.
29 changes: 23 additions & 6 deletions source/common/common/logger.cc
Original file line number Diff line number Diff line change
Expand Up @@ -19,18 +19,32 @@ namespace Logger {
StandardLogger::StandardLogger(const std::string& name)
: Logger(std::make_shared<spdlog::logger>(name, Registry::getSink())) {}

SinkDelegate::SinkDelegate(DelegatingLogSinkSharedPtr log_sink)
: previous_delegate_(log_sink->delegate()), log_sink_(log_sink) {
log_sink->setDelegate(this);
}
SinkDelegate::SinkDelegate(DelegatingLogSinkSharedPtr log_sink) : log_sink_(log_sink) {}

SinkDelegate::~SinkDelegate() {
assert(log_sink_->delegate() == this); // Ensures stacked allocation of delegates.
// The previous delegate should have never been set or should have been reset by now via
// restoreDelegate();
assert(previous_delegate_ == nullptr);
}

void SinkDelegate::setDelegate() {
// There should be no previous delegate before this call.
assert(previous_delegate_ == nullptr);
previous_delegate_ = log_sink_->delegate();
log_sink_->setDelegate(this);
}

void SinkDelegate::restoreDelegate() {
// Ensures stacked allocation of delegates.
assert(log_sink_->delegate() == this);
log_sink_->setDelegate(previous_delegate_);
previous_delegate_ = nullptr;
}

StderrSinkDelegate::StderrSinkDelegate(DelegatingLogSinkSharedPtr log_sink)
: SinkDelegate(log_sink) {}
: SinkDelegate(log_sink) {
setDelegate();
}

void StderrSinkDelegate::log(absl::string_view msg) {
Thread::OptionalLockGuard guard(lock_);
Expand Down Expand Up @@ -60,6 +74,9 @@ void DelegatingLogSink::log(const spdlog::details::log_msg& msg) {
}
lock.Release();

// Hold the sink mutex while performing the actual logging. This prevents the sink from being
// swapped during an individual log event.
absl::ReaderMutexLock sink_lock(&sink_mutex_);
if (should_escape_) {
sink_->log(escapeLogLine(msg_view));
} else {
Expand Down
33 changes: 27 additions & 6 deletions source/common/common/logger.h
Original file line number Diff line number Diff line change
Expand Up @@ -104,10 +104,21 @@ class SinkDelegate : NonCopyable {
virtual void flush() PURE;

protected:
// Swap the current log sink delegate for this one. This should be called by the derived class
// constructor immediately before returning. This is required to match restoreDelegate(),
// otherwise it's possible for the previous delegate to get set in the base class constructor,
// the derived class constructor throws, and cleanup becomes broken.
void setDelegate();

// Swap the current log sink (this) for the previous one. This should be called by the derived
// class destructor in the body. This is critical as otherwise it's possible for a log message
// to get routed to a partially destructed sink.
void restoreDelegate();

SinkDelegate* previousDelegate() { return previous_delegate_; }

private:
SinkDelegate* previous_delegate_;
SinkDelegate* previous_delegate_{nullptr};
DelegatingLogSinkSharedPtr log_sink_;
};

Expand Down Expand Up @@ -141,7 +152,10 @@ class DelegatingLogSink : public spdlog::sinks::sink {

// spdlog::sinks::sink
void log(const spdlog::details::log_msg& msg) override;
void flush() override { sink_->flush(); }
void flush() override {
absl::ReaderMutexLock lock(&sink_mutex_);
sink_->flush();
}
void set_pattern(const std::string& pattern) override {
set_formatter(spdlog::details::make_unique<spdlog::pattern_formatter>(pattern));
}
Expand Down Expand Up @@ -180,13 +194,20 @@ class DelegatingLogSink : public spdlog::sinks::sink {

DelegatingLogSink() = default;

void setDelegate(SinkDelegate* sink) { sink_ = sink; }
SinkDelegate* delegate() { return sink_; }
void setDelegate(SinkDelegate* sink) {
absl::WriterMutexLock lock(&sink_mutex_);
sink_ = sink;
}
SinkDelegate* delegate() {
absl::ReaderMutexLock lock(&sink_mutex_);
return sink_;
}

SinkDelegate* sink_{nullptr};
SinkDelegate* sink_ ABSL_GUARDED_BY(sink_mutex_){nullptr};
absl::Mutex sink_mutex_;
std::unique_ptr<StderrSinkDelegate> stderr_sink_; // Builtin sink to use as a last resort.
std::unique_ptr<spdlog::formatter> formatter_ ABSL_GUARDED_BY(format_mutex_);
absl::Mutex format_mutex_; // direct absl reference to break build cycle.
absl::Mutex format_mutex_;
bool should_escape_{false};
};

Expand Down
6 changes: 5 additions & 1 deletion source/common/common/logger_delegates.cc
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,11 @@ namespace Logger {
FileSinkDelegate::FileSinkDelegate(const std::string& log_path,
AccessLog::AccessLogManager& log_manager,
DelegatingLogSinkSharedPtr log_sink)
: SinkDelegate(log_sink), log_file_(log_manager.createAccessLog(log_path)) {}
: SinkDelegate(log_sink), log_file_(log_manager.createAccessLog(log_path)) {
setDelegate();
}

FileSinkDelegate::~FileSinkDelegate() { restoreDelegate(); }

void FileSinkDelegate::log(absl::string_view msg) {
// Log files have internal locking to ensure serial, non-interleaved
Expand Down
4 changes: 1 addition & 3 deletions source/common/common/logger_delegates.h
Original file line number Diff line number Diff line change
Expand Up @@ -14,16 +14,14 @@
namespace Envoy {
namespace Logger {

class DelegatingLogSink;
using DelegatingLogSinkSharedPtr = std::shared_ptr<DelegatingLogSink>;

/**
* SinkDelegate that writes log messages to a file.
*/
class FileSinkDelegate : public SinkDelegate {
public:
FileSinkDelegate(const std::string& log_path, AccessLog::AccessLogManager& log_manager,
DelegatingLogSinkSharedPtr log_sink);
~FileSinkDelegate();

// SinkDelegate
void log(absl::string_view msg) override;
Expand Down
1 change: 0 additions & 1 deletion test/common/buffer/owned_impl_test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -1034,7 +1034,6 @@ TEST_F(OwnedImplTest, ReadReserveAndCommit) {
}

TEST(OverflowDetectingUInt64, Arithmetic) {
Logger::StderrSinkDelegate stderr_sink(Logger::Registry::getSink()); // For coverage build.
OverflowDetectingUInt64 length;
length += 1;
length -= 1;
Expand Down
1 change: 0 additions & 1 deletion test/common/common/assert_test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@
namespace Envoy {

TEST(ReleaseAssertDeathTest, VariousLogs) {
Logger::StderrSinkDelegate stderr_sink(Logger::Registry::getSink()); // For coverage build.
EXPECT_DEATH({ RELEASE_ASSERT(0, ""); }, ".*assert failure: 0.*");
EXPECT_DEATH({ RELEASE_ASSERT(0, "With some logs"); },
".*assert failure: 0. Details: With some logs.*");
Expand Down
8 changes: 4 additions & 4 deletions test/common/http/header_map_impl_test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -1005,14 +1005,14 @@ TEST(HeaderMapImplTest, TestAppendHeader) {
TEST(TestHeaderMapImplDeathTest, TestHeaderLengthChecks) {
HeaderString value;
value.setCopy("some;");
EXPECT_DEATH_LOG_TO_STDERR(value.append(nullptr, std::numeric_limits<uint32_t>::max()),
"Trying to allocate overly large headers.");
EXPECT_DEATH(value.append(nullptr, std::numeric_limits<uint32_t>::max()),
"Trying to allocate overly large headers.");

std::string source("hello");
HeaderString reference;
reference.setReference(source);
EXPECT_DEATH_LOG_TO_STDERR(reference.append(nullptr, std::numeric_limits<uint32_t>::max()),
"Trying to allocate overly large headers.");
EXPECT_DEATH(reference.append(nullptr, std::numeric_limits<uint32_t>::max()),
"Trying to allocate overly large headers.");
}

TEST(HeaderMapImplTest, PseudoHeaderOrder) {
Expand Down
1 change: 0 additions & 1 deletion test/common/http/http2/metadata_encoder_decoder_test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -333,7 +333,6 @@ using MetadataEncoderDecoderDeathTest = MetadataEncoderDecoderTest;

// Crash if a caller tries to pack more frames than the encoder has data for.
TEST_F(MetadataEncoderDecoderDeathTest, PackTooManyFrames) {
Logger::StderrSinkDelegate stderr_sink(Logger::Registry::getSink()); // For coverage build.
MetadataMap metadata_map = {
{"header_key1", std::string(5, 'a')},
{"header_key2", std::string(5, 'b')},
Expand Down
17 changes: 8 additions & 9 deletions test/common/network/address_impl_test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -448,9 +448,9 @@ TEST(AddressFromSockAddrDeathTest, IPv4) {
EXPECT_EQ(1, inet_pton(AF_INET, "1.2.3.4", &sin.sin_addr));
sin.sin_port = htons(6502);

EXPECT_DEATH_LOG_TO_STDERR(addressFromSockAddr(ss, 1), "ss_len");
EXPECT_DEATH_LOG_TO_STDERR(addressFromSockAddr(ss, sizeof(sockaddr_in) - 1), "ss_len");
EXPECT_DEATH_LOG_TO_STDERR(addressFromSockAddr(ss, sizeof(sockaddr_in) + 1), "ss_len");
EXPECT_DEATH(addressFromSockAddr(ss, 1), "ss_len");
EXPECT_DEATH(addressFromSockAddr(ss, sizeof(sockaddr_in) - 1), "ss_len");
EXPECT_DEATH(addressFromSockAddr(ss, sizeof(sockaddr_in) + 1), "ss_len");

EXPECT_EQ("1.2.3.4:6502", addressFromSockAddr(ss, sizeof(sockaddr_in))->asString());

Expand All @@ -467,9 +467,9 @@ TEST(AddressFromSockAddrDeathTest, IPv6) {
EXPECT_EQ(1, inet_pton(AF_INET6, "01:023::00Ef", &sin6.sin6_addr));
sin6.sin6_port = htons(32000);

EXPECT_DEATH_LOG_TO_STDERR(addressFromSockAddr(ss, 1), "ss_len");
EXPECT_DEATH_LOG_TO_STDERR(addressFromSockAddr(ss, sizeof(sockaddr_in6) - 1), "ss_len");
EXPECT_DEATH_LOG_TO_STDERR(addressFromSockAddr(ss, sizeof(sockaddr_in6) + 1), "ss_len");
EXPECT_DEATH(addressFromSockAddr(ss, 1), "ss_len");
EXPECT_DEATH(addressFromSockAddr(ss, sizeof(sockaddr_in6) - 1), "ss_len");
EXPECT_DEATH(addressFromSockAddr(ss, sizeof(sockaddr_in6) + 1), "ss_len");

EXPECT_EQ("[1:23::ef]:32000", addressFromSockAddr(ss, sizeof(sockaddr_in6))->asString());

Expand All @@ -490,9 +490,8 @@ TEST(AddressFromSockAddrDeathTest, Pipe) {

StringUtil::strlcpy(sun.sun_path, "/some/path", sizeof sun.sun_path);

EXPECT_DEATH_LOG_TO_STDERR(addressFromSockAddr(ss, 1), "ss_len");
EXPECT_DEATH_LOG_TO_STDERR(addressFromSockAddr(ss, offsetof(struct sockaddr_un, sun_path)),
"ss_len");
EXPECT_DEATH(addressFromSockAddr(ss, 1), "ss_len");
EXPECT_DEATH(addressFromSockAddr(ss, offsetof(struct sockaddr_un, sun_path)), "ss_len");

socklen_t ss_len = offsetof(struct sockaddr_un, sun_path) + 1 + strlen(sun.sun_path);
EXPECT_EQ("/some/path", addressFromSockAddr(ss, ss_len)->asString());
Expand Down
2 changes: 1 addition & 1 deletion test/common/network/connection_impl_test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ TEST_P(ConnectionImplDeathTest, BadFd) {
Event::DispatcherPtr dispatcher(api->allocateDispatcher("test_thread"));
IoHandlePtr io_handle = std::make_unique<IoSocketHandleImpl>();
StreamInfo::StreamInfoImpl stream_info(dispatcher->timeSource());
EXPECT_DEATH_LOG_TO_STDERR(
EXPECT_DEATH(
ConnectionImpl(*dispatcher,
std::make_unique<ConnectionSocketImpl>(std::move(io_handle), nullptr, nullptr),
Network::Test::createRawBufferSocket(), stream_info, false),
Expand Down
2 changes: 1 addition & 1 deletion test/common/network/listener_impl_test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ INSTANTIATE_TEST_SUITE_P(IpVersions, ListenerImplDeathTest,
testing::ValuesIn(TestEnvironment::getIpVersionsForTest()),
TestUtility::ipTestParamsToString);
TEST_P(ListenerImplDeathTest, ErrorCallback) {
EXPECT_DEATH_LOG_TO_STDERR(errorCallbackTest(GetParam()), ".*listener accept failure.*");
EXPECT_DEATH(errorCallbackTest(GetParam()), ".*listener accept failure.*");
}

class TestListenerImpl : public ListenerImpl {
Expand Down
1 change: 0 additions & 1 deletion test/common/network/udp_listener_impl_test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -390,7 +390,6 @@ TEST_P(UdpListenerImplTest, SendData) {
* The send fails because the server_socket is created with bind=false.
*/
TEST_P(UdpListenerImplTest, SendDataError) {
Logger::StderrSinkDelegate stderr_sink(Logger::Registry::getSink()); // For coverage build.
const std::string payload("hello world");
Buffer::InstancePtr buffer(new Buffer::OwnedImpl());
buffer->add(payload);
Expand Down
14 changes: 7 additions & 7 deletions test/common/signal/signals_test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ namespace Envoy {
#ifndef ASANITIZED
TEST(SignalsDeathTest, InvalidAddressDeathTest) {
SignalAction actions;
EXPECT_DEATH_LOG_TO_STDERR(
EXPECT_DEATH(
[]() -> void {
// Oops!
volatile int* nasty_ptr = reinterpret_cast<int*>(0x0);
Expand All @@ -44,7 +44,7 @@ TEST(SignalsDeathTest, RegisteredHandlerTest) {
SignalAction::registerFatalErrorHandler(handler);
SignalAction actions;
// Make sure the fatal error log "HERE" registered above is logged on fatal error.
EXPECT_DEATH_LOG_TO_STDERR(
EXPECT_DEATH(
[]() -> void {
// Oops!
volatile int* nasty_ptr = reinterpret_cast<int*>(0x0);
Expand All @@ -56,7 +56,7 @@ TEST(SignalsDeathTest, RegisteredHandlerTest) {

TEST(SignalsDeathTest, BusDeathTest) {
SignalAction actions;
EXPECT_DEATH_LOG_TO_STDERR(
EXPECT_DEATH(
[]() -> void {
// Bus error is tricky. There's one way that can work on POSIX systems
// described below but it depends on mmaping a file. Just make it easy and
Expand All @@ -72,7 +72,7 @@ TEST(SignalsDeathTest, BusDeathTest) {

TEST(SignalsDeathTest, BadMathDeathTest) {
SignalAction actions;
EXPECT_DEATH_LOG_TO_STDERR(
EXPECT_DEATH(
[]() -> void {
// It turns out to be really hard to not have the optimizer get rid of a
// division by zero. Just raise the signal for this test.
Expand All @@ -85,7 +85,7 @@ TEST(SignalsDeathTest, BadMathDeathTest) {
// Unfortunately we don't have a reliable way to do this on other platforms
TEST(SignalsDeathTest, IllegalInstructionDeathTest) {
SignalAction actions;
EXPECT_DEATH_LOG_TO_STDERR(
EXPECT_DEATH(
[]() -> void {
// Intel defines the "ud2" opcode to be an invalid instruction:
__asm__("ud2");
Expand All @@ -96,7 +96,7 @@ TEST(SignalsDeathTest, IllegalInstructionDeathTest) {

TEST(SignalsDeathTest, AbortDeathTest) {
SignalAction actions;
EXPECT_DEATH_LOG_TO_STDERR([]() -> void { abort(); }(), "backtrace.*Abort(ed)?");
EXPECT_DEATH([]() -> void { abort(); }(), "backtrace.*Abort(ed)?");
}

TEST(SignalsDeathTest, RestoredPreviousHandlerDeathTest) {
Expand All @@ -108,7 +108,7 @@ TEST(SignalsDeathTest, RestoredPreviousHandlerDeathTest) {
// goes out of scope, NOT the default.
}
// Outer SignalAction should be active again:
EXPECT_DEATH_LOG_TO_STDERR([]() -> void { abort(); }(), "backtrace.*Abort(ed)?");
EXPECT_DEATH([]() -> void { abort(); }(), "backtrace.*Abort(ed)?");
}

#endif
Expand Down
3 changes: 1 addition & 2 deletions test/common/singleton/manager_impl_test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,7 @@ static void deathTestWorker() {
}

TEST(SingletonManagerImplDeathTest, NotRegistered) {
EXPECT_DEATH_LOG_TO_STDERR(deathTestWorker(),
"invalid singleton name 'foo'. Make sure it is registered.");
EXPECT_DEATH(deathTestWorker(), "invalid singleton name 'foo'. Make sure it is registered.");
}

SINGLETON_MANAGER_REGISTRATION(test);
Expand Down
16 changes: 7 additions & 9 deletions test/common/upstream/conn_pool_map_impl_test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -401,8 +401,8 @@ TEST_F(ConnPoolMapImplDeathTest, ReentryClearTripsAssert) {
ON_CALL(*mock_pools_[0], addDrainedCallback(_))
.WillByDefault(Invoke([](Http::ConnectionPool::Instance::DrainedCb cb) { cb(); }));

EXPECT_DEATH_LOG_TO_STDERR(test_map->addDrainedCallback([&test_map] { test_map->clear(); }),
".*Details: A resource should only be entered once");
EXPECT_DEATH(test_map->addDrainedCallback([&test_map] { test_map->clear(); }),
".*Details: A resource should only be entered once");
}

TEST_F(ConnPoolMapImplDeathTest, ReentryGetPoolTripsAssert) {
Expand All @@ -412,7 +412,7 @@ TEST_F(ConnPoolMapImplDeathTest, ReentryGetPoolTripsAssert) {
ON_CALL(*mock_pools_[0], addDrainedCallback(_))
.WillByDefault(Invoke([](Http::ConnectionPool::Instance::DrainedCb cb) { cb(); }));

EXPECT_DEATH_LOG_TO_STDERR(
EXPECT_DEATH(
test_map->addDrainedCallback([&test_map, this] { test_map->getPool(2, getBasicFactory()); }),
".*Details: A resource should only be entered once");
}
Expand All @@ -424,9 +424,8 @@ TEST_F(ConnPoolMapImplDeathTest, ReentryDrainConnectionsTripsAssert) {
ON_CALL(*mock_pools_[0], addDrainedCallback(_))
.WillByDefault(Invoke([](Http::ConnectionPool::Instance::DrainedCb cb) { cb(); }));

EXPECT_DEATH_LOG_TO_STDERR(
test_map->addDrainedCallback([&test_map] { test_map->drainConnections(); }),
".*Details: A resource should only be entered once");
EXPECT_DEATH(test_map->addDrainedCallback([&test_map] { test_map->drainConnections(); }),
".*Details: A resource should only be entered once");
}

TEST_F(ConnPoolMapImplDeathTest, ReentryAddDrainedCallbackTripsAssert) {
Expand All @@ -436,9 +435,8 @@ TEST_F(ConnPoolMapImplDeathTest, ReentryAddDrainedCallbackTripsAssert) {
ON_CALL(*mock_pools_[0], addDrainedCallback(_))
.WillByDefault(Invoke([](Http::ConnectionPool::Instance::DrainedCb cb) { cb(); }));

EXPECT_DEATH_LOG_TO_STDERR(
test_map->addDrainedCallback([&test_map] { test_map->addDrainedCallback([]() {}); }),
".*Details: A resource should only be entered once");
EXPECT_DEATH(test_map->addDrainedCallback([&test_map] { test_map->addDrainedCallback([]() {}); }),
".*Details: A resource should only be entered once");
}
#endif // !defined(NDEBUG)

Expand Down
2 changes: 1 addition & 1 deletion test/exe/main_common_test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -161,7 +161,7 @@ TEST_P(MainCommonDeathTest, OutOfMemoryHandler) {
// so disable handling that signal.
signal(SIGABRT, SIG_DFL);
#endif
EXPECT_DEATH_LOG_TO_STDERR(
EXPECT_DEATH(
[]() {
// Allocating a fixed-size large array that results in OOM on gcc
// results in a compile-time error on clang of "array size too big",
Expand Down
2 changes: 1 addition & 1 deletion test/exe/terminate_handler_test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ namespace Envoy {

TEST(TerminateHandlerDeathTest, HandlerInstalledTest) {
TerminateHandler handler;
EXPECT_DEATH_LOG_TO_STDERR([]() -> void { std::terminate(); }(), ".*std::terminate called!.*");
EXPECT_DEATH([]() -> void { std::terminate(); }(), ".*std::terminate called!.*");
}

} // namespace Envoy
Loading

0 comments on commit 1155250

Please sign in to comment.