[LOGS] Fix Logger::EmitLogRecord unsafe static_cast to Recordable - #4624
Conversation
EmitLogRecord() unconditionally static_cast a caller-supplied LogRecord to the SDK's internal Recordable. static_cast between unrelated polymorphic types performs no runtime check, so a LogRecord that is not actually a Recordable (a bridge, or a caller-supplied MakeRecordable() override, both of which are legitimate given LogRecord/MakeRecordable() are public, overridable API surface) hits undefined behavior the moment the mismatched vtable/layout is used. dynamic_cast is not an option as a guard: this project supports building with RTTI disabled (the "Bazel nortti" CI job builds and tests the whole tree, sdk/logs included, with -fno-rtti), and a dynamic_cast would fail to compile there. Added LogRecord::IsRecordable(), a virtual capability query with a default false implementation, overridden by Recordable to return true. EmitLogRecord() checks it before casting and drops (with a warning) a LogRecord that isn't a Recordable, rather than forwarding it. Added a regression test with a minimal foreign LogRecord implementation that does not derive from Recordable, confirming it is dropped cleanly rather than reaching the processor. Verified under both ABI v1 and ABI v2, plus the rest of the sdk/logs test suite for regressions.
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #4624 +/- ##
==========================================
+ Coverage 86.53% 86.53% +0.01%
==========================================
Files 525 525
Lines 20475 20475
==========================================
+ Hits 17715 17717 +2
+ Misses 2760 2758 -2
🚀 New features to boost your workflow:
|
|
Thanks for working on this. I agree with the empty |
|
Reworked to the empty Reverted the Replaced the old test with cc: @lalitb |
…ble) Per discussion on open-telemetry#4537 and lalitb's review on this PR, rework from the originally proposed virtual LogRecord::IsRecordable() (ABI-changing) to having Logger::CreateLogRecord() return an empty MultiRecordable while the logger is disabled. This is a safe target for both Logger::EmitLogRecord()'s static_cast and MultiLogRecordProcessor::OnEmit()'s static_cast if the logger is enabled between record creation and emission, with no API/ABI change, since an empty MultiRecordable's Set* calls and ReleaseRecordable() simply loop over zero wrapped recordables.
3155622 to
ef64107
Compare
clang-format alignment for the new test's variable declarations, and remove the now-unused nostd/unique_ptr.h include after the previous rework dropped the test that referenced it directly.
mateenali66
left a comment
There was a problem hiding this comment.
Built main with only your test file added and it dies with SIGBUS, exit 138. Same test on your head passes, so it does regress in a plain debug build.
The ABI v2 CreateLogRecord overload takes the same change, but nothing reaches it while disabled, since the test uses the no argument one. Worth a second create under the v2 guard in the same test?
On credit, yes please, and thanks for asking.
mateenali66 confirmed EmitLogRecordSafeWhenEnabledBetweenCreateAndEmit reproduces a SIGBUS on plain main and passes on this PR's head, but only the no-argument CreateLogRecord() overload was exercised. The v2 CreateLogRecord(context_or_span) overload takes the identical fix, so extend the same test to create and emit through it too. Verified locally under both ABIv1 (10/10 pass, v2 branch compiled out) and ABIv2 (19/19 pass). Co-Authored-By: Mateen Anjum <mateenali66@gmail.com>
|
Thanks for verifying the crash reproduces on main. Added v2 coverage in Added you as co-author on that commit. |
lalitb
left a comment
There was a problem hiding this comment.
LGTM, thanks for the update and coverage for both overloads. Could you also refresh the PR description to match the current implementation?
|
PR description updated to describe the current empty-MultiRecordable implementation instead of the original IsRecordable() approach. |
Fixes #4537
Changes
While a
Loggeris disabled,CreateLogRecord()returned akNoopLogger-createdNoopLogRecord, which is not the SDK's internalRecordable.EmitLogRecord()re-checks the enabled state at emit time rather than trusting what it was at creation time, so enabling the logger in between (e.g. viaLoggerProvider::UpdateLoggerConfigurator()) let thatNoopLogRecordreachEmitLogRecord()'s unconditionalstatic_cast<Recordable *>, and thenMultiLogRecordProcessor::OnEmit()'s ownstatic_cast<MultiRecordable *>, both undefined behavior.The issue's suggested fix uses
dynamic_cast, but that is not an option here: this project supports building with RTTI disabled, and theBazel norttiCI job builds and tests the whole tree (includingsdk/logs) with-fno-rtti, where adynamic_caston a polymorphic type does not compile.Per the design discussion on #4537,
CreateLogRecord()(both the no-argument overload and the ABI v2context_or_spanoverload) now returns an emptyMultiRecordablewhile the logger is disabled, instead of aNoopLogRecord. This needs no detection at emit time: an emptyMultiRecordableis a realRecordable, so bothstatic_casts above are always safe, and itsSet*calls andReleaseRecordable()simply loop over zero wrapped recordables, so the record is dropped without ever reaching a real processor. No API or ABI change.Added
LoggerSDK.EmitLogRecordSafeWhenEnabledBetweenCreateAndEmit, which creates a record through bothCreateLogRecord()overloads while the logger is disabled, enables the logger, and emits both, asserting no crash and that nothing reaches the processor. Ran the fullsdk/logsandapi/logstest suites under both ABI v1 and ABI v2 with no regressions.CHANGELOG.mdupdated for non-trivial changes