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:

  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:

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 Nilesh Pahari, 35 hours ago

Cc: Nilesh Pahari added

comment:2 by Sean Reed, 35 hours ago

comment:3 by Sean Reed, 35 hours ago

Has patch: set

comment:4 by Sean Reed, 34 hours ago

Type: UncategorizedBug

comment:5 by Clifford Gama, 26 hours ago

Component: UncategorizedCore (Other)
Keywords: LazyNonce csp SimpleLazyObject added
Severity: NormalRelease blocker
Triage Stage: UnreviewedAccepted

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 Natalia Bidart, 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?

comment:7 by Clifford Gama, 21 hours ago

Cc: Clifford Gama 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.

in reply to:  7 comment:8 by Natalia Bidart, 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 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.

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 LazyNone with a release note so we backport to 6.0

How does that sound?

comment:9 by Clifford Gama, 21 hours ago

Thanks! sounds good to me.

comment:10 by Natalia Bidart, 20 hours ago

Sean, can you please address the comments I added and create a new PR targetting Django main branch?

comment:11 by Jacob Walls, 15 hours ago

Owner: set to Sean Reed
Status: newassigned

comment:12 by Sean Reed, 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.

Note: See TracTickets for help on using tickets.
Back to Top