Opened 57 minutes ago

Last modified 11 minutes ago

#37191 assigned Bug

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

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

Description

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("k", 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 (2)

comment:1 by Ayoub Bouaik, 11 minutes 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, 11 minutes ago

Owner: set to Ayoub Bouaik
Status: newassigned
Note: See TracTickets for help on using tickets.
Back to Top