fix(logging): stop a hostile metaclass costing the record - #1516
Merged
Conversation
Contributor
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Contributor
|
Important
This repository does not receive automatic reviews because it has fewer than 10 stars. ⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: ASSERTIVE Plan: Pro Plus Run ID: Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Dependency Review✅ No vulnerabilities or license issues or OpenSSF Scorecard issues found.Snapshot WarningsEnsure that dependencies are being submitted on PR branches and consider enabling retry-on-snapshot-warnings. See the documentation for more information and troubleshooting advice. Scanned FilesNone |
#1515 closed #1525's three residual holes and is on main. One path in the same function is still reachable, and it is the one its docstring asserts is safe: "The type name is a plain attribute lookup and is always safe." It is not. `__name__` on a class is looked up on its *metaclass*, so a metaclass defining `__name__` as a raising property defeats the `except` branch. That second raise happens outside any guard, so it propagates past the `_JSON_UNSERIALIZABLE_RECORD` tier and out of `_format_json` entirely, and `Handler.handleError` drops the record. The constant-record tier does not catch it. Measured on 5473bcc: 2 of 3 records reach the sink. `object.__getattribute__(type(exc), "__name__")` does not fix this -- it still routes through the metaclass descriptor: type(exc).__name__ -> RAISES object.__getattribute__(type(exc),"__name__") -> RAISES type.__dict__["__name__"].__get__(type(exc)) -> OK Binding the descriptor from `type.__dict__` bypasses an override and returns the ordinary name for ordinary classes, with a constant as the final floor. The docstring is corrected to state the guarantee the code provides. This is the same failure shape #1525 was filed about, one level down: the recovery step for a failure is itself able to fail. Verification on this head: 36 passed in tests/unit/test_logging_config_crlf.py (33 pre-existing, unchanged). Reverting only logging_config.py: 2 failed, 34 passed. The third new test, test_describe_exception_is_unchanged_for_ordinary_exceptions, passes on 5473bcc by design -- it guards the normal path against regression rather than pinning the fix, so it is excluded from the non-vacuity claim. ruff clean; mypy reports the same 17 pre-existing errors on both heads. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Msg6kqkhiuW1ZiDv66sr4N
groupthinking
force-pushed
the
claude/clever-heisenberg-vk1msh
branch
from
August 29, 2026 06:27
b7eb515 to
f35b5c2
Compare
Contributor
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Canonical issue
Closes #1576
Outcome
_describe_exceptioncan still cost the record, on the one branch its own docstring calls safe:__name__on a class is looked up on its metaclass, so a metaclass defining__name__as a raising property defeats theexceptbranch. The second raise is outside any guard, so it propagates past the_JSON_UNSERIALIZABLE_RECORDtier and out of_format_jsonentirely;Handler.handleErrorthen drops the record. The constant-record tier does not catch it.Measured on
5473bcc, healthy → poisoned → healthy through a realStreamHandler: 2 of 3 records reach the sink.This is the shape the previous issue was filed about, one level further down — the recovery step for a failure is itself able to fail.
The obvious fix does not work
CodeRabbit recommended
object.__getattribute__(type(exc), "__name__")for this on an earlier PR in this series. It still routes through the metaclass descriptor:Binding the descriptor from
type.__dict__bypasses an override and returns the ordinary name for ordinary classes (ValueError→'ValueError'), with a constant as the final floor.Scope
logging_config.py— one added tier in_describe_exception(6 lines of behaviour) and its corrected docstring.tests/unit/test_logging_config_crlf.py— 3 tests.5473bcc; the scalar filter, int bound,allow_nan=Falseand constant tier are untouched as merged.record.getMessage()on mismatched %-args raises above thetryand fails the line-oriented path identically. The guarantee is "serialization never loses a record", not "no record is ever lost".Risk
main's — pinned bytest_describe_exception_is_unchanged_for_ordinary_exceptions, and by the pre-existing fallback tests that assert exact strings.git revert. No migration, config, or schema change.Verification
Head
f35b5c2, on5473bcc. Measured, not inferred.tests/unit/test_logging_config_crlf.py: 36 passed. Collection goes 33 → 36, so every pre-existing test, including all of fix(logging): close three ways the JSON fallback still lost the record #1515's, is unchanged and still passes.logging_config.py: 2 failed, 34 passed —test_describe_exception_survives_a_hostile_metaclassandtest_hostile_metaclass_exception_does_not_cost_the_record. The third new test passes on5473bccby design: it guards the normal path against regression rather than pinning the fix, so it is excluded from the non-vacuity claim rather than padding it.ruff checkclean on both changed files.mypyreports 17 errors on5473bccand 17 on this head, the same pre-existing set at lines this PR does not touch._describe_exceptionis module-private with exactly one caller, on the fallback path of_format_json, reached only viaStructuredFormatter.formatwhenjson_outputis set.test_logging_config_crlf.pyis the only test file importing the module.Not verified in this sandbox: the wider suite.
pytest tests/unithits collection errors from third-party deps absent from this container (fastapi,aiohttp,yaml,sqlalchemy,pydantic), unrelated to this change.Production evidence
Not applicable as a preview: backend logging, no
apps/web/**surface, which is what gate 4 ofMERGE_POLICY.mdscopes previews to. The runtime evidence is the reproduction itself, run against the real formatter through a realStreamHandlerin both directions.production_config.py:72defaultsJSON_LOGGINGto"true", so the JSON path is live.Severity: low, and narrower than the previous issue's. Reaching it needs a call site to pass a value whose
__str__raises an exception whose class also carries a hostile metaclass. No current call site passes a non-scalar. This is a correctness gap in a safety net, not an exploitable path — worth closing because the guarantee is stated unconditionally in the code and is not unconditional.A note on the red
PR Governanceattempts in this PR's historyRecorded because it will otherwise look like this PR failed the gate on its merits, and because it is a reusable gotcha for this repo.
pr-governance.ymlreads the PR body from the frozen webhook payload (const pr = context.payload.pull_request), not from a fresh API fetch. Two consequences:editedevent whose run passed.rerun_failed_jobscannot green this gate, because a re-run replays the original payload. I re-ran it once and it re-published a failure over an already-passing check — my error, and the reason there is a red attempt timestamped after a green one.The way to re-evaluate this gate is a new
pull_request_targetevent (edit the body, or push), never a job re-run. Worth consideringpulls.getinstead of the payload so the gate reflects current state.Agent handoff
_describe_exceptioncan still drop the record —type(exc).__name__resolves through the metaclass #1576, open_describe_exceptioncan still drop the record —type(exc).__name__resolves through the metaclass #1576'sProduced by a scheduled, unattended PR-remediation routine. Halts at the human gate; no auto-merge to protected
mainis requested or performed.