internlm/SWE-Fixer-Train-110K huggingface dataset:
Repository: Gallopsled/pwntools

Issue:
Travis tests are broken
Looks like tests broke somehow.  

We didn't change anything that would cause this, something in the dependency / testing stack changed.

Good: https://travis-ci.org/Gallopsled/pwntools/builds/338641937
Bad: https://travis-ci.org/Gallopsled/pwntools/builds/341197032

Error message:
```
$ PWNLIB_NOTERM=1 coverage run -m sphinx -b doctest docs/source docs/build/doctest
Running Sphinx v1.7.0
Extension error:
Could not import extension pwnlib.internal.dochelper (exception: No module named compat)
Coverage.py warning: Module ~/.pwntools-cache/ was never imported. (module-not-imported)
The command "PWNLIB_NOTERM=1 coverage run -m sphinx -b doctest docs/source docs/build/doctest" exited with 2.
```

Separately, we should probably have something that auto-files a bug when tests break.



Discussion:
@Idolf this will likely block the next release

Resolving patch:
diff --git a/pwnlib/rop/rop.py b/pwnlib/rop/rop.py
--- a/pwnlib/rop/rop.py
+++ b/pwnlib/rop/rop.py
@@ -277,6 +277,7 @@
 import itertools
 import os
 import re
+import shutil
 import six
 import string
 import sys
@@ -1060,13 +1061,19 @@ def __get_cachefile_name(self, files):
 
         return os.path.join(cachedir, sha256.hexdigest())
 
+    @staticmethod
+    def clear_cache():
+        """Clears the ROP gadget cache"""
+        cachedir = os.path.join(tempfile.gettempdir(), 'pwntools-rop-cache')
+        shutil.rmtree(cachedir)
+
     def __cache_load(self, elf):
         filename = self.__get_cachefile_name(elf)
         if not os.path.exists(filename):
             return None
-        log.info_once('Loaded cached gadgets for %r' % elf.file.name)
         gadgets = eval(open(filename).read())
         gadgets = {k - elf.load_addr + elf.address:v for k, v in gadgets.items()}
+        log.info_once('Loaded %s cached gadgets for %r', len(gadgets), elf.file.name)
         return gadgets
 
     def __cache_save(self, elf, data):
diff --git a/setup.py b/setup.py
--- a/setup.py
+++ b/setup.py
@@ -61,7 +61,8 @@
                         'psutil>=3.3.0',
                         'intervaltree>=3.0',
                         'sortedcontainers',
-                        'unicorn']
+                        'unicorn>=1.0.2rc1', # see unicorn-engine/unicorn#1100, unicorn-engine/unicorn#1170
+]
 
 # Check that the user has installed the Python development headers
 PythonH = os.path.join(get_python_inc(), 'Python.h')


Test patch:
Repository: Gallopsled/pwntools

Issue:
Unable to create a shell shellcraft for MIPS
The problem is as follows:
```py
>>> from pwnlib.shellcraft import mips
>>> mips.sh()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<string>", line 8, in sh
  File "/usr/lib64/python2.7/site-packages/mako/template.py", line 462, in render
    return runtime._render(self, self.callable_, args, data)
  File "/usr/lib64/python2.7/site-packages/mako/runtime.py", line 838, in _render
    **_kwargs_for_callable(callable_, data))
  File "/usr/lib64/python2.7/site-packages/mako/runtime.py", line 873, in _render_context
    _exec_template(inherit, lclcontext, args=args, kwargs=kwargs)
  File "/usr/lib64/python2.7/site-packages/mako/runtime.py", line 899, in _exec_template
    callable_(context, *args, **kwargs)
  File "/home/are/.pwntools-cache/mako/mips/linux/sh.asm.py", line 28, in render_body
    __M_writer(unicode(mips.execve('//bin/sh', ['sh'], {})))
  File "<string>", line 8, in execve
  File "/usr/lib64/python2.7/site-packages/mako/template.py", line 462, in render
    return runtime._render(self, self.callable_, args, data)
  File "/usr/lib64/python2.7/site-packages/mako/runtime.py", line 838, in _render
    **_kwargs_for_callable(callable_, data))
  File "/usr/lib64/python2.7/site-packages/mako/runtime.py", line 873, in _render_context
    _exec_template(inherit, lclcontext, args=args, kwargs=kwargs)
  File "/usr/lib64/python2.7/site-packages/mako/runtime.py", line 899, in _exec_template
    callable_(context, *args, **kwargs)
  File "/home/are/.pwntools-cache/mako/mips/linux/syscalls/execve.asm.py", line 69, in render_body
    if arg in allregs:
TypeError: unhashable type: 'list'
```
But it can be fixed by making sure that `shellcraft.registers.current()` returns a list rather than a dict (mips is the only architecture that it happens for, since `type(shellcraft.registers.mips)==dict`). A pull request is on its way.


