#36810: SimpleLazyObject.__repr__ causes infinite recursion when setup function 
is
a bound method
---------------------------+-----------------------------------------
     Reporter:  sean-reed  |                     Type:  Uncategorized
       Status:  new        |                Component:  Uncategorized
      Version:  6.0        |                 Severity:  Normal
     Keywords:             |             Triage Stage:  Unreviewed
    Has patch:  0          |      Needs documentation:  0
  Needs tests:  0          |  Patch needs improvement:  0
Easy pickings:  0          |                    UI/UX:  0
---------------------------+-----------------------------------------
 = 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 ==

 {{{#!python
 from django.middleware.csp import LazyNonce

 nonce = LazyNonce()
 repr(nonce)  # RecursionError: maximum recursion depth exceeded
 }}}

 Or without CSP imports:

 {{{#!python
 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:

 {{{#!python
 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:
  1. `repr(obj)` → `SimpleLazyObject.__repr__`
  2. `__repr__` formats `%r` on `self._setupfunc` (bound method)
  3. Bound method repr includes `repr(self)`
  4. 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:

 {{{#!python
 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`:

 {{{#!python
 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:

 {{{#!python
 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
-- 
Ticket URL: <https://code.djangoproject.com/ticket/36810>
Django <https://code.djangoproject.com/>
The Web framework for perfectionists with deadlines.

-- 
You received this message because you are subscribed to the Google Groups 
"Django updates" group.
To unsubscribe from this group and stop receiving emails from it, send an email 
to [email protected].
To view this discussion visit 
https://groups.google.com/d/msgid/django-updates/0107019b33bb9a30-d03da08e-da06-4ee7-a845-b4d59881e888-000000%40eu-central-1.amazonses.com.

Reply via email to