Opened 35 hours ago
Last modified 13 hours ago
#36810 assigned Bug
SimpleLazyObject.__repr__ causes infinite recursion when setup function is a bound method
| Reported by: | Sean Reed | Owned by: | Sean Reed |
|---|---|---|---|
| Component: | Core (Other) | Version: | 6.0 |
| Severity: | Release blocker | Keywords: | LazyNonce, csp, SimpleLazyObject |
| Cc: | Sean Reed, Nilesh Pahari, Clifford Gama | Triage Stage: | Accepted |
| Has patch: | yes | Needs documentation: | yes |
| Needs tests: | no | Patch needs improvement: | yes |
| Easy pickings: | no | UI/UX: | no |
Description
Bug Report: SimpleLazyObject.repr causes infinite recursion when setup function is a bound method
Summary
SimpleLazyObject.__repr__ causes infinite recursion when the setup function (_setupfunc) is a bound method of the lazy object instance. This affects LazyNonce in the CSP middleware, making it impossible to safely call repr() on unevaluated CSP nonces.
Affected Version
Django 6.0 (likely also affects earlier versions with SimpleLazyObject, but LazyNonce is new in 6.0)
Minimal Reproduction
from django.middleware.csp import LazyNonce nonce = LazyNonce() repr(nonce) # RecursionError: maximum recursion depth exceeded
Or without CSP imports:
from django.utils.functional import SimpleLazyObject class MyLazy(SimpleLazyObject): def __init__(self): super().__init__(self._generate) # bound method as setupfunc def _generate(self): return "value" obj = MyLazy() repr(obj) # RecursionError: maximum recursion depth exceeded
Root Cause
SimpleLazyObject.__repr__ (django/utils/functional.py) does:
def __repr__(self): if self._wrapped is empty: repr_attr = self._setupfunc else: repr_attr = self._wrapped return "<%s: %r>" % (type(self).__name__, repr_attr)
When _setupfunc is a bound method like self._generate, the %r formatting calls repr() on the bound method. Python's bound method repr includes a repr of the instance it's bound to:
<bound method MyLazy._generate of <MyLazy: ...>>
^^^^^^^^^^^
This calls MyLazy.__repr__ again
This creates infinite recursion:
repr(obj)→SimpleLazyObject.__repr____repr__formats%ronself._setupfunc(bound method)- Bound method repr includes
repr(self) - Go to step 1
Impact
Any code calling repr() on an unevaluated LazyNonce will crash:
- django-debug-toolbar's Templates panel (inspects template context)
- Debuggers (pdb, ipdb, IDE debuggers)
- Logging:
logger.debug("nonce: %r", nonce) - Error reporting tools (Sentry, etc.)
- Interactive shell inspection
Suggested Fix
Option 1: Fix in SimpleLazyObject.__repr__ to detect bound methods of self:
def __repr__(self): if self._wrapped is empty: # Avoid recursion if setupfunc is a bound method of self if hasattr(self._setupfunc, '__self__') and self._setupfunc.__self__ is self: repr_attr = f"<bound method {self._setupfunc.__name__}>" else: repr_attr = self._setupfunc else: repr_attr = self._wrapped return "<%s: %r>" % (type(self).__name__, repr_attr)
Option 2: Override __repr__ in LazyNonce:
class LazyNonce(SimpleLazyObject): # ... existing code ... def __repr__(self): if self._wrapped is empty: return f"<{type(self).__name__}: (unevaluated)>" return f"<{type(self).__name__}: {self._wrapped!r}>"
Option 3: Use a lambda or staticmethod in LazyNonce.__init__ to avoid bound method:
def __init__(self): super().__init__(lambda: secrets.token_urlsafe(16))
Environment
- Django 6.0
- Python 3.14.2
- Discovered via django-debug-toolbar 6.1.0 Templates panel
Change History (12)
comment:1 by , 35 hours ago
| Cc: | added |
|---|
comment:2 by , 35 hours ago
comment:3 by , 35 hours ago
| Has patch: | set |
|---|
comment:4 by , 34 hours ago
| Type: | Uncategorized → Bug |
|---|
comment:5 by , 26 hours ago
| Component: | Uncategorized → Core (Other) |
|---|---|
| Keywords: | LazyNonce csp SimpleLazyObject added |
| Severity: | Normal → Release blocker |
| Triage Stage: | Unreviewed → Accepted |
Thanks for the report! Reproduced for LazyNonce. Marking as release blocker as csp is new in 6.0.
In the future, please avoid using LLMs to write overly verbose reports.
comment:6 by , 22 hours ago
Thank you Sean for the ticket report and thank you Clifford for the triage. I do have one question: the bug is in SimpleLazyObject, so my initial thinking was that this is not a release blocker. Could you comment on your rationale for marking as such? Are you thinking that we should *also* have a defensive __repr__ definition for LazyNonce as a workaround and backport that to 6.0?
follow-up: 8 comment:7 by , 21 hours ago
| Cc: | added |
|---|
I only marked as release blocker because the issue surfaced because of LazyNonce's usage of SimpleLazyObject's __init__(), and I imagined we'd have to do something about LazyNonce.__repr__(). But my knowledge about backporting is a bit naive. Please feel free to adjust the severity as you see fit.
comment:8 by , 21 hours ago
| Needs documentation: | set |
|---|---|
| Patch needs improvement: | set |
Replying to Clifford Gama:
I only marked as release blocker because the issue surfaced because of
LazyNonce's usage ofSimpleLazyObject's__init__(), and I imagined we'd have to do something aboutLazyNonce.__repr__(). But my knowledge about backporting is a bit naive. Please feel free to adjust the severity as you see fit.
Thank you for the extra rationale!
I think we can leave the RB status set and ensure that the fixing PR has two commits:
- one with the fix for the underlying cause in
SimpleLazyObject - one with a workaround for
LazyNonewith a release note so we backport to 6.0
How does that sound?
comment:10 by , 20 hours ago
Sean, can you please address the comments I added and create a new PR targetting Django main branch?
comment:11 by , 15 hours ago
| Owner: | set to |
|---|---|
| Status: | new → assigned |
comment:12 by , 13 hours ago
I've added the workaround for LazyNonce with release note to the PR as requested (I also added a regression test specific to LazyNonce). Thanks all.
Suggested fix (Option 1) available https://github.com/sean-reed/django/tree/ticket_36810