Skip to content

Commit

Permalink
fix: Fallback to temp dir if cache does not exist
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
maybe-sybr committed Feb 24, 2021
1 parent 957a5a5 commit f91003c
Showing 1 changed file with 63 additions and 19 deletions.
82 changes: 63 additions & 19 deletions pwnlib/context/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,17 +7,22 @@
from __future__ import absolute_import
from __future__ import division

import atexit
import collections
import errno
import functools
import logging
import os
import os.path
import platform
import shutil
import six
import socket
import stat
import string
import subprocess
import sys
import tempfile
import threading
import time

Expand Down Expand Up @@ -335,7 +340,7 @@ class ContextType(object):
# Setting any properties on a ContextType object will throw an
# exception.
#
__slots__ = '_tls',
__slots__ = ('_tls', )

#: Default values for :class:`pwnlib.context.ContextType`
defaults = {
Expand All @@ -346,6 +351,10 @@ class ContextType(object):
'binary': None,
'bits': 32,
'buffer_size': 4096,
'cache_dir_base': os.environ.get(
'XDG_CACHE_HOME',
os.path.join(os.path.expanduser('~'), '.cache')
),
'cyclic_alphabet': string.ascii_lowercase.encode(),
'cyclic_size': 4,
'delete_corefiles': False,
Expand Down Expand Up @@ -1213,6 +1222,21 @@ def buffer_size(self, size):
"""
return int(size)

@_validator
def cache_dir_base(self, new_base):
"""Base directory to use for caching content.
Changing this to a different value will clear the `cache_dir` path
stored in TLS since a new path will need to be generated to respect the
new `cache_dir_base` value.
"""

if new_base != self.cache_dir_base:
del self._tls["cache_dir"]
if os.access(new_base, os.F_OK) and not os.access(new_base, W_OK):
raise OSError(errno.EPERM, "Cache base dir is not writable")
return new_base

@property
def cache_dir(self):
"""Directory used for caching data.
Expand All @@ -1232,27 +1256,47 @@ def cache_dir(self):
>>> cache_dir == context.cache_dir
True
"""
xdg_cache_home = os.environ.get('XDG_CACHE_HOME') or \
os.path.join(os.path.expanduser('~'), '.cache')

if not os.access(xdg_cache_home, os.W_OK):
return None

cache = os.path.join(xdg_cache_home, '.pwntools-cache-%d.%d' % sys.version_info[:2])

if not os.path.exists(cache):
try:
os.mkdir(cache)
except OSError:
return None
try:
# If the TLS already has a cache directory path, we return it
# without any futher checks since it must have been valid when it
# was set and if that has changed, hiding the TOCTOU here would be
# potentially confusing
return self._tls["cache_dir"]
except KeyError:
pass

# Some wargames e.g. pwnable.kr have created dummy directories
# which cannot be modified by the user account (owned by root).
if not os.access(cache, os.W_OK):
# Attempt to create a Python version specific cache dir and its parents
cache_dirname = '.pwntools-cache-%d.%d' % sys.version_info[:2]
cache_dirpath = os.path.join(self.cache_dir_base, cache_dirname)
try:
os.makedirs(cache_dirpath)
except OSError as exc:
# If we failed for any reason other than the cache directory
# already existing then we'll fall back to a temporary directory
# object which doesn't respect the `cache_dir_base`
if exc.errno != errno.EEXIST:
try:
cache_dirpath = tempfile.mkdtemp(prefix=".pwntools-tmp")
except IOError as exc:
# This implies no good candidates for temporary files so we
# have to return `None`
return None
else:
# Ensure the temporary cache dir is cleaned up on exit. A
# `TemporaryDirectory` would do this better upon garbage
# collection but this is necessary for Python 2 support.
atexit.register(shutil.rmtree, cache_dirpath)
# By this time we have a cache directory which exists but we don't know
# if it is actually writable. Some wargames e.g. pwnable.kr have
# created dummy directories which cannot be modified by the user
# account (owned by root).
if os.access(cache_dirpath, os.W_OK):
# Stash this in TLS for later reuse
self._tls["cache_dir"] = cache_dirpath
return cache_dirpath
else:
return None

return cache

@_validator
def delete_corefiles(self, v):
"""Whether pwntools automatically deletes corefiles after exiting.
Expand Down

0 comments on commit f91003c

Please sign in to comment.