Skip to content

Commit c93bdfe

Browse files
authored
fix: Fallback to temp dir if cache does not exist (#1784)
This fixes a misbehaviour in environments where `XDG_CACHE_DIR` is not set and `~/.cache` does not exist, e.g. in the docker image. We simply attempt to create a temporary directory which will be registered with `atexit` to be deleted on exit using `shutil.rmtree()`. A Python 3 approach for future would be to use a `TemporaryDirectory` instead which would get cleaned up when the held object gets dropped out of the TLS stack. It is still possible for the `context.cache_dir` to be `None` if the default directory exists but is not writable, which means some code which uses this property is liable to blow up in such a circumstance.
1 parent 4fd08b0 commit c93bdfe

File tree

1 file changed

+68
-19
lines changed

1 file changed

+68
-19
lines changed

pwnlib/context/__init__.py

Lines changed: 68 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -6,17 +6,20 @@
66
from __future__ import absolute_import
77
from __future__ import division
88

9+
import atexit
910
import collections
11+
import errno
1012
import functools
1113
import logging
1214
import os
15+
import os.path
1316
import platform
17+
import shutil
1418
import six
1519
import socket
16-
import stat
1720
import string
18-
import subprocess
1921
import sys
22+
import tempfile
2023
import threading
2124
import time
2225

@@ -345,6 +348,10 @@ class ContextType(object):
345348
'binary': None,
346349
'bits': 32,
347350
'buffer_size': 4096,
351+
'cache_dir_base': os.environ.get(
352+
'XDG_CACHE_HOME',
353+
os.path.join(os.path.expanduser('~'), '.cache')
354+
),
348355
'cyclic_alphabet': string.ascii_lowercase.encode(),
349356
'cyclic_size': 4,
350357
'delete_corefiles': False,
@@ -1269,6 +1276,21 @@ def buffer_size(self, size):
12691276
"""
12701277
return int(size)
12711278

1279+
@_validator
1280+
def cache_dir_base(self, new_base):
1281+
"""Base directory to use for caching content.
1282+
1283+
Changing this to a different value will clear the `cache_dir` path
1284+
stored in TLS since a new path will need to be generated to respect the
1285+
new `cache_dir_base` value.
1286+
"""
1287+
1288+
if new_base != self.cache_dir_base:
1289+
del self._tls["cache_dir"]
1290+
if os.access(new_base, os.F_OK) and not os.access(new_base, os.W_OK):
1291+
raise OSError(errno.EPERM, "Cache base dir is not writable")
1292+
return new_base
1293+
12721294
@property
12731295
def cache_dir(self):
12741296
"""Directory used for caching data.
@@ -1282,32 +1304,59 @@ def cache_dir(self):
12821304
>>> cache_dir is not None
12831305
True
12841306
>>> os.chmod(cache_dir, 0o000)
1307+
>>> del context._tls['cache_dir']
12851308
>>> context.cache_dir is None
12861309
True
12871310
>>> os.chmod(cache_dir, 0o755)
12881311
>>> cache_dir == context.cache_dir
12891312
True
12901313
"""
1291-
xdg_cache_home = os.environ.get('XDG_CACHE_HOME') or \
1292-
os.path.join(os.path.expanduser('~'), '.cache')
1293-
1294-
if not os.access(xdg_cache_home, os.W_OK):
1295-
return None
1296-
1297-
cache = os.path.join(xdg_cache_home, '.pwntools-cache-%d.%d' % sys.version_info[:2])
1298-
1299-
if not os.path.exists(cache):
1300-
try:
1301-
os.mkdir(cache)
1302-
except OSError:
1303-
return None
1314+
try:
1315+
# If the TLS already has a cache directory path, we return it
1316+
# without any futher checks since it must have been valid when it
1317+
# was set and if that has changed, hiding the TOCTOU here would be
1318+
# potentially confusing
1319+
return self._tls["cache_dir"]
1320+
except KeyError:
1321+
pass
13041322

1305-
# Some wargames e.g. pwnable.kr have created dummy directories
1306-
# which cannot be modified by the user account (owned by root).
1307-
if not os.access(cache, os.W_OK):
1323+
# Attempt to create a Python version specific cache dir and its parents
1324+
cache_dirname = '.pwntools-cache-%d.%d' % sys.version_info[:2]
1325+
cache_dirpath = os.path.join(self.cache_dir_base, cache_dirname)
1326+
try:
1327+
os.makedirs(cache_dirpath)
1328+
except OSError as exc:
1329+
# If we failed for any reason other than the cache directory
1330+
# already existing then we'll fall back to a temporary directory
1331+
# object which doesn't respect the `cache_dir_base`
1332+
if exc.errno != errno.EEXIST:
1333+
try:
1334+
cache_dirpath = tempfile.mkdtemp(prefix=".pwntools-tmp")
1335+
except IOError:
1336+
# This implies no good candidates for temporary files so we
1337+
# have to return `None`
1338+
return None
1339+
else:
1340+
# Ensure the temporary cache dir is cleaned up on exit. A
1341+
# `TemporaryDirectory` would do this better upon garbage
1342+
# collection but this is necessary for Python 2 support.
1343+
atexit.register(shutil.rmtree, cache_dirpath)
1344+
# By this time we have a cache directory which exists but we don't know
1345+
# if it is actually writable. Some wargames e.g. pwnable.kr have
1346+
# created dummy directories which cannot be modified by the user
1347+
# account (owned by root).
1348+
if os.access(cache_dirpath, os.W_OK):
1349+
# Stash this in TLS for later reuse
1350+
self._tls["cache_dir"] = cache_dirpath
1351+
return cache_dirpath
1352+
else:
13081353
return None
13091354

1310-
return cache
1355+
@cache_dir.setter
1356+
def cache_dir(self, v):
1357+
if os.access(v, os.W_OK):
1358+
# Stash this in TLS for later reuse
1359+
self._tls["cache_dir"] = v
13111360

13121361
@_validator
13131362
def delete_corefiles(self, v):

0 commit comments

Comments
 (0)