Fix Python Tarfile Symlink Permission Denied Error
Resolve 'Cannot create symlink: Permission denied' in Python tarfile when extracting archives with dereference=False. Workarounds using custom extractor, GNU tar fallback, and debugging tips for symlinks.
How to resolve ‘Cannot create symlink: Permission denied’ errors when extracting archives created with Python’s tarfile module? The issue occurs when using dereference=False to preserve symlinks in the archive, but the same files can be archived successfully using GNU tar directly.
Python tarfile often raises “Cannot create symlink: Permission denied” when extracting archives made with dereference=False because the tarfile extractor either attempts operations your user or filesystem forbids (creating symlinks, changing ownership, or overwriting existing items) or encounters platform-specific symlink restrictions. You can resolve it by (a) extracting symlinks yourself with a safe custom extractor that skips/channels chown/chmod, (b) extracting with GNU tar as a fallback, or © changing how the archive is produced (e.g., dereference symlinks when creating it) — below I explain causes, show diagnostics, and give copy‑paste safe-extraction code and practical workarounds.
Contents
- Why Python tarfile raises “Cannot create symlink: Permission denied”
- Why GNU tar can succeed where tarfile fails
- Quick fixes and choices (when to use which)
- Safe extraction: copy‑paste Python implementation
- Debugging and reproducible checks
- Sources
- Conclusion
Why Python tarfile raises “Cannot create symlink: Permission denied”
Several root causes produce that PermissionError when Python’s tarfile extracts a symlink entry (created with dereference=False):
-
Platform or filesystem restrictions. On Windows creating symlinks usually requires Administrator rights or Developer Mode; on some network filesystems (CIFS/SMB) or with special mount options symlink creation is disallowed. Try
os.symlink('t','l')in a small test to reproduce. The Stack Overflow thread reporting the exact error shows this kind of behavior when symlink creation is blocked in the environment: https://stackoverflow.com/questions/79837782/python-tarfile-produces-cannot-create-symlink-permission-denied-on-extraction -
Parent-directory write permissions or existing file conflicts. Creating a symlink requires write permission on the target directory. If a non-writable object already exists at that path (regular file or directory owned by root),
os.symlink()fails. GNU tar sometimes unlinks/overwrites differently; tarfile’s code path can differ and raise PermissionError instead. -
Attribute-setting calls (chown/chmod) after creation. Python’s tarfile attempts to restore metadata (ownership and modes).
os.lchown()oros.chmod()can raise PermissionError when your account lacks privilege; historically tarfile has had several symlink/attribute bugs and differences in behavior that led to permission-related failures (see CPython bug reports about symlink handling): https://bugs.python.org/issue35483 and https://github.com/python/cpython/issues/57911 -
Archive encoding/format and subtle format quirks. tarfile and GNU tar can encode link names and PAX headers differently; some tarfiles trigger edge-case behavior in tarfile that ends up trying an operation that fails. Recent CPython issues show the module has had multiple symlink-related edge cases: https://github.com/python/cpython/issues/107845
In short: the permission failure is usually an operational/environmental restriction (or a metadata restore step), not a mysterious “tarfile cannot create symlinks at all.”
Why GNU tar can succeed where Python tarfile fails
Why does GNU tar extract the same archive without complaint?
-
Different default behavior for owner/permission restoration. GNU tar typically tolerates some conditions or has command-line flags (
--no-same-owner) that avoid privileged calls; it may also unlink conflicting targets in places where tarfile does not. -
Platform/implementation differences. GNU tar handles certain filesystem edge cases or unsupported metadata differently; it may transform names or fall back more gracefully on Windows or on unusual mounts.
-
GNU tar is a native C program tuned for many filesystem edge cases; tarfile is a pure-Python implementation and historically has had corner-case bugs and different error-handling patterns (see user reports and CPython issues above). For platform-specific extraction you may prefer the system
tarbinary.
If you want the exact behavior of GNU tar, invoking it directly is often the most pragmatic approach.
Quick fixes and choices (when to use which)
Which approach should you pick? Short decision tree:
-
You control the extraction environment and can run shell tools: use GNU tar (fast, robust).
- Example:
tar --no-same-owner -xpf archive.tar -C /destpreserves symlinks without attempting to chown to original owners when you are non-root.
- Example:
-
You must extract in pure Python (embedding, cross-platform code, or Windows): use a safe custom extractor that
- creates symlinks explicitly with
os.symlink(), - skips or safely ignores
chown/chmodfailures, - validates paths (avoid path-traversal),
- falls back for environments that disallow symlinks (create small marker files, copy targets, or log warnings).
- creates symlinks explicitly with
-
You control archive creation and don’t need symlinks later: create archives with
dereference=Trueso the archive contains the file contents rather than symlink entries. -
Quick one-off: run extraction with elevated privileges (sudo) — this will let chown/lchown succeed, but it has security risks and is not recommended for untrusted archives.
Practical commands and examples:
- Extract with GNU tar from Python:
import subprocess, os
os.makedirs('/dest', exist_ok=True)
subprocess.run(['tar', '--no-same-owner', '-xpf', 'archive.tar', '-C', '/dest'], check=True)
(Works on Unix systems with GNU tar installed.)
- If your environment prohibits symlinks (Windows without Developer Mode), consider enabling Developer Mode or running as admin for symlink creation, or fall back to another representation for symlinks.
Safe extraction: copy‑paste Python implementation
Below is a production-ready, well‑documented extractor you can copy into your project. It:
- avoids tarfile.extractall,
- handles symlinks explicitly,
- prevents path traversal (symlink or file names like …/…/etc/passwd),
- avoids chown attempts that would raise PermissionError,
- provides clear fallbacks if symlink creation fails.
import os
import tarfile
import shutil
import errno
def _is_within_directory(directory, target):
abs_directory = os.path.abspath(directory)
abs_target = os.path.abspath(target)
return os.path.commonpath([abs_directory, abs_target]) == abs_directory
def safe_extract(tar_path, dest_dir, *, allow_absolute_links=False):
"""
Safe extraction of a tar archive that preserves symlinks where possible.
- Avoids calling chown/lchown (so non-root extraction does not fail).
- Validates paths to prevent path traversal.
- If os.symlink() fails, writes a small .symlink file with the link target.
"""
os.makedirs(dest_dir, exist_ok=True)
with tarfile.open(tar_path, 'r:*') as tar:
for member in tar.getmembers():
member_path = os.path.join(dest_dir, member.name)
# Prevent path traversal attacks
if not _is_within_directory(dest_dir, member_path):
raise Exception(f"Unsafe path in tar archive: {member.name}")
# Ensure parent exists
parent = os.path.dirname(member_path)
if parent:
os.makedirs(parent, exist_ok=True)
if member.isdir():
os.makedirs(member_path, exist_ok=True)
continue
if member.issym():
link_target = member.linkname
# Optionally refuse absolute link targets
if not allow_absolute_links and os.path.isabs(link_target):
# store the link target in a marker file instead of creating a risky absolute symlink
with open(member_path + '.symlink', 'w', encoding='utf-8') as f:
f.write(link_target)
continue
# Remove existing file/symlink if present
if os.path.lexists(member_path):
try:
os.remove(member_path)
except OSError:
pass
try:
os.symlink(link_target, member_path)
except (OSError, NotImplementedError) as exc:
# Could be PermissionError on Windows or on network FS; write marker as fallback
with open(member_path + '.symlink', 'w', encoding='utf-8') as f:
f.write(link_target)
# optionally log: print(f"Could not create symlink {member.name}: {exc}")
# We avoid calling lchown or setting mode on symlinks (platform differences)
continue
if member.islnk():
# Hard link: try to create a filesystem hard link; if that fails, extract the file contents
link_target = os.path.join(dest_dir, member.linkname)
try:
if os.path.exists(link_target):
if os.path.lexists(member_path):
os.remove(member_path)
os.link(link_target, member_path)
continue
except OSError:
# fall back to extracting content below
pass
# Regular file (and fallback for hard link)
f = tar.extractfile(member)
if f is None:
# Some special members have no data; skip
continue
with open(member_path, 'wb') as out_f:
shutil.copyfileobj(f, out_f)
# Best-effort mode/time set; ignore failures (no privileged calls)
try:
os.chmod(member_path, member.mode)
except OSError:
pass
try:
os.utime(member_path, (member.mtime, member.mtime))
except OSError:
pass
Notes on the code:
- It avoids
chowncalls: you will not be able to reproduce original uid/gid unless running as root. - It writes a
.symlinkmarker if symlink creation fails; you can change that to create text files, or to raise an exception if you prefer. - The
_is_within_directorycheck protects against path traversal (a common supply-chain risk).
This approach follows patterns used in community bug workarounds: see the Meson issue where maintainers catch extraction errors and retry after removing conflicting files: https://github.com/mesonbuild/meson/issues/2355
Debugging and reproducible checks
When you see “Cannot create symlink: Permission denied”, run these quick checks to pinpoint the cause:
-
Inspect the archive to see which entries are symlinks:
- On Unix:
tar -tvf archive.tarwill show lines likelrwxrwxrwx user/group size date path -> target. - In Python:python
import tarfile with tarfile.open('archive.tar','r:*') as t: for m in t.getmembers(): if m.issym(): print('symlink', m.name, '->', m.linkname)
- On Unix:
-
Test symlink creation in the extraction directory:
pythonimport os try: os.symlink('target', '/path/to/extract/testlink') os.remove('/path/to/extract/testlink') print('symlink creation allowed') except OSError as e: print('symlink failed:', e)If this fails, the environment or filesystem disallows symlinks (Windows, CIFS mount, restricted container).
-
Check permissions on the parent directory and for existing path conflicts:
ls -la /path/to/extract/thepath- If a file exists and is not writable, remove or adjust ownership before extracting.
-
Try GNU tar extraction as a control:
tar --no-same-owner -xpf archive.tar -C /tmp/test
If GNU tar succeeds, that confirms tarfile-specific behavior differences.
-
Check CPython bug tracker for similar reports before assuming code bug:
- There are several symlink-related issues in the tracker and in GitHub that describe corner cases: https://bugs.python.org/issue35483 and https://github.com/python/cpython/issues/57911
Sources
- https://stackoverflow.com/questions/79837782/python-tarfile-produces-cannot-create-symlink-permission-denied-on-extraction
- https://stackoverflow.com/a/47961948
- https://github.com/mesonbuild/meson/issues/2355
- https://bugs.python.org/issue35483
- https://github.com/python/cpython/issues/57911
- https://github.com/python/cpython/issues/107845
- https://unix.stackexchange.com/questions/669074/how-to-prevent-tar-extract-from-changing-the-permssions-for-current-directory
Conclusion
Python tarfile can produce “Cannot create symlink: Permission denied” for several practical reasons: platform symlink restrictions, directory-permission or existing-file conflicts, and tarfile’s metadata restoration attempts. If you must extract in Python, use a safe custom extractor that creates symlinks explicitly, skips privileged chown/chmod calls, and validates paths (example provided). If you can call system tools, fall back to GNU tar (tar -xpf / --no-same-owner) for the widest compatibility.