From 5564196c38c0bde2f28fbb248626010b212a15c8 Mon Sep 17 00:00:00 2001 From: TexLeeV Date: Fri, 3 Jul 2026 10:52:28 -0500 Subject: [PATCH 1/3] asio-module-addition --- CMakeLists.txt | 1 + README.md | 1 + docs/LEARNING_PATH.md | 3 +- learning_asio/CMakeLists.txt | 24 ++ .../tests/test_io_context_basics.cpp | 217 ++++++++++++++ .../tests/test_post_dispatch_strand.cpp | 208 +++++++++++++ .../tests/test_timer_lifetime_uaf.cpp | 282 ++++++++++++++++++ learning_asio/tests/test_timers.cpp | 200 +++++++++++++ 8 files changed, 935 insertions(+), 1 deletion(-) create mode 100644 learning_asio/CMakeLists.txt create mode 100644 learning_asio/tests/test_io_context_basics.cpp create mode 100644 learning_asio/tests/test_post_dispatch_strand.cpp create mode 100644 learning_asio/tests/test_timer_lifetime_uaf.cpp create mode 100644 learning_asio/tests/test_timers.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index e65439f..80659d1 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -39,6 +39,7 @@ add_subdirectory(common) add_subdirectory(examples) add_subdirectory(profile_showcase) +add_subdirectory(learning_asio) add_subdirectory(learning_shared_ptr) add_subdirectory(learning_deadlocks) add_subdirectory(learning_move_semantics) diff --git a/README.md b/README.md index 040bb66..9b0ca49 100644 --- a/README.md +++ b/README.md @@ -125,6 +125,7 @@ Registered test counts match each module’s `CMakeLists.txt` (what `ctest` runs | `learning_stl` | 5 | Containers, iterators, algorithms, invalidation | | `learning_concurrency` | 5 | Threads, mutexes, lock-free basics, pools | | `learning_polymorphism` | 6 | Virtual dispatch, virtual destructors, abstract interfaces, multiple inheritance, RTTI, CRTP / variant / concepts | +| `learning_asio` | 4 | Asio async model: `io_context`, timers, strands, handler-lifetime use-after-free (Boost.Asio; targets pinned to **C++11**) | | `learning_design_patterns` | 4 | Creational, structural, behavioral, modern | | `learning_templates` | 6 | Templates, SFINAE, variadics, traits | | `learning_performance` | 6 | Profiling, cache layout, elision, SSO, constexpr, benchmarks | diff --git a/docs/LEARNING_PATH.md b/docs/LEARNING_PATH.md index 8a08928..dbee677 100644 --- a/docs/LEARNING_PATH.md +++ b/docs/LEARNING_PATH.md @@ -30,6 +30,7 @@ Each row is a `learning_*` directory. **Registered tests** are targets listed in | [learning_stl](../learning_stl/) | 5 | Containers, iterators, algorithms, comparators, invalidation. | | [learning_concurrency](../learning_concurrency/) | 5 | Requires `Threads::Threads`. | | [learning_polymorphism](../learning_polymorphism/) | 6 | Virtual dispatch, destructors, abstract interfaces, diamonds, RTTI, CRTP/variant/concepts. | +| [learning_asio](../learning_asio/) | 4 | Asio async execution model: `io_context`, `steady_timer`, strands, and a handler-lifetime use-after-free lab. Uses Boost.Asio; targets pinned to **C++11** (overrides project C++20). Strand suite is `gcc-tsan`-clean; the UAF repro is a `DISABLED_` test surfaced via `gcc-asan`. | | [learning_design_patterns](../learning_design_patterns/) | 4 | Creational, structural, behavioral, modern idioms. | | [learning_templates](../learning_templates/) | 6 | Specialization, SFINAE, variadics, traits, etc. | | [learning_performance](../learning_performance/) | 6 | Profiling, cache layout, elision, SSO, constexpr, benchmarks. | @@ -51,7 +52,7 @@ Dependencies are soft; adjust for your goals. 3. **Move semantics** — `learning_move_semantics` (pairs well after shared_ptr). 4. **Modern syntax and STL** — `learning_modern_cpp`, `learning_stl`. 5. **Error handling and templates** — `learning_error_handling`, `learning_templates` (order flexible). -6. **Concurrency** — `learning_concurrency` after you are comfortable with mutex/RAII basics. +6. **Concurrency** — `learning_concurrency` after you are comfortable with mutex/RAII basics. Follow with `learning_asio` to see how an event loop (`io_context`) and strands serialize work without explicit locks. 7. **Deadlocks (advanced lab)** — Work through `learning_deadlocks` scenarios one file at a time; some cases are intentionally disabled until remaining fixes are completed. 8. **Polymorphism** — `learning_polymorphism` after move semantics; lays the dispatch/inheritance groundwork the patterns module assumes. 9. **Patterns, performance, debugging** — `learning_design_patterns`, `learning_performance`, `learning_debugging` in any order that matches your projects. diff --git a/learning_asio/CMakeLists.txt b/learning_asio/CMakeLists.txt new file mode 100644 index 0000000..ccc7df9 --- /dev/null +++ b/learning_asio/CMakeLists.txt @@ -0,0 +1,24 @@ +# Asio test suite (Boost.Asio) — authored to a C++11 minimum. +# +# Each target is pinned to C++11 (overriding the project-wide C++20) so the +# learner-facing code stays within classic, callback-based Asio idioms. Boost +# is header-only here: Asio and boost::system need no compiled libraries. + +find_package(Boost REQUIRED) + +set(asio_tests + test_io_context_basics + test_timers + test_post_dispatch_strand + test_timer_lifetime_uaf + test_multi_timer_ownership +) + +foreach(asio_test ${asio_tests}) + add_learning_test(${asio_test} tests/${asio_test}.cpp instrumentation Boost::headers Threads::Threads) + set_target_properties(${asio_test} PROPERTIES + CXX_STANDARD 11 + CXX_STANDARD_REQUIRED ON + CXX_EXTENSIONS OFF + ) +endforeach() diff --git a/learning_asio/tests/test_io_context_basics.cpp b/learning_asio/tests/test_io_context_basics.cpp new file mode 100644 index 0000000..dc60064 --- /dev/null +++ b/learning_asio/tests/test_io_context_basics.cpp @@ -0,0 +1,217 @@ +// Test Suite: io_context Basics +// Estimated Time: 2 hours +// Difficulty: Easy +// C++ Standard: C++11 (Boost.Asio, classic callback style) +// +// The io_context is Asio's execution engine: it owns a queue of completion +// handlers and runs them on whatever thread calls run(). Nothing in Asio +// happens "in the background" by magic -- a handler only executes while some +// thread is inside run()/poll()/run_one(). These scenarios make that +// scheduling visible through EventLog. +// +// Library-evolution note: the free functions boost::asio::post/dispatch/defer +// and the io_context type itself arrived in Boost 1.66 (2017). Before that the +// type was boost::asio::io_service and you called ctx.post(handler) as a member +// function. The scheduling semantics below are the same in both eras. + +#include "instrumentation.h" + +#include + +#include +#include +#include +#include + +class IoContextBasicsTest : public ::testing::Test +{ +protected: + void SetUp() override + { + EventLog::instance().clear(); + } +}; + +// ============================================================================ +// Scenario 1: post() Queues Work That Only Runs Inside run() (Easy) +// ============================================================================ + +TEST_F(IoContextBasicsTest, PostQueuesHandlersUntilRunDrainsThemFifo) +{ + boost::asio::io_context ctx; + + boost::asio::post(ctx, [] { EventLog::instance().record("h1"); }); + boost::asio::post(ctx, [] { EventLog::instance().record("h2"); }); + boost::asio::post(ctx, [] { EventLog::instance().record("h3"); }); + + // Q: At this point three handlers have been posted but run() has not been + // called. How many have executed, and what EventLog evidence confirms it? + // A: + // R: + EXPECT_EQ(EventLog::instance().events().size(), 0u); + + const std::size_t executed = ctx.run(); + + // Q: post() defers work to the thread that calls run(). In what order are + // the three handlers executed, and which observable signal proves that + // ordering rather than your assumption about it? + // A: + // R: + const std::vector ev = EventLog::instance().events(); + ASSERT_EQ(ev.size(), 3u); + EXPECT_EQ(ev[0], "h1"); + EXPECT_EQ(ev[1], "h2"); + EXPECT_EQ(ev[2], "h3"); + + // Q: run() returns a std::size_t. What does that number count here? + // A: + // R: + EXPECT_EQ(executed, 3u); +} + +// ============================================================================ +// Scenario 2: run() Returns the Number of Handlers It Executed (Easy) +// ============================================================================ + +TEST_F(IoContextBasicsTest, RunReturnsHandlerCountAndZeroWhenIdle) +{ + boost::asio::io_context ctx; + + // Q: No work has been posted. Why does run() return immediately instead of + // blocking forever waiting for something to do? + // A: + // R: + const std::size_t none = ctx.run(); + EXPECT_EQ(none, 0u); + + // Nothing executed, so EventLog is still empty. + EXPECT_EQ(EventLog::instance().events().size(), 0u); +} + +// ============================================================================ +// Scenario 3: A Drained Context Is "stopped" Until restart() (Moderate) +// ============================================================================ + +TEST_F(IoContextBasicsTest, DrainedContextRequiresRestartBeforeReuse) +{ + boost::asio::io_context ctx; + + boost::asio::post(ctx, [] { EventLog::instance().record("first"); }); + const std::size_t first_run = ctx.run(); + EXPECT_EQ(first_run, 1u); + EXPECT_EQ(EventLog::instance().count_events("first"), 1u); + + // Q: After run() drains all work, what does ctx.stopped() report, and what + // does that imply for the next call to run()? + // A: + // R: + EXPECT_TRUE(ctx.stopped()); + + // Post more work, then try to run WITHOUT restarting first. + boost::asio::post(ctx, [] { EventLog::instance().record("second"); }); + const std::size_t reused_without_restart = ctx.run(); + + // Q: "second" was posted before this run(). Why does run() return 0 and why + // does EventLog show "second" did NOT execute yet? + // A: + // R: + EXPECT_EQ(reused_without_restart, 0u); + EXPECT_EQ(EventLog::instance().count_events("second"), 0u); + + // restart() clears the stopped state so the context can run again. The work + // posted earlier was never discarded -- it was only waiting for a live run. + ctx.restart(); + const std::size_t after_restart = ctx.run(); + + // Q: run() now returns 1 and "second" finally executes. What single call + // made the difference, and what state did it reset? + // A: + // R: + EXPECT_EQ(after_restart, 1u); + EXPECT_EQ(EventLog::instance().count_events("second"), 1u); +} + +// ============================================================================ +// Scenario 4: dispatch() May Run Inline; post() Always Defers (Moderate) +// ============================================================================ + +TEST_F(IoContextBasicsTest, DispatchRunsInlineWhilePostDefers) +{ + boost::asio::io_context ctx; + + // The outer handler runs while we are inside ctx.run() below. From inside + // it, we both post() and dispatch() additional handlers. + boost::asio::post(ctx, [&ctx] { + EventLog::instance().record("outer-start"); + boost::asio::post(ctx, [] { EventLog::instance().record("posted"); }); + boost::asio::dispatch(ctx, [] { EventLog::instance().record("dispatched"); }); + EventLog::instance().record("outer-end"); + }); + + ctx.run(); + + // Q: dispatch() is allowed to invoke its handler immediately when the caller + // is already running on the io_context. Given that, where does + // "dispatched" fall relative to "outer-end", and where does "posted" + // fall? Predict the full order before reading the expectations below. + // A: + // R: + const std::vector ev = EventLog::instance().events(); + ASSERT_EQ(ev.size(), 4u); + EXPECT_EQ(ev[0], "outer-start"); + EXPECT_EQ(ev[1], "dispatched"); + EXPECT_EQ(ev[2], "outer-end"); + EXPECT_EQ(ev[3], "posted"); + + // Q: What observable difference would you expect if "dispatched" were + // changed to another post()? Which two entries would swap? + // A: + // R: + + // TODO (learner): Change the inner boost::asio::dispatch(...) to + // boost::asio::post(...) and re-run. Record the new order in the // A: + // lines above and explain why dispatch's inline shortcut disappeared. +} + +// ============================================================================ +// Scenario 5: poll() Runs Only Ready Work and Returns Without Blocking (Hard) +// ============================================================================ + +TEST_F(IoContextBasicsTest, PollRunsReadyHandlersThenReturns) +{ + boost::asio::io_context ctx; + + // A handler that, when run, posts a *second* handler. The second one is not + // "ready" until the first one has executed and queued it. + boost::asio::post(ctx, [&ctx] { + EventLog::instance().record("stage1"); + boost::asio::post(ctx, [] { EventLog::instance().record("stage2"); }); + }); + + // Q: poll() executes all handlers that are ready *right now* and then + // returns. "stage2" is only queued once "stage1" runs. Does a single + // poll() reach "stage2"? Predict the count it returns. + // A: + // R: + const std::size_t first_poll = ctx.poll(); + + // Both stage1 and stage2 ran in this poll: once stage1 executed, stage2 was + // queued and was still "ready" within the same poll() pass. + EXPECT_EQ(first_poll, 2u); + EXPECT_EQ(EventLog::instance().count_events("stage1"), 1u); + EXPECT_EQ(EventLog::instance().count_events("stage2"), 1u); + + // Q: poll() differs from run() in what it does when *no* handler is ready. + // With the queue now empty, what does a second poll() return, and why is + // that the key behavioral contrast with run() in a blocking design? + // A: + // R: + ctx.restart(); + const std::size_t second_poll = ctx.poll(); + EXPECT_EQ(second_poll, 0u); + + // TODO (learner): Replace the first poll() with run() and confirm the + // EventLog counts are identical here. Then describe one situation (hint: + // long-lived asynchronous work such as a timer or socket) where run() and + // poll() would behave very differently. Record your answer in // A: above. +} diff --git a/learning_asio/tests/test_post_dispatch_strand.cpp b/learning_asio/tests/test_post_dispatch_strand.cpp new file mode 100644 index 0000000..7c486ab --- /dev/null +++ b/learning_asio/tests/test_post_dispatch_strand.cpp @@ -0,0 +1,208 @@ +// Test Suite: Strands, post(), and dispatch() +// Estimated Time: 3-4 hours +// Difficulty: Moderate +// C++ Standard: C++11 (Boost.Asio, classic callback style) +// +// When several threads call io_context::run() on the SAME context, completion +// handlers can execute concurrently. That is great for throughput and terrible +// for any shared mutable state. A strand is Asio's answer: handlers submitted +// through one strand are guaranteed never to run concurrently with each other, +// and the strand also establishes the memory ordering needed for the data they +// touch. A strand is "a mutex you never lock" -- serialization without a lock +// held across the handler. +// +// Recommended: run this suite under the gcc-tsan preset +// cmake --preset gcc-tsan +// cmake --build --preset gcc-tsan --target test_post_dispatch_strand +// ctest --preset gcc-tsan -R test_post_dispatch_strand --output-on-failure +// The plain (non-atomic) counter in Scenario 1 is touched from a 4-thread pool; +// it is race-free ONLY because the strand serializes the handlers. Remove the +// strand and ThreadSanitizer will report the race that proves why strands exist. + +#include "instrumentation.h" + +#include + +#include +#include +#include +#include +#include +#include + +class StrandTest : public ::testing::Test +{ +protected: + void SetUp() override + { + EventLog::instance().clear(); + } +}; + +// ============================================================================ +// Scenario 1: A Strand Serializes Handlers So They Never Overlap (Moderate) +// ============================================================================ + +TEST_F(StrandTest, StrandSerializesHandlersOnAThreadPool) +{ + const int kHandlers = 2000; + const int kThreads = 4; + + boost::asio::io_context ctx; + auto strand = boost::asio::make_strand(ctx); + + // Deliberately a PLAIN int, not a std::atomic. Its correctness depends + // entirely on the strand preventing concurrent handler execution. + int unsynchronized_counter = 0; + + // These atomics only *observe* concurrency; they are not what we are + // testing. active = handlers currently executing; max_active = the peak. + std::atomic active(0); + std::atomic max_active(0); + + for (int i = 0; i < kHandlers; ++i) + { + boost::asio::post(strand, [&] { + const int now = active.fetch_add(1) + 1; + int observed = max_active.load(); + while (now > observed && !max_active.compare_exchange_weak(observed, now)) + { + } + + ++unsynchronized_counter; + + // Record each worker thread once so EventLog shows the pool really + // was multi-threaded (count is 1..kThreads, scheduler-dependent). + thread_local bool noted = false; + if (!noted) + { + noted = true; + EventLog::instance().record("worker-thread"); + } + + active.fetch_sub(1); + }); + } + + std::vector pool; + for (int t = 0; t < kThreads; ++t) + { + pool.emplace_back([&ctx] { ctx.run(); }); + } + for (std::size_t t = 0; t < pool.size(); ++t) + { + pool[t].join(); + } + + // Q: Four threads ran handlers that each did a non-atomic ++ on the same + // int. Why does the final value equal kHandlers exactly, with no lost + // updates, despite there being no mutex and no atomic on that counter? + // A: + // R: + EXPECT_EQ(unsynchronized_counter, kHandlers); + + // Q: max_active is the largest number of handlers seen running at the same + // instant. What value must it be if the strand did its job, and what + // would a value greater than that have proven? + // A: + // R: + EXPECT_EQ(max_active.load(), 1); + + // The pool was genuinely multi-threaded (at least one worker ran handlers). + const std::size_t workers = EventLog::instance().count_events("worker-thread"); + EXPECT_GE(workers, 1u); + EXPECT_LE(workers, static_cast(kThreads)); + + // TODO (learner): Build this file under the gcc-tsan preset, then remove the + // strand (post directly to `ctx` instead of `strand`) and rebuild. Record + // in // A: above the exact ThreadSanitizer report you get, and identify + // which two operations it names as racing. +} + +// ============================================================================ +// Scenario 2: A Strand Preserves Submission Order Across Threads (Moderate) +// ============================================================================ + +TEST_F(StrandTest, StrandPreservesPostOrderEvenOnAPool) +{ + boost::asio::io_context ctx; + auto strand = boost::asio::make_strand(ctx); + + // Posted in a fixed order from this single thread, then drained by a pool. + boost::asio::post(strand, [] { EventLog::instance().record("s1"); }); + boost::asio::post(strand, [] { EventLog::instance().record("s2"); }); + boost::asio::post(strand, [] { EventLog::instance().record("s3"); }); + boost::asio::post(strand, [] { EventLog::instance().record("s4"); }); + boost::asio::post(strand, [] { EventLog::instance().record("s5"); }); + + std::vector pool; + for (int t = 0; t < 4; ++t) + { + pool.emplace_back([&ctx] { ctx.run(); }); + } + for (std::size_t t = 0; t < pool.size(); ++t) + { + pool[t].join(); + } + + // Q: Four worker threads competed to run these handlers, yet the EventLog + // order is fully determined. What ordering guarantee does a strand make + // for handlers submitted via post() from the same thread, and why is + // that stronger than what a plain mutex would give you? + // A: + // R: + std::cout << EventLog::instance().dump() << std::endl; + const std::vector ev = EventLog::instance().events(); + // ASSERT_EQ(ev.size(), 5u); + // EXPECT_EQ(ev[0], "s1"); + // EXPECT_EQ(ev[1], "s2"); + // EXPECT_EQ(ev[2], "s3"); + // EXPECT_EQ(ev[3], "s4"); + // EXPECT_EQ(ev[4], "s5"); +} + +// ============================================================================ +// Scenario 3: dispatch() on Your Own Strand Runs Inline; post() Defers (Moderate) +// ============================================================================ + +TEST_F(StrandTest, DispatchOnOwnStrandRunsInlineWhilePostDefers) +{ + boost::asio::io_context ctx; + auto strand = boost::asio::make_strand(ctx); + + // Single-threaded run() keeps this fully deterministic. The outer handler + // is already executing *inside* the strand when it calls post and dispatch. + boost::asio::post(strand, [&strand] { + EventLog::instance().record("outer-start"); + boost::asio::post(strand, [] { EventLog::instance().record("posted"); }); + boost::asio::dispatch(strand, [] { EventLog::instance().record("dispatched"); }); + EventLog::instance().record("outer-end"); + }); + + ctx.run(); + + // Q: dispatch() may run its handler immediately when the caller is already + // inside the target strand (running inline cannot violate the + // no-concurrent-execution guarantee). Given that, where does + // "dispatched" land relative to "outer-end", and where does "posted" + // land? Predict the order before reading the expectations. + // A: + // R: + const std::vector ev = EventLog::instance().events(); + ASSERT_EQ(ev.size(), 4u); + EXPECT_EQ(ev[0], "outer-start"); + EXPECT_EQ(ev[1], "dispatched"); + EXPECT_EQ(ev[2], "outer-end"); + EXPECT_EQ(ev[3], "posted"); + + // Q: Suppose a SECOND thread were also running this io_context and called + // dispatch() on this strand while the outer handler was mid-flight. Would + // dispatch still run inline on that other thread? What rule about strand + // membership decides inline-vs-deferred? + // A: + // R: + + // TODO (learner): Change the inner dispatch() to post() and predict which + // two EventLog entries swap. Record the prediction in // A: above, then + // verify by rebuilding. +} diff --git a/learning_asio/tests/test_timer_lifetime_uaf.cpp b/learning_asio/tests/test_timer_lifetime_uaf.cpp new file mode 100644 index 0000000..da7a3ea --- /dev/null +++ b/learning_asio/tests/test_timer_lifetime_uaf.cpp @@ -0,0 +1,282 @@ +// Test Suite: Timer Handler Lifetime -- Use-After-Free and How to Fix It +// Estimated Time: 3-4 hours +// Difficulty: Hard +// C++ Standard: C++11 (Boost.Asio, classic callback style) +// +// THE HAZARD +// ---------- +// An asynchronous operation outlives the statement that started it. A timer's +// completion handler runs *later*, when some thread is inside io_context::run(). +// If that handler captured a raw `this` (or a reference/pointer into an object) +// and the object is destroyed before the handler runs, the handler dereferences +// freed memory: a use-after-free. +// +// The trap is sharpened by a fact from test_timers.cpp: destroying a timer (for +// example because its owner is being destroyed) does NOT silently drop the +// pending wait. It CANCELS it, which *queues the handler to run with +// boost::asio::error::operation_aborted*. So the handler still runs -- now with +// a dangling `this`. A handler that touches members before (or instead of) +// checking the error_code walks straight off the cliff. +// +// HOW TO REPRODUCE THE CRASH (the disabled test below) +// ---------------------------------------------------- +// The reproduction is marked DISABLED_ so the normal suite stays green. To see +// the failure the way you would when triaging a legacy crash, run it under the +// AddressSanitizer preset: +// +// cmake --preset gcc-asan +// cmake --build --preset gcc-asan --target test_timer_lifetime_uaf +// ./build/gcc-asan/learning_asio/test_timer_lifetime_uaf \ +// --gtest_also_run_disabled_tests \ +// --gtest_filter='*RawThisHandlerUseAfterFree*' +// +// AddressSanitizer prints "heap-use-after-free", a READ of size 4, and -- most +// usefully -- names the lambda inside RawThisReporter::start() as the access +// site, plus the "freed by" stack (the owner's destruction). That report is the +// thread you pull on to fix the real bug. + +#include "instrumentation.h" + +#include + +#include +#include +#include +#include +#include +#include + +class TimerLifetimeTest : public ::testing::Test +{ +protected: + void SetUp() override + { + EventLog::instance().clear(); + } +}; + +// ============================================================================ +// BROKEN owner: captures a raw `this` into its timer handler. +// ============================================================================ + +class RawThisReporter +{ +public: + explicit RawThisReporter(boost::asio::io_context& ctx) + : timer_(ctx, std::chrono::milliseconds(1)) + , tick_count_(nullptr) + { + tick_count_ = std::make_shared(5); + } + + ~RawThisReporter() + { + // Destroying the member timer cancels the pending async_wait, which + // QUEUES on_tick() to run later with operation_aborted -- after `this` + // is gone. + EventLog::instance().record("RawThisReporter::dtor"); + } + + void start() + { + // The defect: the handler captures the bare `this` pointer. Its + // lifetime is now silently coupled to an object it does not own. + timer_.async_wait([this](const boost::system::error_code& ec) { on_tick(ec); }); + } + +private: + void on_tick(const boost::system::error_code& /*ec*/) + { + // Legacy-style handler: it touches members BEFORE consulting the + // error_code. By the time this runs, `this` may already be freed, so + // every access below is undefined behavior. + ++(*tick_count_); + EventLog::instance().record("RawThisReporter::on_tick magic=" + std::to_string(magic_)); + } + + boost::asio::steady_timer timer_; + int magic_ = 0x1234; + std::shared_ptr tick_count_; +}; + +// ============================================================================ +// Scenario 1: Reproduce the Use-After-Free (Hard) -- DISABLED by default +// ============================================================================ + +TEST_F(TimerLifetimeTest, DISABLED_RawThisHandlerUseAfterFreeWhenOwnerDiesFirst) +{ + boost::asio::io_context ctx; + + RawThisReporter* reporter = new RawThisReporter(ctx); + reporter->start(); + + // Destroy the owner while the wait is still pending. This cancels the timer, + // which queues on_tick() to run with operation_aborted -- now pointing at + // freed memory. + delete reporter; + + // Q: This test has no EXPECT assertions -- its only "result" is the runtime + // report. Under the AddressSanitizer preset, what access triggers the + // heap-use-after-free, and which source line does the "freed by" stack + // name as the cause? + // A: + // R: + ctx.run(); +} + +// ============================================================================ +// FIX 1: enable_shared_from_this -- the handler co-owns the object. +// ============================================================================ + +class SharedReporter : public std::enable_shared_from_this +{ +public: + explicit SharedReporter(boost::asio::io_context& ctx) + : timer_(ctx, std::chrono::milliseconds(1)) + { + } + + ~SharedReporter() + { + EventLog::instance().record("SharedReporter::dtor"); + } + + void start() + { + // Capture a shared_ptr to self. While the handler is queued, this + // shared_ptr keeps the object alive -- the object cannot be destroyed + // until the handler (and thus the captured shared_ptr) is gone. + std::shared_ptr self = shared_from_this(); + timer_.async_wait([self](const boost::system::error_code& /*ec*/) { + EventLog::instance().record("SharedReporter::on_tick"); + }); + } + +private: + boost::asio::steady_timer timer_; +}; + +// ============================================================================ +// Scenario 2: shared_from_this Extends Lifetime Until the Handler Runs (Hard) +// ============================================================================ + +TEST_F(TimerLifetimeTest, SharedFromThisExtendsLifetimeUntilHandlerCompletes) +{ + boost::asio::io_context ctx; + + { + std::shared_ptr reporter = std::make_shared(ctx); + reporter->start(); + // Our local reference goes out of scope HERE -- but the object does not + // die, because the queued handler holds its own shared_ptr copy. + } + + EventLog::instance().record("before-run"); + + // Q: At this point our only named reference to the reporter is gone. Why is + // the object still alive, and what exactly is keeping it alive? + // A: + // R: + ctx.run(); + + // The decisive observable: the handler ran, and the destructor ran AFTER + // it -- never before. + const std::vector ev = EventLog::instance().events(); + ASSERT_EQ(ev.size(), 3u); + EXPECT_EQ(ev[0], "before-run"); + EXPECT_EQ(ev[1], "SharedReporter::on_tick"); + EXPECT_EQ(ev[2], "SharedReporter::dtor"); + + // Q: The destructor entry appears immediately after on_tick, not at end of + // scope above. What event drops the final reference count to zero and + // triggers destruction right there? + // A: + // R: + + // TODO (learner): Change SharedReporter::start() to capture a raw `this` + // instead of `self` (as RawThisReporter does), then re-run under the gcc-asan + // preset. Record what changes in the // A: lines above and explain why the + // "dtor runs after on_tick" ordering can no longer be guaranteed. +} + +// ============================================================================ +// FIX 2: weak_ptr guard -- let the owner die, but detect it safely. +// ============================================================================ + +class GuardedReporter : public std::enable_shared_from_this +{ +public: + explicit GuardedReporter(boost::asio::io_context& ctx) + : timer_(ctx, std::chrono::seconds(60)) + { + } + + ~GuardedReporter() + { + EventLog::instance().record("GuardedReporter::dtor"); + } + + void start() + { + // Capture a weak_ptr, not a shared_ptr. This deliberately does NOT keep + // the object alive -- it lets the owner be destroyed on schedule, but + // gives the handler a safe way to ask "are you still there?". + std::weak_ptr weak_self = shared_from_this(); + timer_.async_wait([weak_self](const boost::system::error_code& /*ec*/) { + std::shared_ptr self = weak_self.lock(); + if (!self) + { + EventLog::instance().record("GuardedReporter::owner-gone-bail"); + return; + } + EventLog::instance().record("GuardedReporter::touched-owner"); + }); + } + +private: + boost::asio::steady_timer timer_; +}; + +// ============================================================================ +// Scenario 3: weak_ptr Lets the Owner Die and the Handler Bails Safely (Hard) +// ============================================================================ + +TEST_F(TimerLifetimeTest, WeakPtrGuardLetsOwnerDieAndHandlerBailsSafely) +{ + boost::asio::io_context ctx; + + std::shared_ptr reporter = std::make_shared(ctx); + reporter->start(); + + // Destroy the owner while the wait is pending. Unlike FIX 1, nothing keeps + // it alive, so the destructor runs now; the timer is cancelled and the + // handler is queued with operation_aborted. + reporter.reset(); + + // Q: This is the exact setup that crashed RawThisReporter: owner dead, + // handler still queued. Why is it safe here? What does weak_self.lock() + // return now, and what does the handler do as a result? + // A: + // R: + ctx.run(); + + const std::vector ev = EventLog::instance().events(); + ASSERT_EQ(ev.size(), 2u); + EXPECT_EQ(ev[0], "GuardedReporter::dtor"); + EXPECT_EQ(ev[1], "GuardedReporter::owner-gone-bail"); + EXPECT_EQ(EventLog::instance().count_events("GuardedReporter::touched-owner"), 0u); + + // Q: Compare the two fixes. shared_from_this guarantees the work completes + // by keeping the object alive; weak_ptr lets the object die and abandons + // the work. For a periodic health-reporter being shut down, which + // semantics do you want, and what observable signal would you assert to + // prove you got it? + // A: + // R: + + // TODO (learner): Change GuardedReporter::start() to capture a shared_ptr + // (as SharedReporter does) instead of a weak_ptr, then re-run. Record which + // EventLog entries change -- especially the dtor's position relative to the + // handler -- in the // A: lines above, and explain why the + // "owner-gone-bail" path can no longer be reached. +} diff --git a/learning_asio/tests/test_timers.cpp b/learning_asio/tests/test_timers.cpp new file mode 100644 index 0000000..1120c14 --- /dev/null +++ b/learning_asio/tests/test_timers.cpp @@ -0,0 +1,200 @@ +// Test Suite: Asio Timers +// Estimated Time: 2-3 hours +// Difficulty: Easy -> Moderate +// C++ Standard: C++11 (Boost.Asio, classic callback style) +// +// A steady_timer is the simplest asynchronous operation in Asio: it completes +// after a duration elapses, or earlier if it is cancelled. Its completion +// handler has the signature void(const boost::system::error_code&). The +// error_code is the channel through which Asio reports *why* the handler ran -- +// natural expiry vs. cancellation -- so reading it is the whole point. +// +// Key idea reinforced throughout: a cancelled asynchronous operation does NOT +// silently vanish. Its handler is still invoked, but with +// boost::asio::error::operation_aborted. Handlers are a promise the library +// always keeps. + +#include "instrumentation.h" + +#include + +#include +#include +#include +#include +#include +#include + +class TimerTest : public ::testing::Test +{ +protected: + void SetUp() override + { + EventLog::instance().clear(); + } +}; + +// ============================================================================ +// Scenario 1: A Timer That Expires Reports Success (Easy) +// ============================================================================ + +TEST_F(TimerTest, ExpiredTimerInvokesHandlerWithSuccessCode) +{ + boost::asio::io_context ctx; + boost::asio::steady_timer timer(ctx, std::chrono::milliseconds(1)); + + boost::system::error_code observed; + timer.async_wait([&observed](const boost::system::error_code& ec) { + observed = ec; + EventLog::instance().record("timer:fired"); + }); + + // Q: At this line the handler has been *registered* but not run. What must + // happen before "timer:fired" can appear in EventLog? + // A: + // R: + EXPECT_EQ(EventLog::instance().count_events("timer:fired"), 0u); + + ctx.run(); + + // Q: The timer expired normally. What does the error_code passed to the + // handler evaluate to in a boolean context, and what is its numeric + // value() for a successful wait? + // A: + // R: + EXPECT_EQ(EventLog::instance().count_events("timer:fired"), 1u); + EXPECT_FALSE(observed); + EXPECT_EQ(observed.value(), 0); +} + +// ============================================================================ +// Scenario 2: Cancelling a Pending Timer Still Runs Its Handler (Easy->Moderate) +// ============================================================================ + +TEST_F(TimerTest, CancelDeliversOperationAbortedNotSilence) +{ + boost::asio::io_context ctx; + + // Arm the timer far in the future so it cannot expire on its own during the + // test. We will cancel it before it ever has the chance. + boost::asio::steady_timer timer(ctx, std::chrono::seconds(60)); + + boost::system::error_code observed; + timer.async_wait([&observed](const boost::system::error_code& ec) { + observed = ec; + EventLog::instance().record("timer:handler-ran"); + }); + + const std::size_t cancelled = timer.cancel(); + + // Q: cancel() returns how many pending operations it stopped here, and at + // this exact point (before run()) has the handler executed yet? + // A: + // R: + EXPECT_EQ(cancelled, 1u); + EXPECT_EQ(EventLog::instance().count_events("timer:handler-ran"), 0u); + + ctx.run(); + + // Q: run() returns almost immediately rather than waiting 60 seconds, and + // the handler DID run. What error_code did it receive, and what does + // that tell you about how Asio treats cancellation versus dropping work? + // A: + // R: + EXPECT_EQ(EventLog::instance().count_events("timer:handler-ran"), 1u); + EXPECT_TRUE(observed); + EXPECT_EQ(observed, boost::asio::error::operation_aborted); + + // TODO (learner): Many real handlers begin with + // if (ec == boost::asio::error::operation_aborted) return; + // Explain in // A: above why omitting that early-return is a common source + // of use-after-free bugs when the object owning the timer is being torn + // down. What observable signal would betray such a bug? +} + +// ============================================================================ +// Scenario 3: Multiple Timers Complete in Expiry Order, Not Arming Order (Moderate) +// ============================================================================ + +TEST_F(TimerTest, MultipleTimersFireInExpiryOrder) +{ + boost::asio::io_context ctx; + + // Note the arming order: "slow" is created and waited on FIRST, but is set + // to expire LATER than "fast". + boost::asio::steady_timer slow(ctx, std::chrono::milliseconds(40)); + boost::asio::steady_timer fast(ctx, std::chrono::milliseconds(10)); + + slow.async_wait([](const boost::system::error_code&) { + EventLog::instance().record("slow"); + }); + fast.async_wait([](const boost::system::error_code&) { + EventLog::instance().record("fast"); + }); + + ctx.run(); + + // Q: Both handlers run, but in which order? Is the ordering decided by the + // sequence in which async_wait was called, or by expiry time? Which + // EventLog entries are your evidence? + // A: + // R: + const std::vector ev = EventLog::instance().events(); + ASSERT_EQ(ev.size(), 2u); + EXPECT_EQ(ev[0], "fast"); + EXPECT_EQ(ev[1], "slow"); +} + +// ============================================================================ +// Scenario 4: A Timer That Re-Arms Itself From Its Own Handler (Hard) +// ============================================================================ + +TEST_F(TimerTest, RecurringTimerReArmsFromWithinItsHandler) +{ + boost::asio::io_context ctx; + boost::asio::steady_timer timer(ctx); + + int ticks = 0; + + // C++11 self-referential handler: store the std::function first, then let + // the lambda capture a reference to it so it can re-register itself. (A + // lambda cannot name itself directly until C++23's "deducing this".) + std::function on_tick; + on_tick = [&](const boost::system::error_code& ec) { + if (ec) + { + return; + } + EventLog::instance().record("tick"); + ++ticks; + if (ticks < 3) + { + timer.expires_after(std::chrono::milliseconds(1)); + timer.async_wait(on_tick); + } + }; + + timer.expires_after(std::chrono::milliseconds(1)); + timer.async_wait(on_tick); + + // Q: A single io_context with a single timer produces a *repeating* effect. + // There is no loop in this test driving it. What keeps run() from + // returning after the first tick, and what makes it finally return? + // A: + // R: + ctx.run(); + + EXPECT_EQ(ticks, 3); + EXPECT_EQ(EventLog::instance().count_events("tick"), 3u); + + // Q: The handler re-arms the timer only while ticks < 3. If that guard were + // removed, what would happen to ctx.run(), and what EventLog growth + // pattern would you observe before you killed the process? + // A: + // R: + + // TODO (learner): Change the re-arm guard to ticks < 5 and predict the new + // count BEFORE rebuilding. Record your prediction in // A: above, then + // verify it. Then explain why this "re-arm from the handler" shape is the + // standard way to build a periodic timer in callback-style Asio. +} From 27de6b6bb16bb5838f77e095e60264ce84ccaf03 Mon Sep 17 00:00:00 2001 From: TexLeeV Date: Fri, 3 Jul 2026 11:14:59 -0500 Subject: [PATCH 2/3] removed old test file --- .../tests/test_scope_lifetime_patterns.cpp | 643 ------------------ 1 file changed, 643 deletions(-) delete mode 100644 learning_shared_ptr/tests/test_scope_lifetime_patterns.cpp diff --git a/learning_shared_ptr/tests/test_scope_lifetime_patterns.cpp b/learning_shared_ptr/tests/test_scope_lifetime_patterns.cpp deleted file mode 100644 index 498893c..0000000 --- a/learning_shared_ptr/tests/test_scope_lifetime_patterns.cpp +++ /dev/null @@ -1,643 +0,0 @@ -#include "instrumentation.h" - -#include -#include -#include -#include - -class ScopeLifetimePatternsTest : public ::testing::Test -{ -protected: - void SetUp() override - { - EventLog::instance().clear(); - } -}; - -// TEST_F(ScopeLifetimePatternsTest, WeakPtrLockBasic) -// { -// // TODO: Create weak_ptr -// // YOUR CODE HERE - -// bool expired_before = false; -// // TODO: Check if expired -// // expired_before = ??? - -// { -// // TODO: Create shared_ptr and assign to weak -// // YOUR CODE HERE - -// bool expired_during = false; -// // TODO: Check if expired during -// // expired_during = ??? - -// // TODO: Lock weak_ptr -// // YOUR CODE HERE - -// long use_count = 0; -// // TODO: Get use_count of locked ptr -// // use_count = ??? - -// EXPECT_FALSE(expired_before); -// EXPECT_FALSE(expired_during); -// EXPECT_EQ(use_count, 2); -// } - -// bool expired_after = false; -// // TODO: Check if expired after -// // expired_after = ??? - -// // TODO: Try to lock after shared is destroyed -// // YOUR CODE HERE - -// bool locked_after_null = false; -// // TODO: Check if locked_after is nullptr -// // locked_after_null = ??? - -// EXPECT_TRUE(expired_after); -// EXPECT_TRUE(locked_after_null); -// } - -// TEST_F(ScopeLifetimePatternsTest, WeakPtrLockMultipleTimes) -// { -// // TODO: Create shared_ptr and weak_ptr -// // YOUR CODE HERE - -// // TODO: Lock weak_ptr three times -// // YOUR CODE HERE - -// long use_count = 0; -// // TODO: Get use_count of shared -// // use_count = ??? - -// EXPECT_EQ(use_count, 4); -// } - -// TEST_F(ScopeLifetimePatternsTest, LambdaCaptureSharedPtr) -// { -// long captured_use_count = 0; - -// // TODO: Declare callback function -// // YOUR CODE HERE - -// { -// // TODO: Create shared_ptr -// // YOUR CODE HERE - -// // TODO: Create lambda that captures shared by value -// // YOUR CODE HERE - -// long use_count_before_call = 0; -// // TODO: Get use_count before calling callback -// // use_count_before_call = ??? - -// EXPECT_EQ(use_count_before_call, 2); -// } - -// // TODO: Call callback after shared goes out of scope -// // YOUR CODE HERE - -// EXPECT_EQ(captured_use_count, 1); -// } - -// TEST_F(ScopeLifetimePatternsTest, LambdaCaptureWeakPtr) -// { -// bool was_valid = false; - -// // TODO: Declare callback function -// // YOUR CODE HERE - -// { -// // TODO: Create shared_ptr and weak_ptr -// // YOUR CODE HERE - -// // TODO: Create lambda that captures weak by value -// // YOUR CODE HERE - -// long use_count = 0; -// // TODO: Get use_count of shared -// // use_count = ??? - -// EXPECT_EQ(use_count, 1); -// } - -// // TODO: Call callback after shared is destroyed -// // YOUR CODE HERE - -// EXPECT_FALSE(was_valid); -// } - -// TEST_F(ScopeLifetimePatternsTest, LambdaCaptureWeakPtrStillAlive) -// { -// bool was_valid = false; -// long captured_use_count = 0; - -// // TODO: Create shared_ptr and weak_ptr -// // YOUR CODE HERE - -// // TODO: Create lambda that captures weak -// // YOUR CODE HERE - -// // TODO: Call callback while shared is still alive -// // YOUR CODE HERE - -// EXPECT_TRUE(was_valid); -// EXPECT_EQ(captured_use_count, 2); -// } - -// class CallbackManager -// { -// public: -// void register_callback(std::function callback) -// { -// callbacks_.push_back(callback); -// } - -// void invoke_all() -// { -// for (auto& callback : callbacks_) -// { -// callback(); -// } -// } - -// private: -// std::vector> callbacks_; -// }; - -// TEST_F(ScopeLifetimePatternsTest, CallbackWithSharedPtrLifetimeExtension) -// { -// CallbackManager manager; - -// { -// // TODO: Create shared_ptr -// // YOUR CODE HERE - -// // TODO: Register callback that captures shared -// // YOUR CODE HERE -// } - -// auto events = EventLog::instance().events(); -// size_t dtor_count = 0; - -// for (const auto& event : events) -// { -// if (event.find("::dtor") != std::string::npos) -// { -// ++dtor_count; -// } -// } - -// EXPECT_EQ(dtor_count, 0); -// } - -// TEST_F(ScopeLifetimePatternsTest, CallbackWithWeakPtrNoLifetimeExtension) -// { -// CallbackManager manager; - -// { -// // TODO: Create shared_ptr and weak_ptr -// // YOUR CODE HERE - -// // TODO: Register callback that captures weak -// // YOUR CODE HERE -// } - -// auto events = EventLog::instance().events(); -// size_t dtor_count = 0; - -// for (const auto& event : events) -// { -// if (event.find("::dtor") != std::string::npos) -// { -// ++dtor_count; -// } -// } - -// EXPECT_EQ(dtor_count, 1); -// } - -// TEST_F(ScopeLifetimePatternsTest, NestedLambdaCapture) -// { -// long outer_use_count = 0; -// long inner_use_count = 0; - -// // TODO: Create shared_ptr -// // YOUR CODE HERE - -// // TODO: Create outer lambda that captures shared -// // TODO: Inside outer lambda, create inner lambda that also captures shared -// // YOUR CODE HERE - -// // TODO: Call outer lambda -// // YOUR CODE HERE - -// EXPECT_EQ(outer_use_count, 2); -// EXPECT_EQ(inner_use_count, 3); -// } - -// TEST_F(ScopeLifetimePatternsTest, WeakPtrExpiredCheck) -// { -// // TODO: Create weak_ptr -// // YOUR CODE HERE - -// bool expired_initial = false; -// // TODO: Check if expired initially -// // expired_initial = ??? - -// { -// // TODO: Create shared_ptr and assign to weak -// // YOUR CODE HERE - -// bool expired_alive = false; -// // TODO: Check if expired while alive -// // expired_alive = ??? - -// EXPECT_TRUE(expired_initial); -// EXPECT_FALSE(expired_alive); -// } - -// bool expired_final = false; -// // TODO: Check if expired after shared is destroyed -// // expired_final = ??? - -// EXPECT_TRUE(expired_final); -// } - -// TEST_F(ScopeLifetimePatternsTest, ConditionalLockInCallback) -// { -// int invocation_count = 0; - -// // TODO: Declare callback function -// // YOUR CODE HERE - -// { -// // TODO: Create shared_ptr and weak_ptr -// // YOUR CODE HERE - -// // TODO: Create callback with conditional lock -// // YOUR CODE HERE - -// // TODO: Call callback while shared is alive -// // YOUR CODE HERE -// } - -// // TODO: Call callback after shared is destroyed -// // YOUR CODE HERE - -// EXPECT_EQ(invocation_count, 1); -// } - -// TEST_F(ScopeLifetimePatternsTest, MutableLambdaWithSharedPtr) -// { -// // TODO: Create shared_ptr -// // YOUR CODE HERE - -// long initial_count = 0; -// // TODO: Get initial use_count -// // initial_count = ??? - -// // TODO: Create mutable lambda that captures shared and resets it -// // YOUR CODE HERE - -// long before_call = 0; -// // TODO: Get use_count before calling callback -// // before_call = ??? - -// // TODO: Call callback -// // YOUR CODE HERE - -// long after_call = 0; -// // TODO: Get use_count after calling callback -// // after_call = ??? - -// EXPECT_EQ(initial_count, 1); -// EXPECT_EQ(before_call, 2); -// EXPECT_EQ(after_call, 1); -// } - -// ============================================================================ -// CONTROL BLOCK CORRUPTION SCENARIOS -// ============================================================================ -// -// These tests demonstrate how control blocks can become corrupted through -// incorrect usage patterns. Understanding these failure modes is critical -// for debugging production issues. -// -// ============================================================================ - -TEST_F(ScopeLifetimePatternsTest, ControlBlockCorruption_DoubleControlBlock) -{ - // DANGER: Creating two shared_ptrs from the same raw pointer creates - // two separate control blocks, both thinking they own the object. - // When either reaches use_count=0, it deletes the object, leaving the - // other with a dangling pointer and corrupted control block state. - - // Tracked* raw = new Tracked("Dangerous"); - - // Q: What happens when you create two shared_ptrs from the same raw pointer? - // A: - // R: - - // std::shared_ptr p1(raw); // Creates control block #1 - - // Q: At this point, what is p1.use_count()? - // A: - // R: - - // DANGER: This creates a SECOND control block for the same object - // std::shared_ptr p2(raw); // Creates control block #2 - - // Q: What is p1.use_count() now? What about p2.use_count()? - // A: - // R: - - // Q: When p1 goes out of scope, what happens to the Tracked object? - // A: - // R: - - // Q: When p2 tries to access the object after p1 destroyed it, what happens? - // A: - // R: - - // Q: What is the correct way to create p2 from p1? - // A: - // R: - - // Q: At line 381, both p1 and p2 exist and point to the same object. Each has its own control block. When you call - // p1.use_count(), what memory does it need to access? - // A: - // R: - - // Q: Given that p2's control block also points to the same Tracked object, and both control blocks might be trying - // to manage the same memory, what kind of race condition or memory corruption could occur? - // A: - // R: - - // Q: Is this test actually safe to run, or does it invoke undefined behavior before it even reaches the assertions? - // A: - // R: - - // FOLLOW-UP: Validating the hang - // Q: What information would you get from running the test under a debugger (gdb) and interrupting it during the - // hang? What specific commands would show you the call stack and what each thread is doing? - // A: - // A: - // A: - // A: - // R: - - // Q: What would AddressSanitizer (ASan) report if you compiled and ran this test with -fsanitize=address? Would it - // catch the double control block issue before the hang occurs? - // A: - // R: - - // Q: If you added print statements (or EventLog records) immediately before and after line 350 - // (std::shared_ptr p2(raw)), and before line 393 (p1.use_count()), which print statements would you expect - // to see before the hang? - // A: - // R: - - // Q: What would happen if you ran this test with Valgrind? What specific errors would it report about the memory - // operations? - // A: - // R: - - // Q: Could you isolate the problem by creating a minimal test that just does: Tracked* raw = new Tracked("X"); - // shared_ptr p1(raw); shared_ptr p2(raw); long c = p1.use_count(); without any test framework - // overhead? - // A: - // R: - - // long p1_count = p1.use_count(); // this line of code appears to hang. Why? - // long p2_count = p2.use_count(); - - // EXPECT_EQ(p1_count, 1); // Each control block thinks it's the only owner - // EXPECT_EQ(p2_count, 1); // This is the bug—should be 2 if sharing -} - -TEST_F(ScopeLifetimePatternsTest, ControlBlockCorruption_StackObject) -{ - // DANGER: Creating a shared_ptr to a stack object means the control block - // will try to delete the object when use_count reaches 0, but stack objects - // are automatically destroyed when they go out of scope. This causes: - // 1. Double destruction (stack + shared_ptr deleter) - // 2. Deleting non-heap memory (undefined behavior) - - // Q: What happens if you create a shared_ptr to a stack object? - // A: - // R: - - // Q: When does the stack object get destroyed? - // A: - // R: - - // Q: When does the shared_ptr try to destroy the object? - // A: - // R: - - // Q: What is the result of trying to delete a stack object? - // A: - // R: - - // DANGER: This is undefined behavior - // Tracked stack_obj("StackObject"); - // std::shared_ptr sp(&stack_obj); // WRONG: Will try to delete stack memory - - // When p goes out of scope, it will call delete on stack memory → crash - // When stack_obj goes out of scope, destructor runs again → double destruction - - // CORRECT: Only create shared_ptr from heap-allocated objects - std::shared_ptr p = std::make_shared("HeapObject"); - - EXPECT_EQ(p.use_count(), 1); -} - -TEST_F(ScopeLifetimePatternsTest, ControlBlockCorruption_SharedFromThisBeforeShared) -{ - // DANGER: Calling shared_from_this() before the object is owned by a - // shared_ptr results in: - // 1. Bad_weak_ptr exception (C++17+) - // 2. Undefined behavior (C++11/14) - // 3. Corrupted control block state - - class BadWidget : public std::enable_shared_from_this - { - public: - std::shared_ptr get_self() - { - // Q: What happens if you call shared_from_this() before the object is owned by a shared_ptr? - // A: - // A: - // R: - - // Q: What is stored in the internal weak_ptr before a shared_ptr owns the object? - // A: - // R: - - return shared_from_this(); // DANGER if called before shared_ptr owns this - } - }; - - // DANGER: Creating object on stack or with new, then calling shared_from_this() - // BadWidget* raw = new BadWidget(); - // auto self = raw->get_self(); // THROWS bad_weak_ptr or undefined behavior - - // CORRECT: Create with shared_ptr first, then call shared_from_this() - auto widget = std::make_shared(); - auto self = widget->get_self(); // Safe: widget already owns the object - - EXPECT_EQ(widget.use_count(), 2); // widget + self -} - -TEST_F(ScopeLifetimePatternsTest, ControlBlockCorruption_DeletedObjectAccess) -{ - // DANGER: Holding a raw pointer after the shared_ptr is destroyed - // leaves you with a dangling pointer. Accessing it corrupts the control - // block's view of reality (it thinks object is deleted, but you're using it). - - Tracked* raw_ptr = nullptr; - - { - std::shared_ptr p = std::make_shared("Temporary"); - raw_ptr = p.get(); // Get raw pointer - - // Q: What is the lifetime of the pointer returned by get()? - // A: - // R: - - // Q: What happens to raw_ptr when p goes out of scope? - // A: - // R: - - // p goes out of scope here, object is deleted - } - - // Q: What happens if you dereference raw_ptr now? - // A: - // R: - - // Q: What happens if you try to create a new shared_ptr from raw_ptr? - // A: - // R: - - // DANGER: raw_ptr is now dangling - // Accessing it is undefined behavior: - // - Might crash - // - Might return garbage - // - Might appear to work (most dangerous—silent corruption) - - // std::string name = raw_ptr->name(); // UNDEFINED BEHAVIOR - - // Creating new shared_ptr from dangling pointer: - // std::shared_ptr p2(raw_ptr); // DOUBLE CONTROL BLOCK + dangling pointer - - EXPECT_TRUE(raw_ptr != nullptr); // Pointer value unchanged - // But the object it points to is DELETED -} - -TEST_F(ScopeLifetimePatternsTest, ControlBlockCorruption_WeakPtrUseAfterControlBlockDeleted) -{ - // DANGER: The control block is deleted when both use_count and weak_count - // reach 0. If you somehow access a weak_ptr after its control block is - // deleted (e.g., through memory corruption or use-after-free), you get - // undefined behavior. - - std::weak_ptr* weak_ptr_ptr = nullptr; - - { - auto p = std::make_shared("Temp"); - std::weak_ptr weak = p; - - // Q: When is the control block deleted? - // A: - // R: - - // Q: What keeps the control block alive after p is destroyed? - // A: - // R: - - // Store pointer to weak_ptr (DANGER: for demonstration only) - weak_ptr_ptr = &weak; - - // weak goes out of scope here - // Control block's weak_count drops to 0 - // Control block is DELETED - } - - // Q: What happens if you try to access weak_ptr_ptr now? - // A: - // R: - - // DANGER: weak_ptr_ptr points to deleted stack memory - // AND the control block it referenced is also deleted - - // Accessing it is undefined behavior: - // bool expired = weak_ptr_ptr->expired(); // CRASH or garbage - - // This scenario is rare but can happen with: - // - Memory corruption - // - Use-after-free bugs - // - Incorrect lifetime management in complex systems - - EXPECT_TRUE(weak_ptr_ptr != nullptr); // Pointer unchanged, but object deleted -} - -// ============================================================================ -// SUMMARY: How Control Blocks Get Corrupted -// ============================================================================ -// -// 1. **Double Control Block**: Two shared_ptrs from same raw pointer -// - Each thinks it's the sole owner -// - First to reach use_count=0 deletes object -// - Second has dangling pointer and corrupted state -// - Fix: Always copy shared_ptr, never create from raw pointer twice -// -// 2. **Stack Object**: shared_ptr to stack-allocated object -// - Control block tries to delete non-heap memory -// - Stack destructor also runs -// - Double destruction + invalid delete -// - Fix: Only create shared_ptr from heap objects -// -// 3. **shared_from_this() Too Early**: Before shared_ptr owns object -// - Internal weak_ptr is uninitialized -// - Throws bad_weak_ptr or undefined behavior -// - Fix: Ensure object is owned by shared_ptr before calling -// -// 4. **Dangling Raw Pointer**: Using get() after shared_ptr destroyed -// - Raw pointer outlives the object -// - Accessing deleted memory -// - Creating new shared_ptr from it makes double control block -// - Fix: Never store raw pointers beyond shared_ptr lifetime -// -// 5. **Control Block Use-After-Free**: Accessing weak_ptr after control block deleted -// - Control block deleted when use_count=0 AND weak_count=0 -// - Accessing weak_ptr after this is undefined behavior -// - Fix: Ensure weak_ptr lifetime doesn't exceed control block -// -// ============================================================================ -// DEBUGGING CORRUPTED CONTROL BLOCKS -// ============================================================================ -// -// Symptoms: -// - Crashes in shared_ptr destructor -// - Double-free detected by allocator -// - Heap corruption errors -// - use_count() returns unexpected values -// - Segfaults when accessing object through shared_ptr -// -// Tools: -// - AddressSanitizer (detects use-after-free, double-free) -// - Valgrind (detects memory errors) -// - GDB: `p *ptr._M_refcount._M_pi` (inspect control block) -// - Enable debug allocator (detects heap corruption) -// -// Prevention: -// - Never create shared_ptr from raw pointer twice -// - Never create shared_ptr to stack objects -// - Always use make_shared when possible -// - Use enable_shared_from_this for self-references -// - Never store raw pointers from get() long-term -// - Use weak_ptr for non-owning references -// -// ============================================================================ From e13e8f7c26511f0418bb3b1a6924d54080c8adbb Mon Sep 17 00:00:00 2001 From: TexLeeV Date: Fri, 3 Jul 2026 11:19:31 -0500 Subject: [PATCH 3/3] removing old code file --- .../tests/test_exercises_fill_in.cpp | 457 ------------------ 1 file changed, 457 deletions(-) delete mode 100644 learning_shared_ptr/tests/test_exercises_fill_in.cpp diff --git a/learning_shared_ptr/tests/test_exercises_fill_in.cpp b/learning_shared_ptr/tests/test_exercises_fill_in.cpp deleted file mode 100644 index f54d90c..0000000 --- a/learning_shared_ptr/tests/test_exercises_fill_in.cpp +++ /dev/null @@ -1,457 +0,0 @@ -#include "instrumentation.h" - -#include -#include -#include -#include -#include -#include - -class FillInExercisesTest : public ::testing::Test -{ -protected: - void SetUp() override - { - EventLog::instance().clear(); - } -}; - -// Demonstration: What does the instrumentation actually log? -TEST_F(FillInExercisesTest, Demo_InstrumentationOutput) -{ - std::cout << "\n=== Instrumentation Demo ===\n"; - - { - std::cout << "Creating shared_ptr...\n"; - std::shared_ptr ptr = std::make_shared("Demo"); - std::cout << "use_count: " << ptr.use_count() << "\n"; - - std::cout << "Copying shared_ptr...\n"; - std::shared_ptr ptr2 = ptr; - std::cout << "use_count: " << ptr.use_count() << "\n"; - - std::cout << "Resetting ptr2...\n"; - ptr2.reset(); - std::cout << "use_count: " << ptr.use_count() << "\n"; - - std::cout << "About to exit scope (ptr will be destroyed)...\n"; - } - - std::cout << "\n=== EventLog Contents ===\n"; - std::cout << EventLog::instance().dump(); - std::cout << "=========================\n\n"; -} - -// Exercise 1: Implement a class that uses enable_shared_from_this -// TODO: Complete the Widget class so it can safely return a shared_ptr to itself -class Widget : public std::enable_shared_from_this -{ -public: - explicit Widget(const std::string& name) : tracked_(name) - { - } - - // TODO: Implement this method to return a shared_ptr to this object - std::shared_ptr get_self() - { - // YOUR CODE HERE - return shared_from_this(); - } - -private: - Tracked tracked_; -}; - -TEST_F(FillInExercisesTest, Exercise1_EnableSharedFromThis) -{ - std::shared_ptr w1 = std::make_shared("W1"); - std::shared_ptr w2 = w1; - w1.reset(); - if (w2) - { - std::cout << "is NOT nullptr" << std::endl; - } - else - { - std::cout << "is nullptr" << std::endl; - } - // std::shared_ptr w1 = std::make_shared("W1"); - // std::shared_ptr w2 = w1->get_self(); - - // EXPECT_EQ(w1.use_count(), 2); - // EXPECT_EQ(w2.use_count(), 2); - // EXPECT_EQ(w1.get(), w2.get()); -} - -// Exercise 2: Implement a weak_ptr cache -// TODO: Complete the Cache class to store weak_ptr and auto-cleanup expired entries -class Cache -{ -public: - std::shared_ptr get(const std::string& key) - { - // TODO: Implement cache logic: - // 1. Check if key exists in cache - auto it = cache_.find(key); - - // 2. If exists, try to lock the weak_ptr - if (it != cache_.end()) - { - std::shared_ptr locked = it->second.lock(); - // 3. If lock succeeds, return the shared_ptr - if (locked) - { - return locked; - } - } - - // 4. If lock fails or key doesn't exist, create new resource - std::shared_ptr new_resource = std::make_shared(key); - - // 5. Store weak_ptr in cache and return shared_ptr - cache_[key] = new_resource; - return new_resource; - } - - void cleanup() - { - // TODO: Remove all expired weak_ptr entries from cache - for (auto it = cache_.begin(); it != cache_.end();) - { - if (it->second.expired()) - { - it = cache_.erase(it); - } - else - { - ++it; - } - } - } - - size_t size() const - { - // YOUR CODE HERE - return cache_.size(); - } - -private: - // TODO: Add appropriate data structure to store weak_ptr - std::map> cache_; -}; - -TEST_F(FillInExercisesTest, Exercise2_WeakPtrCache) -{ - Cache cache; - - // Q: How many shared_ptr instances exist after this call? Where are they? - // A: - // R: - std::shared_ptr r1 = cache.get("key1"); - EXPECT_EQ(cache.size(), 1); - // Q: If the cache stored shared_ptr instead of weak_ptr, what would use_count() be here? - // A: - // R: - EXPECT_EQ(r1.use_count(), 1); - - // Q: What does weak_ptr::lock() return? What happens to the reference count when assigned to r2? - // A: - // R: - std::shared_ptr r2 = cache.get("key1"); - EXPECT_EQ(cache.size(), 1); - // Q: Walk through the cache lookup—what operation caused the count to increment from 1 to 2? - // A: - // R: - EXPECT_EQ(r1.use_count(), 2); - EXPECT_EQ(r1.get(), r2.get()); - - // Q: After both reset() calls, what is the reference count? What happens to the object? - // A: - // R: - r1.reset(); - r2.reset(); - - // Q: When cleanup() calls expired() on the weak_ptr, what will it return and why? - // A: - // R: - // Q: What observable event in the instrumentation log confirms the object was destroyed? - // A: - // R: - // R: - // R: - // R: - cache.cleanup(); - EXPECT_EQ(cache.size(), 0); -} - -// Exercise 3: Implement Observer pattern with automatic cleanup -// TODO: Complete the Subject class to manage observers using weak_ptr -class Observer -{ -public: - explicit Observer(const std::string& name) : tracked_(name) - { - } - - void notify() - { - EventLog::instance().record("Observer notified"); - } - -private: - Tracked tracked_; -}; - -class Subject -{ -public: - void attach(std::shared_ptr observer) - { - // TODO: Store observer as weak_ptr - observers_.push_back(observer); - } - - void notify_all() - { - // TODO: Iterate through observers - // 1. Try to lock each weak_ptr - // 2. If lock succeeds, call notify() - // 3. If lock fails, remove from list - for (auto it = observers_.begin(); it != observers_.end();) - { - std::shared_ptr locked = it->lock(); - if (locked) - { - locked->notify(); - ++it; - } - else - { - it = observers_.erase(it); - } - } - } - - size_t observer_count() const - { - // YOUR CODE HERE - return observers_.size(); - } - -private: - // TODO: Add data structure to store weak_ptr - std::vector> observers_; -}; - -TEST_F(FillInExercisesTest, Exercise3_ObserverPattern) -{ - Subject subject; - - { - std::shared_ptr obs1 = std::make_shared("Obs1"); - std::shared_ptr obs2 = std::make_shared("Obs2"); - - subject.attach(obs1); - subject.attach(obs2); - - EXPECT_EQ(subject.observer_count(), 2); - - EventLog::instance().clear(); - subject.notify_all(); - - auto events = EventLog::instance().events(); - size_t notify_count = 0; - for (const auto& event : events) - { - if (event.find("Observer notified") != std::string::npos) - { - ++notify_count; - } - } - EXPECT_EQ(notify_count, 2); - } - - subject.notify_all(); - EXPECT_EQ(subject.observer_count(), 0); -} - -// Exercise 4: Implement custom deleter for C resource -// TODO: Create a custom deleter that logs when it's called -TEST_F(FillInExercisesTest, Exercise4_CustomDeleter) -{ - { - FILE* file = std::tmpfile(); - - // TODO: Create a shared_ptr with a custom deleter that: - // 1. Logs "Custom deleter called" to EventLog - // 2. Closes the file with fclose() - // YOUR CODE HERE - create shared_ptr with custom deleter - std::shared_ptr file_ptr(file, [](FILE* f) { - EventLog::instance().record("Custom deleter called"); - if (f) - { - fclose(f); - } - }); - - EXPECT_NE(file_ptr.get(), nullptr); - } - - auto events = EventLog::instance().events(); - bool deleter_called = false; - for (const auto& event : events) - { - if (event.find("Custom deleter called") != std::string::npos) - { - deleter_called = true; - } - } - EXPECT_TRUE(deleter_called); -} - -// Exercise 5: Implement aliasing constructor usage -// TODO: Use aliasing constructor to create shared_ptr to member -TEST_F(FillInExercisesTest, Exercise5_AliasingConstructor) -{ - struct Container - { - Tracked member; - explicit Container(const std::string& name) : member(name) - { - } - }; - - std::shared_ptr owner = std::make_shared("Owner"); - - // TODO: Create a shared_ptr that: - // 1. Points to owner->member - // 2. Shares ownership with owner (keeps Container alive) - // 3. Use the aliasing constructor - // YOUR CODE HERE - std::shared_ptr alias(owner, &owner->member); - - EXPECT_EQ(owner.use_count(), 2); - EXPECT_EQ(alias.use_count(), 2); - - owner.reset(); - - EXPECT_EQ(alias.use_count(), 1); - EXPECT_NE(alias.get(), nullptr); -} - -// Exercise 6: Implement copy-on-write string -// TODO: Complete the CowString class -class CowString -{ -public: - explicit CowString(const std::string& str) : data_(std::make_shared(str)) - { - // TODO: Initialize data_ with a shared_ptr to Tracked - // YOUR CODE HERE - } - - std::string get() const - { - // TODO: Return the string from data_ - // YOUR CODE HERE - return data_->name(); - } - - void set(const std::string& str) - { - // TODO: Implement copy-on-write: - // 1. Check if use_count() > 1 - // 2. If yes, create new Tracked with str - // 3. If no, can modify in place (but for simplicity, always create new) - // YOUR CODE HERE - if (data_.use_count() > 1) - { - data_ = std::make_shared(str); - } - else - { - data_ = std::make_shared(str); - } - } - - long use_count() const - { - // YOUR CODE HERE - return data_.use_count(); - } - -private: - // TODO: Add shared_ptr member - std::shared_ptr data_; -}; - -TEST_F(FillInExercisesTest, Exercise6_CopyOnWrite) -{ - CowString s1("Original"); - EXPECT_EQ(s1.use_count(), 1); - - CowString s2 = s1; - EXPECT_EQ(s1.use_count(), 2); - EXPECT_EQ(s2.use_count(), 2); - - s2.set("Modified"); - EXPECT_EQ(s1.use_count(), 1); - EXPECT_EQ(s2.use_count(), 1); - EXPECT_EQ(s1.get(), "Original"); - EXPECT_EQ(s2.get(), "Modified"); -} - -// Exercise 7: Break a circular reference -// TODO: Fix the Node class to avoid memory leak -class Node -{ -public: - explicit Node(const std::string& name) : tracked_(name) - { - } - - void set_next(std::shared_ptr next) - { - // TODO: Store next in a way that doesn't create circular reference - // Hint: Should this be shared_ptr or weak_ptr? - // YOUR CODE HERE - next_ = next; - } - - std::shared_ptr next() const - { - // TODO: Return the next node (may need to lock if using weak_ptr) - // YOUR CODE HERE - return next_.lock(); - } - -private: - Tracked tracked_; - // TODO: Add member to store next node - std::weak_ptr next_; -}; - -TEST_F(FillInExercisesTest, Exercise7_BreakCircularReference) -{ - { - std::shared_ptr node1 = std::make_shared("Node1"); - std::shared_ptr node2 = std::make_shared("Node2"); - - node1->set_next(node2); - node2->set_next(node1); - - EXPECT_EQ(node1.use_count(), 1); - EXPECT_EQ(node2.use_count(), 1); - } - - auto events = EventLog::instance().events(); - size_t dtor_count = 0; - for (const auto& event : events) - { - if (event.find("::dtor") != std::string::npos) - { - ++dtor_count; - } - } - EXPECT_EQ(dtor_count, 2); -}