Skip to content

[LOGS] Fix Logger::EmitLogRecord unsafe static_cast to Recordable - #4624

Merged
marcalff merged 9 commits into
open-telemetry:mainfrom
om7057:fix/logger-emit-unsafe-cast
Sep 24, 2026
Merged

marcalff merged 9 commits into
open-telemetry:mainfrom
om7057:fix/logger-emit-unsafe-cast

Conversation

@om7057

@om7057 om7057 commented Sep 21, 2026

Copy link
Copy Markdown
Contributor

Fixes #4537

Changes

While a Logger is disabled, CreateLogRecord() returned a kNoopLogger-created NoopLogRecord, which is not the SDK's internal Recordable. 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. via LoggerProvider::UpdateLoggerConfigurator()) let that NoopLogRecord reach EmitLogRecord()'s unconditional static_cast<Recordable *>, and then MultiLogRecordProcessor::OnEmit()'s own static_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 the Bazel nortti CI job builds and tests the whole tree (including sdk/logs) with -fno-rtti, where a dynamic_cast on a polymorphic type does not compile.

Per the design discussion on #4537, CreateLogRecord() (both the no-argument overload and the ABI v2 context_or_span overload) now returns an empty MultiRecordable while the logger is disabled, instead of a NoopLogRecord. This needs no detection at emit time: an empty MultiRecordable is a real Recordable, so both static_casts above are always safe, and its Set* calls and ReleaseRecordable() 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 both CreateLogRecord() overloads while the logger is disabled, enables the logger, and emits both, asserting no crash and that nothing reaches the processor. Ran the full sdk/logs and api/logs test suites under both ABI v1 and ABI v2 with no regressions.

  • CHANGELOG.md updated for non-trivial changes
  • Unit tests have been added
  • Changes in public API reviewed

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.
@om7057
om7057 requested a review from a team as a code owner September 21, 2026 18:22
@codecov

codecov Bot commented Sep 21, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 86.53%. Comparing base (ad74ba2) to head (5e36b31).

Additional details and impacted files

Impacted file tree graph

@@            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     
Files with missing lines Coverage Δ
sdk/src/logs/logger.cc 95.10% <100.00%> (+0.99%) ⬆️

... and 1 file with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@lalitb

lalitb commented Sep 22, 2026

Copy link
Copy Markdown
Member

Thanks for working on this. I agree with the empty MultiRecordable approach discussed in #4537. Let's rework this along those lines, with a regression test for enabling the logger between record creation and emission.

@linux-foundation-easycla

linux-foundation-easycla Bot commented Sep 22, 2026

Copy link
Copy Markdown

CLA Signed
The committers listed above are authorized under a signed CLA.

@om7057

om7057 commented Sep 22, 2026

Copy link
Copy Markdown
Contributor Author

Reworked to the empty MultiRecordable approach from #4537: Logger::CreateLogRecord() (both overloads) now returns a bare MultiRecordable() with no wrapped recordables while the logger is disabled, instead of kNoopLogger.CreateLogRecord(). This needs no detection at emit time, since Set* and ReleaseRecordable() on an empty MultiRecordable already loop over zero children, so MultiLogRecordProcessor::OnEmit()'s static_cast is always safe regardless of whether the logger got enabled in between.

Reverted the LogRecord::IsRecordable() virtual and the EmitLogRecord() guard entirely, so there's no API/ABI change now.

Replaced the old test with EmitLogRecordSafeWhenEnabledBetweenCreateAndEmit, which creates a record while disabled, enables the logger via UpdateLoggerConfigurator(), then emits it, and asserts no crash and no data reaching the processor. Verified locally under both ABIv1 and ABIv2 (10 and 19 tests respectively in logger_sdk_test), plus the full sdk/logs and api/logs suites (78 tests total), all passing.

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.
@om7057
om7057 force-pushed the fix/logger-emit-unsafe-cast branch from 3155622 to ef64107 Compare September 22, 2026 03:34
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 mateenali66 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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>
@om7057

om7057 commented Sep 23, 2026

Copy link
Copy Markdown
Contributor Author

Thanks for verifying the crash reproduces on main. Added v2 coverage in a1bcbf4c: the test now also creates a record through CreateLogRecord(context_or_span) while disabled and emits it after enabling, alongside the existing no-argument case. 19/19 pass under ABIv2, 10/10 under ABIv1 with the v2 branch compiled out.

Added you as co-author on that commit.

@lalitb lalitb left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM, thanks for the update and coverage for both overloads. Could you also refresh the PR description to match the current implementation?

@om7057

om7057 commented Sep 23, 2026

Copy link
Copy Markdown
Contributor Author

PR description updated to describe the current empty-MultiRecordable implementation instead of the original IsRecordable() approach.

@marcalff
marcalff merged commit 9d6f398 into open-telemetry:main Sep 24, 2026
77 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[BUG] Logger::EmitLogRecord() blindly static_casts a caller-supplied LogRecord to Recordable

5 participants