Discussion:

Resolving patch:
diff --git a/pwnlib/shellcraft/registers.py b/pwnlib/shellcraft/registers.py
--- a/pwnlib/shellcraft/registers.py
+++ b/pwnlib/shellcraft/registers.py
@@ -211,7 +211,7 @@ def current():
         'arm': arm,
         'thumb': arm,
         'aarch64': aarch64,
-        'mips': mips,
+        'mips': list(mips),
         'powerpc': powerpc
     }[context.arch]
 


Test patch:
Repository: Gallopsled/pwntools

Issue:
ELF.search gives false positives
`ELF.search` can sometimes give out false positives.

For instance:

```
$ ipython
In [1]: hex(max(elf.ELF('./foo').search('e')))


Out[1]: '0x60102f'
```

```
$ gdb ./foo
(gdb) x/1xb 0x60102f
0x60102f:	0x00
(gdb) start
Temporary breakpoint 1 at 0x4003b0
Starting program: /home/idolf/git/pcs-f2018/weeklies/week6-rop/rop-forensics/foo 

Temporary breakpoint 1, 0x00000000004003b0 in main ()
(gdb) x/1xb 0x60102f
0x60102f:	0x00
```
[foo.zip](https://github.com/Gallopsled/pwntools/files/2084686/foo.zip)



Discussion:
The binary was generated with `echo 'int main() {}' | gcc -x c - -no-pie -fno-PIE -o foo`

Resolving patch:
diff --git a/pwnlib/elf/elf.py b/pwnlib/elf/elf.py
--- a/pwnlib/elf/elf.py
+++ b/pwnlib/elf/elf.py
@@ -926,10 +926,12 @@ def search(self, needle, writable = False):
         for seg in segments:
             addr   = seg.header.p_vaddr
             memsz  = seg.header.p_memsz
-            zeroed = memsz - seg.header.p_filesz
+            filesz = seg.header.p_filesz
+            zeroed = memsz - filesz
             offset = seg.header.p_offset
-            data   = self.mmap[offset:offset+memsz]
+            data   = self.mmap[offset:offset+filesz]
             data   += '\x00' * zeroed
+            assert len(data) == memsz
             offset = 0
             while True:
                 offset = data.find(needle, offset)


Test patch:
Repository: Gallopsled/pwntools

Issue:
ELF.search gives false positives
`ELF.search` can sometimes give out false positives.

For instance:

```
$ ipython
In [1]: hex(max(elf.ELF('./foo').search('e')))


Out[1]: '0x60102f'
```

```
$ gdb ./foo
(gdb) x/1xb 0x60102f
0x60102f:	0x00
(gdb) start
Temporary breakpoint 1 at 0x4003b0
Starting program: /home/idolf/git/pcs-f2018/weeklies/week6-rop/rop-forensics/foo 

Temporary breakpoint 1, 0x00000000004003b0 in main ()
(gdb) x/1xb 0x60102f
0x60102f:	0x00
```
[foo.zip](https://github.com/Gallopsled/pwntools/files/2084686/foo.zip)



Discussion:
The binary was generated with `echo 'int main() {}' | gcc -x c - -no-pie -fno-PIE -o foo`

Resolving patch:
diff --git a/pwnlib/elf/elf.py b/pwnlib/elf/elf.py
--- a/pwnlib/elf/elf.py
+++ b/pwnlib/elf/elf.py
@@ -926,12 +926,10 @@ def search(self, needle, writable = False):
         for seg in segments:
             addr   = seg.header.p_vaddr
             memsz  = seg.header.p_memsz
-            filesz = seg.header.p_filesz
-            zeroed = memsz - filesz
+            zeroed = memsz - seg.header.p_filesz
             offset = seg.header.p_offset
-            data   = self.mmap[offset:offset+filesz]
+            data   = self.mmap[offset:offset+memsz]
             data   += '\x00' * zeroed
-            assert len(data) == memsz
             offset = 0
             while True:
                 offset = data.find(needle, offset)


Test patch:
Repository: Gallopsled/pwntools

Issue:
"IOError: File name too long " when call ROP
Error log:
> Traceback (most recent call last):
>   File "vortex12.py", line 46, in <module>
>     rop   = ROP(libs)
>   File "/usr/local/lib/python2.7/dist-packages/pwnlib/rop/rop.py", line 466, in __init__
>     self.__load()
>   File "/usr/local/lib/python2.7/dist-packages/pwnlib/rop/rop.py", line 1090, in __load
>     self.__cache_save(elf, elf_gadgets)
>   File "/usr/local/lib/python2.7/dist-packages/pwnlib/rop/rop.py", line 1009, in __cache_save
>     file(self.__get_cachefile_name(elf), 'w+').write(repr(data))
> IOError: [Errno 36] File name too long: '/tmp/pwntools-rop-cache/2af3321088068fd0fabfbc8c938b47aa2a22e3876b39aa0f9c894f02f1c5edf6_e79282760fb8d4fd9c7310515c6d9617c28f95fc9562a9058cb69cd861c534be_331321edfc0b8c9b643600ce1b5bf7348b2a635cd04b1ec631cf65b8128afe12_066e2be587ae87d77cf03fd5a2155f1a203c8e3d2d405ca89f006c81c7145e58'

This probably what cause problem:

```
def __get_cachefile_name(self, files):
        """Given an ELF or list of ELF objects, return a cache file for the set of files"""
        cachedir = os.path.join(tempfile.gettempdir(), 'pwntools-rop-cache')
        if not os.path.exists(cachedir):
            os.mkdir(cachedir)

        if isinstance(files, ELF):
            files = [files]

        hashes = []

        for elf in self.elfs:
            sha256 = hashlib.sha256(elf.get_data()).hexdigest()
            hashes.append(sha256)

        return os.path.join(cachedir, '_'.join(hashes))
```
Linux max filename length only 255 so instead of join hash files name, dont better should hash the join file name ?


Discussion:

Resolving patch:
diff --git a/pwnlib/rop/rop.py b/pwnlib/rop/rop.py
--- a/pwnlib/rop/rop.py
+++ b/pwnlib/rop/rop.py
@@ -1010,14 +1010,12 @@ def __get_cachefile_name(self, files):
 
         if isinstance(files, ELF):
             files = [files]
+        
+        sha256 = hashlib.sha256()
+        for elf_data in sorted(elf.get_data() for elf in self.elfs):
+            sha256.update(elf_data)
 
-        hashes = []
-
-        for elf in self.elfs:
-            sha256 = hashlib.sha256(elf.get_data()).hexdigest()
-            hashes.append(sha256)
-
-        return os.path.join(cachedir, '_'.join(hashes))
+        return os.path.join(cachedir, sha256.hexdigest())
 
     def __cache_load(self, elf):
         filename = self.__get_cachefile_name(elf)


Test patch:
Repository: Gallopsled/pwntools

Issue:
Memory leak in pwnlib.tubes.ssh
In function pwnlib.tubes.ssh.download, self.system is called this way:

> if 0 == self.system('test -d ' + sh_string(file_or_directory)).wait():
>             self.download_dir(file_or_directory, local)
>         else:
>             self.download_file(file_or_directory, local)

=> The ssh channel created by system is not closed => memory leak
It should be something like this:

> s = self.system('test -d ' + sh_string(file_or_directory))
>         if 0 == s.wait():
>             self.download_dir(file_or_directory, local)
>         else:
>             self.download_file(file_or_directory, local)
>         s.close()


Discussion:

Resolving patch:
diff --git a/pwnlib/tubes/ssh.py b/pwnlib/tubes/ssh.py
--- a/pwnlib/tubes/ssh.py
+++ b/pwnlib/tubes/ssh.py
@@ -1619,7 +1619,6 @@ def upload(self, file_or_directory, remote=None):
 
         self.error('%r does not exist' % file_or_directory)
 
-
     def download(self, file_or_directory, local=None):
         """download(file_or_directory, local=None)
 
@@ -1633,7 +1632,10 @@ def download(self, file_or_directory, local=None):
         if not self.sftp:
             self.error("Cannot determine remote file type without SFTP")
 
-        if 0 == self.system('test -d ' + sh_string(file_or_directory)).wait():
+        with self.system('test -d ' + sh_string(file_or_directory)) as io:
+            is_dir = io.wait()
+
+        if 0 == is_dir:
             self.download_dir(file_or_directory, local)
         else:
             self.download_file(file_or_directory, local)


Test patch:
Repository: Gallopsled/pwntools

Issue:
Infinite loop for a specially crafted escape sequence
Hi,

here is a small script to reproduce the issue:

```
#!/usr/bin/env python
from pwn import *
print('\x1bP\x1b')
```

Note: if one receives '\x1bP\x1b' in a `p = process()` via `p.recv()` and then launches `p.interactive()`, even `Ctrl+C` cannot stop the infinite loop, and one needs to kill the python process.

The code responsible for the infinite loop is found in file `pwnlib/term/term.py` and function `parse()`.
* The escape sequence '\x1bP' enforces `_graphics_mode = True`
* The final '\x1b' makes sure all tests performed in the `while True:` loop fail, so that one lands on: 
```
if _graphics_mode:
    continue
```
hence the infinite loop.

All the best


Discussion:

Resolving patch:
diff --git a/pwnlib/term/term.py b/pwnlib/term/term.py
--- a/pwnlib/term/term.py
+++ b/pwnlib/term/term.py
@@ -246,9 +246,7 @@ def parse(s):
     out = []
     buf = map(ord, s)
     i = 0
-    while True:
-        if i >= len(buf):
-            break
+    while i < len(buf):
         x = None
         c = buf[i]
         if c >= 0x20 and c <= 0x7e:
@@ -315,6 +313,9 @@ def parse(s):
         elif c == 0x0d:
             x = (CR, None)
             i += 1
+        else:
+            i += 1
+
         if _graphics_mode:
             continue
         if x is None:


Test patch:
Repository: Gallopsled/pwntools

Issue:
'from pwn import *' fails when one of the command line args is a Base64 string (or has more than one equal sign)
The argument parsing functionality invoked by `from pwn import *` causes an error when one of the command line arguments has more than one equal sign:
`echo "from pwn import *\nprint('hello world')" > test.py`
`python test.py afdsa==`

```
Traceback (most recent call last):
  File "test.py", line 1, in <module>
    from pwn import *
  File "/usr/local/lib/python2.7/site-packages/pwn/__init__.py", line 6, in <module>
    pwnlib.args.initialize()
  File "/usr/local/lib/python2.7/site-packages/pwnlib/args.py", line 197, in initialize
    arg, value = arg.split('=')
ValueError: too many values to unpack
```

https://github.com/Gallopsled/pwntools/blob/dev/pwnlib/args.py#L197 probably just needs to be changed to `if arg.count("=") == 1:`


Discussion:
First, the case of multiple `=` signs creating an exception is an actual bug, thank you for the report.  I'll create a fix for this soon.

However, if the bug were not there, the current functionality would be that `pwnlib.args['asdf'] == '='` would be `True` and that item would be removed from `sys.argv`.

This is a unfortunate limitation of using the "kitchen sink" `from pwn import *` mechanism with the current `args` implementation.

You can avoid this mechanism by using `import pwnlib.foo.bar` explicitly for each item you want to use, e.g. 

```py
from pwnlib.tubes.remote import remote`
```

Separately, you can do any necessary processing on `sys.argv` *before* you `from pwn import *`.

A third option might be to create a pull request to add support to `args.py` to stop looking when it encounters e.g. `--` or is disabled entirely when e.g. `PWNLIB_NO_ARGS=1` is set in the environment.

A final "HACKHACKHACK" work-around solution would be to do:

```
import pwnlib.args
pwnlib.args.initialize = lambda: None
from pwn import *
```

Resolving patch:
diff --git a/pwnlib/args.py b/pwnlib/args.py
--- a/pwnlib/args.py
+++ b/pwnlib/args.py
@@ -194,7 +194,7 @@ def initialize():
         value = 'True'
 
         if '=' in arg:
-            arg, value = arg.split('=')
+            arg, value = arg.split('=', 1)
 
         if arg in hooks:
             sys.argv.remove(orig)


Test patch:
Repository: Gallopsled/pwntools

Issue:
`disasm()` creates excess lines when `byte=0` on long instructions
This can be seen in very long instructions, like this one:

```py
>>> print(disasm(unhex('656664676665400F18A4000000000051'), byte=0, arch='amd64'))
   0:   gs data16 fs data16 rex nop/reserved BYTE PTR gs:[eax+eax*1+0x0]
   7:   
   e:   
   f:   push   rcx
```

This can be fixed by passing `--no-show-raw-insns` to `objdump`. A pull request on its way.


Discussion:
Thanks!  I'm curious how you came across this, it's likely a use-case we didn't consider fully.
So I guess the PR should go against dev, as it is just an enhancement? Or should it be a bug fix on stable?

Resolving patch:
diff --git a/pwnlib/asm.py b/pwnlib/asm.py
--- a/pwnlib/asm.py
+++ b/pwnlib/asm.py
@@ -754,6 +754,9 @@ def disasm(data, vma = 0, byte = True, offset = True, instructions = True):
            4:   00900052        addseq  r0, r0, r2, asr r0
         >>> print(disasm(unhex('4ff00500'), arch = 'thumb', bits=32))
            0:   f04f 0005       mov.w   r0, #5
+        >>> print(disasm(unhex('656664676665400F18A4000000000051'), byte=0, arch='amd64'))
+           0:   gs data16 fs data16 rex nop/reserved BYTE PTR gs:[eax+eax*1+0x0]
+           f:   push   rcx
     """
     result = ''
 
@@ -775,6 +778,9 @@ def disasm(data, vma = 0, byte = True, offset = True, instructions = True):
         '--rename-section', '.data=.text',
     ]
 
+    if not byte:
+        objdump += ['--no-show-raw-insn']
+
     if arch == 'thumb':
         objcopy += ['--prefix-symbol=$t.']
     else:
@@ -801,10 +807,17 @@ def disasm(data, vma = 0, byte = True, offset = True, instructions = True):
 
 
     lines = []
-    pattern = '^( *[0-9a-f]+: *)((?:[0-9a-f]+ )+ *)(.*)'
+    pattern = '^( *[0-9a-f]+: *)', '((?:[0-9a-f]+ )+ *)', '(.*)'
+    if not byte:
+        pattern = pattern[::2]
+    pattern = ''.join(pattern)
     for line in result.splitlines():
         try:
-            o, b, i = re.search(pattern, line).groups()
+            groups = re.search(pattern, line).groups()
+            if byte:
+                o, b, i = groups
+            else:
+                o, i = groups
         except:
             lines.append(line)
             continue


Test patch:
Repository: Gallopsled/pwntools

Issue:
Path with space produce bogus temp gdb script file
There is a bug if the path has space in it. You should use double quote for the file command.

https://github.com/Gallopsled/pwntools/blob/224ddb411b8927533d7b3122a992e04f99a3508d/pwnlib/gdb.py#L444

https://github.com/Gallopsled/pwntools/blob/224ddb411b8927533d7b3122a992e04f99a3508d/pwnlib/gdb.py#L728



Discussion:

Resolving patch:
diff --git a/pwnlib/gdb.py b/pwnlib/gdb.py
--- a/pwnlib/gdb.py
+++ b/pwnlib/gdb.py
@@ -441,7 +441,7 @@ def debug(args, gdbscript=None, exe=None, ssh=None, env=None, sysroot=None, **kw
     if not exe:
         log.error("%s does not exist" % orig_args[0])
     else:
-        gdbscript = 'file %s\n%s' % (exe, gdbscript)
+        gdbscript = 'file "%s"\n%s' % (exe, gdbscript)
 
     # Start gdbserver/qemu
     # (Note: We override ASLR here for the gdbserver process itself.)
@@ -725,7 +725,7 @@ def findexe():
 
     if exe:
         # The 'file' statement should go first
-        pre = 'file %s\n%s' % (exe, pre)
+        pre = 'file "%s"\n%s' % (exe, pre)
 
     cmd = binary()
 


Test patch:
Repository: Gallopsled/pwntools

Issue:
phd “-c” flag is not implemented
This is entirely unimplemented


Discussion:

Resolving patch:
diff --git a/pwnlib/commandline/phd.py b/pwnlib/commandline/phd.py
--- a/pwnlib/commandline/phd.py
+++ b/pwnlib/commandline/phd.py
@@ -5,6 +5,7 @@
 import argparse
 import os
 import sys
+import io
 
 import pwnlib
 pwnlib.args.free_form = False
@@ -89,6 +90,9 @@ def main(args):
         else:
             infile.seek(skip, os.SEEK_CUR)
 
+    if count:
+        infile = io.BytesIO(infile.read(count))
+
     hl = []
     if args.highlight:
         for hs in args.highlight:


Test patch:
Repository: Gallopsled/pwntools

Issue:
from pwn import * changes __name__
Running `pwntools==3.14.0.dev0` from py3 dev branch.

from pwn import * imports __name__ apparently.

```python
In [1]: __name__
Out[1]: '__main__'

In [2]: from pwn import *

In [3]: __name__
Out[3]: 'pwn.toplevel'
```


Discussion:

Resolving patch:
diff --git a/pwn/toplevel.py b/pwn/toplevel.py
--- a/pwn/toplevel.py
+++ b/pwn/toplevel.py
@@ -67,7 +67,6 @@
 
 # Promote these modules, so that "from pwn import *" will let you access them
 
-__all__ = ["pickle", "StringIO", "BytesIO"]
 from six.moves import cPickle as pickle, cStringIO as StringIO
 from six import BytesIO
 
@@ -78,4 +77,4 @@
 debug   = log.debug
 success = log.success
 
-__all__ += list(globals().keys())
+__all__ = [x for x in globals().keys() if x != '__name__']


Test patch:
Repository: Gallopsled/pwntools

Issue:
ROP builder wrong behavior for multiple binaries
When building a ROP chain with multiple ELFs, there seems to be a bug.

I'm getting wildly varying behavior depending on the order in which I load the ELFs, which definitely should not be happening, gadgets using the wrong base address, and other issues.

The easiest way to demonstrate the problem is the following:

```python
from pwn import *
context.arch = "amd64"

e1 = ELF.from_assembly("pop rdi; ret")
e2 = ELF.from_assembly("pop rdi; ret; pop rax; ret; pop rdx; ret")

rop1 = ROP([e1, e2])
rop2 = ROP([e2, e1])

print(len(rop1.gadgets)) #should print 2
print(len(rop2.gadgets)) #should print 4
```

rop1 and rop2 will have a different number of gadgets, even though they are both built from the exact same ELFs, which shouldn't be happening as it should be finding gadgets from both passed ELFs.


Discussion:
I misunderstood the original problem, and have reopened this.
This might be due to sorting of hashes in cache file names, and also due to caching not being deterministic when passing in [a,b] vs [b,a].  

Resolving patch:
diff --git a/pwnlib/rop/rop.py b/pwnlib/rop/rop.py
--- a/pwnlib/rop/rop.py
+++ b/pwnlib/rop/rop.py
@@ -1012,7 +1012,7 @@ def __get_cachefile_name(self, files):
             files = [files]
 
         sha256 = hashlib.sha256()
-        for elf_data in sorted(elf.get_data() for elf in self.elfs):
+        for elf_data in sorted(elf.get_data() for elf in files):
             sha256.update(elf_data)
 
         return os.path.join(cachedir, sha256.hexdigest())


Test patch:
Repository: Gallopsled/pwntools

Issue:
Respect XDG Base Directory Spec
I've noticed `~/.pwntools-cache` files appearing. These should be stored in the XDG Cache directory by default, not in the `$HOME` directory. It'd be nice if there were a way to configure this.



Discussion:
Would you mind submitting a pull request for this? The default paths are generated by a function in `pwnlib/context/__init__.py`.
https://github.com/Gallopsled/pwntools/blob/faab6ea3dd8806ce693681ca4b7cfb451a335c8c/pwnlib/context/__init__.py#L1232

This is the line that needs to be modified. Don't have time at the moment, but if this hits the top of my priority list at some point in the future, I'll submit a patch.

Resolving patch:
diff --git a/pwnlib/context/__init__.py b/pwnlib/context/__init__.py
--- a/pwnlib/context/__init__.py
+++ b/pwnlib/context/__init__.py
@@ -1225,12 +1225,13 @@ def cache_dir(self):
             >>> cache_dir == context.cache_dir
             True
         """
-        home = os.path.expanduser('~')
+        xdg_cache_home = os.environ.get('XDG_CACHE_HOME') or \
+                         os.path.join(os.path.expanduser('~'), '.cache')
 
-        if not os.access(home, os.W_OK):
+        if not os.access(xdg_cache_home, os.W_OK):
             return None
 
-        cache = os.path.join(home, '.pwntools-cache-%d.%d' % sys.version_info[:2])
+        cache = os.path.join(xdg_cache_home, '.pwntools-cache-%d.%d' % sys.version_info[:2])
 
         if not os.path.exists(cache):
             try:


Test patch: