Summary
#1515 closed #1525's three residual holes and is on main (5473bcc). One path in the same function is still reachable, and it is the one #1515's own docstring asserts is safe:
def _describe_exception(exc: BaseException) -> str:
"""...The type name is a plain attribute lookup and is always safe."""
try:
return f"{type(exc).__name__}: {exc}"
except Exception:
return type(exc).__name__ # <- this can raise too
__name__ on a class is looked up on its metaclass, so a metaclass defining __name__ as a raising property defeats the except branch. The second raise happens outside any guard, so it propagates past the _JSON_UNSERIALIZABLE_RECORD tier and out of _format_json entirely — Handler.handleError then drops the record. The final constant-record tier does not catch it.
This is the same failure shape #1525 was filed about, one level further down: the recovery step for a failure is itself able to fail.
Reproduction
Against main @ 5473bcc:
class HostileMeta(type):
@property
def __name__(cls): raise RuntimeError("__name__ exploded")
class NamelessExc(Exception, metaclass=HostileMeta):
def __str__(self): raise RuntimeError("str exploded")
class RaisesNameless:
def __str__(self): raise NamelessExc()
logger.info("poisoned", extra={"request_id": RaisesNameless()})
_describe_exception -> RAISES RuntimeError: __name__ exploded => record LOST
Healthy → poisoned → healthy through a real StreamHandler: 2 of 3 records reach the sink.
The obvious fix does not work
CodeRabbit recommended object.__getattribute__(type(exc), "__name__") for this on #1494. It still routes through the metaclass descriptor:
type(exc).__name__ -> RAISES RuntimeError
object.__getattribute__(type(exc),"__name__") -> RAISES RuntimeError
type.__dict__["__name__"].__get__(type(exc)) -> OK: 'NamelessExc'
Fetching the descriptor from type.__dict__ and binding it directly bypasses the override, and returns the ordinary name for ordinary classes (ValueError → 'ValueError').
Suggested fix
def _describe_exception(exc: BaseException) -> str:
try:
return f"{type(exc).__name__}: {exc}"
- except Exception: # noqa: BLE001 - the fallback must not need a fallback
- return type(exc).__name__
+ except Exception: # noqa: BLE001 - fall through to a narrower attempt
+ pass
+ try:
+ name: str = type.__dict__["__name__"].__get__(type(exc))
+ return name
+ except Exception: # noqa: BLE001 - the fallback must not need a fallback
+ return "UnrenderableException"
The docstring's "always safe" claim should be corrected at the same time — that inaccuracy is what makes this worth tracking rather than leaving as a curiosity, since it invites the next reader to trust the branch.
Reachability and severity
Severity: low, and lower than #1525's. Reaching it requires a call site to pass a value whose __str__ raises an exception whose class also has a hostile metaclass. No current call site passes a non-scalar at all. This is a correctness gap in a safety net, not an exploitable path — filed because the guarantee is stated unconditionally in the code and is not unconditional.
Acceptance criteria
- A record whose enrichment raises an exception with a hostile metaclass is still emitted, with
level and message authoritative.
- A regression test in the shape of the existing ones in
tests/unit/test_logging_config_crlf.py covers it and fails on 5473bcc.
- Ordinary exceptions are unaffected —
_describe_exception(ValueError("msg")) == "ValueError: msg" still holds, and the existing test_describe_exception_survives_an_exception_that_cannot_be_stringified still passes unchanged.
_describe_exception's docstring states the guarantee the code actually provides.
History
This was found during the #1452 remediation and handed to #1515 as a patch and a failing test before it merged; it was not applied. #1516 carries the fix and is being reduced to exactly this delta against current main.
Summary
#1515 closed #1525's three residual holes and is on
main(5473bcc). One path in the same function is still reachable, and it is the one #1515's own docstring asserts is 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 happens outside any guard, so it propagates past the_JSON_UNSERIALIZABLE_RECORDtier and out of_format_jsonentirely —Handler.handleErrorthen drops the record. The final constant-record tier does not catch it.This is the same failure shape #1525 was filed about, one level further down: the recovery step for a failure is itself able to fail.
Reproduction
Against
main@5473bcc:Healthy → poisoned → healthy through a real
StreamHandler: 2 of 3 records reach the sink.The obvious fix does not work
CodeRabbit recommended
object.__getattribute__(type(exc), "__name__")for this on #1494. It still routes through the metaclass descriptor:Fetching the descriptor from
type.__dict__and binding it directly bypasses the override, and returns the ordinary name for ordinary classes (ValueError→'ValueError').Suggested fix
def _describe_exception(exc: BaseException) -> str: try: return f"{type(exc).__name__}: {exc}" - except Exception: # noqa: BLE001 - the fallback must not need a fallback - return type(exc).__name__ + except Exception: # noqa: BLE001 - fall through to a narrower attempt + pass + try: + name: str = type.__dict__["__name__"].__get__(type(exc)) + return name + except Exception: # noqa: BLE001 - the fallback must not need a fallback + return "UnrenderableException"The docstring's "always safe" claim should be corrected at the same time — that inaccuracy is what makes this worth tracking rather than leaving as a curiosity, since it invites the next reader to trust the branch.
Reachability and severity
Severity: low, and lower than #1525's. Reaching it requires a call site to pass a value whose
__str__raises an exception whose class also has a hostile metaclass. No current call site passes a non-scalar at all. This is a correctness gap in a safety net, not an exploitable path — filed because the guarantee is stated unconditionally in the code and is not unconditional.Acceptance criteria
levelandmessageauthoritative.tests/unit/test_logging_config_crlf.pycovers it and fails on5473bcc._describe_exception(ValueError("msg")) == "ValueError: msg"still holds, and the existingtest_describe_exception_survives_an_exception_that_cannot_be_stringifiedstill passes unchanged._describe_exception's docstring states the guarantee the code actually provides.History
This was found during the #1452 remediation and handed to #1515 as a patch and a failing test before it merged; it was not applied. #1516 carries the fix and is being reduced to exactly this delta against current
main.