Opened 3 weeks ago

Last modified 3 weeks ago

#37191 closed Bug

FileBasedCache.touch() raises ValueError: I/O operation on closed file when the key is already expired — at Version 3

Reported by: Daniel Owned by: Ayoub Bouaik
Component: Core (Cache system) Version: 6.0
Severity: Normal Keywords: cache, FileBasedCache, touch
Cc: Daniel Triage Stage: Ready for checkin
Has patch: yes Needs documentation: no
Needs tests: no Patch needs improvement: no
Easy pickings: yes UI/UX: no

Description (last modified by Jacob Walls)

FileBasedCache.touch() raises ValueError: I/O operation on closed file when called on a key whose timeout has already elapsed (but whose file hasn't been lazily cleaned up yet).

Steps to reproduce:

Use the following settings

CACHES = {
    "default": {
        "BACKEND": "django.core.cache.backends.filebased.FileBasedCache",
        "LOCATION": "/var/tmp/django_cache",
    },
}

Run the following (e.g. from the django shell)

from django.core.cache import cache

cache.add("key", "value", timeout=1)
time.sleep(2)
cache.touch("key", 60)

Result: The last cache.touch call raises ValueError: I/O operation on closed file

Expected: touch() to return False

Stacktrace:

Traceback (most recent call last):
  File "<console>", line 1, in <module>
  File ".../django_cache_test/.venv/lib/python3.13/site-packages/django/core/cache/backends/filebased.py", line 76, in touch
    locks.unlock(f)
    ~~~~~~~~~~~~^^^
  File ".../django_cache_test/.venv/lib/python3.13/site-packages/django/core/files/locks.py", line 127, in unlock
    fcntl.flock(_fd(f), fcntl.LOCK_UN)
                ~~~^^^
  File ".../django_cache_test/.venv/lib/python3.13/site-packages/django/core/files/locks.py", line 27, in _fd
    return f.fileno() if hasattr(f, "fileno") else f
           ~~~~~~~~^^
ValueError: I/O operation on closed file

I assume the issue lies in the fileBasedCache class in django/core/cache/backends/filebased.py

try:
    locks.lock(f, locks.LOCK_EX)
    if self._is_expired(f):
        return False
    else:
        previous_value = pickle.loads(zlib.decompress(f.read()))
        f.seek(0)
        self._write_content(f, timeout, previous_value)
        return True
finally:
    locks.unlock(f)

self._is_expired() closes the file when it detects that the cache expired and returns False. The finally block is still executed and tries to access the previously closed file.

Change History (3)

comment:1 by Ayoub Bouaik, 3 weeks ago

The issue stems from how the finally block interacts with locks.unlock(f) .When the file is expired, _is_expired(f) closes the file descriptor.

Then, the finally block executes locks.unlock(f) which routes through the internal _fd(f) helper:

def _fd(f):
    """Get a filedescriptor from something which could be a file or an fd."""
    return f.fileno() if hasattr(f, "fileno") else f

Because f has the fileno attribute but is already closed, calling f.fileno() throws the ValueError: I/O operation on closed file.

We can safely fix this by wrapping the unlock utility in a check to ensure the file descriptor is still active:

finally:
    if not f.closed:
        locks.unlock(f)

I have reproduced the issue locally and confirmed this fix stops the crash and correctly returns False. I'd be happy to open a pull request with this fix and a regression test if that works

comment:2 by Ayoub Bouaik, 3 weeks ago

Owner: set to Ayoub Bouaik
Status: newassigned

comment:3 by Jacob Walls, 3 weeks ago

Description: modified (diff)
Triage Stage: UnreviewedAccepted
Note: See TracTickets for help on using tickets.
Back to Top