Environment: Go 1.26, Linux (kernel 5.6+ for openat2(2)), golang.org/x/sys/unix (v0.48.0).
I've built and verified the solution. The working package is at ~/securetar/ and the fully assembled markdown is at ~/SOLUTION.md. Here is the solution.
Environment: Go 1.26, Linux (kernel 5.6+ for openat2(2)), golang.org/x/sys/unix (v0.48.0).
Result: all hostile-archive tests refuse with a typed, member-naming error while the outside tree is byte-for-byte and inode-for-inode unchanged; the legitimate archive (dirs, in-root symlinks, in-root hardlinks, GNU sparse file, zero-padded file) matches GNU tar -x 1.35 exactly.
The naive extractor is path-string based:
p := filepath.Join(dest, hdr.Name)
os.MkdirAll(filepath.Dir(p), 0755)
f, _ := os.Create(p) // kernel follows any symlink already at p
io.Copy(f, tr)
The bug is structural: a validated path string is not the object the kernel later opens. Between "check" and "use" the filesystem can differ, and filepath.Join/Clean cannot see symlinks. Every listed attack exploits that gap:
| # | Hostile input | Why string logic fails |
|---|---|---|
| 1 | ../../etc/cron.d/x |
.. escapes dest. |
| 2 | /etc/shadow |
an absolute path makes the directory fd irrelevant. |
| 3 | member s = symlink to /etc, then s/passwd |
os.Create("dest/s/passwd") follows the symlink and writes /etc/passwd; the string looks safe. |
| 4 | hardlink with Linkname = /etc/shadow |
os.Link hard-links the host inode; a later write through it mutates the host file. |
| 5 | a symlink already present in dest |
same as (3) without needing it in the archive. |
| 6 | duplicate names: a symlink, then a/ dir, then a/file |
extraction follows a before replacing it. |
| 7 | TOCTOU: validate dest/x/y, then open(dest/x/y) |
x is swapped for a symlink in between; the check is stale. |
| 8 | %2e%2e/..., fullwidth .., NUL bytes |
any URL-decoding/normalizing layer changes the surface; NUL can truncate a C string. |
The required mental model: express mutations as operations on file descriptors, not on previously-checked strings. openat2(2) with RESOLVE_BENEATH resolves a relative path from a directory fd and fails with EXDEV/ELOOP if any component, symlink, or .. would leave it — atomically, in one syscall. O_NOFOLLOW on final components prevents writing through a symlink. Hard links are made from an already-resolved O_PATH fd with linkat(..., AT_EMPTY_PATH), immune to later renames. Hostile names are still parsed explicitly so they are refused with a typed error, not silently cleaned.
go.modmodule securetar
go 1.26.0
require golang.org/x/sys v0.48.0
extract.go// Package securetar implements a hardened tar extractor whose core invariant is:
//
// no filesystem mutation ever lands outside the extraction root.
//
// Every path is resolved by the kernel relative to an open root directory file
// descriptor with openat2(2) using RESOLVE_BENEATH, so symlinks that leave the
// root, ".." traversal, absolute names and TOCTOU symlink swaps are rejected by
// the kernel itself. Final components are additionally opened with O_NOFOLLOW so
// a member can never be written *through* a symlink. Hard links are created from
// an O_PATH fd of the already-resolved target (linkat + AT_EMPTY_PATH), which is
// immune to a later rename of the target.
//
// The package deliberately refuses hostile members with a typed *ExtractError
// naming the offending member instead of silently sanitizing it.
package securetar
import (
"archive/tar"
"errors"
"fmt"
"io"
"io/fs"
"os"
"path"
"strings"
"time"
"golang.org/x/sys/unix"
)
// ExtractError is returned whenever a member is refused. It always names the
// archive member that triggered the failure.
type ExtractError struct {
Op string // operation: "validate", "open", "mkdir", "symlink", "hardlink", ...
Member string // offending archive member name
Reason string // human readable reason
Escape bool // true when the member escaped (or tried to escape) the root
Err error // underlying error, if any
}
func (e *ExtractError) Error() string {
if e.Err != nil {
return fmt.Sprintf("securetar: %s %q: %s: %v", e.Op, e.Member, e.Reason, e.Err)
}
return fmt.Sprintf("securetar: %s %q: %s", e.Op, e.Member, e.Reason)
}
func (e *ExtractError) Unwrap() error { return e.Err }
// IsEscape reports whether err is an ExtractError caused by an attempt to
// leave the extraction root, either by our validation or by the kernel's
// RESOLVE_BENEATH enforcement (EXDEV).
func IsEscape(err error) bool {
var ee *ExtractError
if errors.As(err, &ee) && ee.Escape {
return true
}
return errors.Is(err, unix.EXDEV)
}
func escapeFrom(err error) bool {
return errors.Is(err, unix.EXDEV) || errors.Is(err, unix.ELOOP)
}
// Root is a race-free handle to an extraction root directory.
type Root struct {
fd int
name string
}
// OpenRoot opens dir as an extraction root. dir should be a dedicated, private
// directory. The returned Root must be closed when done.
func OpenRoot(dir string) (*Root, error) {
fd, err := unix.Open(dir, unix.O_RDONLY|unix.O_DIRECTORY|unix.O_CLOEXEC, 0)
if err != nil {
return nil, fmt.Errorf("securetar: open root %q: %w", dir, err)
}
return &Root{fd: fd, name: dir}, nil
}
func (r *Root) Close() error { return unix.Close(r.fd) }
// openBeneath resolves rel relative to the root (or to dirfd) with
// RESOLVE_BENEATH. Any component that would escape the starting directory
// (absolute symlink, "..", magic link) is rejected by the kernel with EXDEV or
// ELOOP.
func openBeneathAt(dirfd int, rel string, flags int, mode uint32) (int, error) {
how := &unix.OpenHow{
Flags: uint64(flags) | unix.O_CLOEXEC,
Mode: uint64(mode),
Resolve: unix.RESOLVE_BENEATH,
}
return unix.Openat2(dirfd, rel, how)
}
// openBeneath opens a path relative to the extraction root.
func (r *Root) openBeneath(rel string, flags int, mode uint32) (int, error) {
return openBeneathAt(r.fd, rel, flags, mode)
}
// ensureDir opens rel, creating missing directory components one at a time.
// Existing intermediate symlinks are followed by the kernel but only if they
// stay beneath the directory that contains them.
func (r *Root) ensureDir(rel string) (int, error) {
rel = strings.TrimSuffix(rel, "/")
if rel == "" || rel == "." {
return unix.Dup(r.fd)
}
cur, err := unix.Dup(r.fd)
if err != nil {
return -1, err
}
for _, comp := range strings.Split(rel, "/") {
next, err := openBeneathAt(cur, comp, unix.O_RDONLY|unix.O_DIRECTORY, 0)
if err != nil {
if !errors.Is(err, unix.ENOENT) {
unix.Close(cur)
return -1, err
}
if merr := unix.Mkdirat(cur, comp, 0o700); merr != nil && !errors.Is(merr, unix.EEXIST) {
unix.Close(cur)
return -1, merr
}
next, err = openBeneathAt(cur, comp, unix.O_RDONLY|unix.O_DIRECTORY, 0)
if err != nil {
unix.Close(cur)
return -1, err
}
}
unix.Close(cur)
cur = next
}
return cur, nil
}
// parentOf returns a directory fd for the parent of rel plus the final base
// name. Missing parents are created.
func (r *Root) parentOf(rel string) (int, string, error) {
dir, base := path.Split(rel)
dir = strings.TrimSuffix(dir, "/")
if base == "" {
return -1, "", &ExtractError{Op: "validate", Member: rel, Reason: "empty final path component"}
}
if dir == "" {
fd, err := unix.Dup(r.fd)
return fd, base, err
}
fd, err := r.ensureDir(dir)
return fd, base, err
}
// lstatBeneath lstat()s rel without following the final component.
func (r *Root) lstatBeneath(rel string) (*unix.Stat_t, error) {
fd, err := r.openBeneath(rel, unix.O_PATH|unix.O_NOFOLLOW, 0)
if err != nil {
return nil, err
}
defer unix.Close(fd)
var st unix.Stat_t
if err := unix.Fstat(fd, &st); err != nil {
return nil, err
}
return &st, nil
}
// removeFinal unlinks the final component of rel (never a directory).
func (r *Root) removeFinal(rel string) error {
parent, base, err := r.parentOf(rel)
if err != nil {
return err
}
defer unix.Close(parent)
return unlinkAt(parent, base)
}
func unlinkAt(parent int, base string) error {
err := unix.Unlinkat(parent, base, 0)
if err == nil {
return nil
}
if errors.Is(err, unix.EISDIR) || errors.Is(err, unix.EPERM) {
// Try removing an empty directory, but never recurse.
return unix.Unlinkat(parent, base, unix.AT_REMOVEDIR)
}
return err
}
// ---------------------------------------------------------------------------
// Path validation
// ---------------------------------------------------------------------------
// safeRel validates an archive member name. It rejects absolute names, NUL
// bytes and any ".." component instead of cleaning them away. Returns the
// cleaned relative path ("" for the root itself).
func safeRel(name string) (string, error) {
if name == "" {
return "", errors.New("empty member name")
}
if strings.IndexByte(name, 0) >= 0 {
return "", errors.New("member name contains NUL byte")
}
if strings.HasPrefix(name, "/") {
return "", errors.New("absolute member name")
}
comps := strings.Split(name, "/")
out := make([]string, 0, len(comps))
for _, c := range comps {
switch c {
case "", ".":
// benign
case "..":
return "", errors.New(`member name contains ".." component`)
default:
out = append(out, c)
}
}
return strings.Join(out, "/"), nil
}
// linkTargetStaysInside reports whether a symlink target, interpreted relative
// to memberDir, lexically stays inside the root. This is a policy check on top
// of the kernel's RESOLVE_BENEATH guarantee (which is the actual enforcement).
func linkTargetStaysInside(memberDir, target string) bool {
if target == "" || strings.IndexByte(target, 0) >= 0 {
return false
}
if strings.HasPrefix(target, "/") {
return false
}
stack := []string{}
if memberDir != "" && memberDir != "." {
stack = append(stack, strings.Split(memberDir, "/")...)
}
for _, c := range strings.Split(target, "/") {
switch c {
case "", ".":
default:
if c == ".." {
if len(stack) == 0 {
return false
}
stack = stack[:len(stack)-1]
continue
}
stack = append(stack, c)
}
}
return true
}
// ---------------------------------------------------------------------------
// Extractor
// ---------------------------------------------------------------------------
// Extractor extracts a tar stream into a Root.
type Extractor struct {
root *Root
pendingDirs []pendingDir
// Chown, when true (and euid==0), restores ownership.
Chown bool
}
type pendingDir struct {
rel string
modTime time.Time
access time.Time
change time.Time
}
// NewExtractor creates an Extractor that writes into root.
func NewExtractor(root *Root) *Extractor { return &Extractor{root: root} }
// Extract extracts every member of r into root and returns the first refused
// member as a typed error.
func (x *Extractor) Extract(r io.Reader) error {
tr := tar.NewReader(r)
for {
hdr, err := tr.Next()
if err == io.EOF {
break
}
if err != nil {
return err
}
if err := x.extractOne(tr, hdr); err != nil {
return err
}
}
return x.applyPendingDirs()
}
func (x *Extractor) extractOne(tr *tar.Reader, hdr *tar.Header) error {
rel, err := safeRel(hdr.Name)
if err != nil {
return &ExtractError{Op: "validate", Member: hdr.Name, Reason: err.Error(), Escape: true, Err: err}
}
if rel == "" { // "." root entry
return nil
}
switch hdr.Typeflag {
case tar.TypeDir:
return x.extractDir(hdr, rel)
case tar.TypeReg, tar.TypeRegA, tar.TypeGNUSparse:
return x.extractReg(tr, hdr, rel)
case tar.TypeSymlink:
return x.extractSymlink(hdr, rel)
case tar.TypeLink:
return x.extractHardlink(hdr, rel)
default:
return &ExtractError{
Op: "extract",
Member: hdr.Name,
Reason: fmt.Sprintf("unsupported tar entry type %q", hdr.Typeflag),
}
}
}
func (x *Extractor) extractReg(tr *tar.Reader, hdr *tar.Header, rel string) error {
if dir := path.Dir(rel); dir != "." {
if _, err := x.root.ensureDir(dir); err != nil {
return &ExtractError{Op: "mkdir", Member: hdr.Name, Reason: "cannot create parent directory", Err: err}
}
}
flags := unix.O_WRONLY | unix.O_CREAT | unix.O_TRUNC | unix.O_NOFOLLOW
mode := uint32(hdr.Mode) & 0o7777
fd, err := x.root.openBeneath(rel, flags, mode)
if errors.Is(err, unix.ELOOP) {
// Existing symlink at the final component: unlink it, never follow it.
if rerr := x.root.removeFinal(rel); rerr == nil {
fd, err = x.root.openBeneath(rel, flags, mode)
}
}
if err != nil {
return &ExtractError{Op: "open", Member: hdr.Name, Reason: "cannot create regular file", Escape: escapeFrom(err), Err: err}
}
f := os.NewFile(uintptr(fd), hdr.Name)
defer f.Close()
n, err := io.Copy(f, tr)
if err != nil {
return &ExtractError{Op: "write", Member: hdr.Name, Reason: "write failed", Err: err}
}
if hdr.Size >= 0 && n != hdr.Size {
// Sparse files are expanded to their logical size by archive/tar; a
// short copy would mean a malformed stream.
if hdr.Typeflag != tar.TypeGNUSparse && !hasGNUsparse(hdr) {
return &ExtractError{Op: "write", Member: hdr.Name, Reason: "short write: stream ended early"}
}
}
if err := x.setMetadata(fd, hdr); err != nil {
return err
}
return nil
}
func hasGNUsparse(hdr *tar.Header) bool {
if hdr.PAXRecords == nil {
return false
}
_, ok := hdr.PAXRecords["GNU.sparse.map"]
if ok {
return true
}
_, ok = hdr.PAXRecords["GNU.sparse.major"]
return ok
}
func (x *Extractor) extractDir(hdr *tar.Header, rel string) error {
// If the final component already exists as a symlink (e.g. a hostile
// symlink member followed by a directory member of the same name), replace
// it with a real directory instead of following it.
if st, err := x.root.lstatBeneath(rel); err == nil && st.Mode&unix.S_IFMT == unix.S_IFLNK {
if err := x.root.removeFinal(rel); err != nil {
return &ExtractError{Op: "mkdir", Member: hdr.Name, Reason: "cannot replace symlink with directory", Err: err}
}
}
fd, err := x.root.ensureDir(rel)
if err != nil {
return &ExtractError{Op: "mkdir", Member: hdr.Name, Reason: "cannot create directory", Err: err}
}
defer unix.Close(fd)
if err := unix.Fchmod(fd, uint32(hdr.Mode)&0o7777); err != nil && !errors.Is(err, unix.EPERM) {
return &ExtractError{Op: "chmod", Member: hdr.Name, Reason: "chmod directory", Err: err}
}
if x.Chown && os.Geteuid() == 0 {
_ = unix.Fchown(fd, hdr.Uid, hdr.Gid)
}
x.pendingDirs = append(x.pendingDirs, pendingDir{
rel: rel,
modTime: hdr.ModTime,
access: hdr.AccessTime,
change: hdr.ChangeTime,
})
return nil
}
func (x *Extractor) extractSymlink(hdr *tar.Header, rel string) error {
memberDir := path.Dir(rel)
if !linkTargetStaysInside(memberDir, hdr.Linkname) {
return &ExtractError{
Op: "symlink",
Member: hdr.Name,
Reason: fmt.Sprintf("symlink target %q escapes extraction root", hdr.Linkname),
Escape: true,
}
}
parent, base, err := x.root.parentOf(rel)
if err != nil {
return &ExtractError{Op: "symlink", Member: hdr.Name, Reason: "cannot create parent directory", Err: err}
}
defer unix.Close(parent)
if err := unix.Symlinkat(hdr.Linkname, parent, base); err != nil {
if errors.Is(err, unix.EEXIST) {
if rerr := unlinkAt(parent, base); rerr == nil {
err = unix.Symlinkat(hdr.Linkname, parent, base)
}
}
if err != nil {
return &ExtractError{Op: "symlink", Member: hdr.Name, Reason: "symlinkat failed", Err: err}
}
}
return nil
}
func (x *Extractor) extractHardlink(hdr *tar.Header, rel string) error {
target, err := safeRel(hdr.Linkname)
if err != nil || target == "" {
return &ExtractError{
Op: "hardlink",
Member: hdr.Name,
Reason: fmt.Sprintf("hardlink target %q escapes extraction root", hdr.Linkname),
Escape: true,
Err: err,
}
}
// Resolve the target atomically beneath the root and require a regular
// file. O_NOFOLLOW means a symlink target is refused rather than linked to.
tfd, err := x.root.openBeneath(target, unix.O_PATH|unix.O_NOFOLLOW, 0)
if err != nil {
return &ExtractError{Op: "hardlink", Member: hdr.Name, Reason: fmt.Sprintf("hardlink target %q not accessible", hdr.Linkname), Escape: escapeFrom(err), Err: err}
}
defer unix.Close(tfd)
var st unix.Stat_t
if err := unix.Fstat(tfd, &st); err != nil {
return &ExtractError{Op: "hardlink", Member: hdr.Name, Reason: "cannot stat hardlink target", Err: err}
}
if st.Mode&unix.S_IFMT != unix.S_IFREG {
return &ExtractError{Op: "hardlink", Member: hdr.Name, Reason: fmt.Sprintf("hardlink target %q is not a regular file", hdr.Linkname)}
}
dparent, dbase, err := x.root.parentOf(rel)
if err != nil {
return &ExtractError{Op: "hardlink", Member: hdr.Name, Reason: "cannot create parent directory", Err: err}
}
defer unix.Close(dparent)
if err := unlinkAt(dparent, dbase); err != nil && !errors.Is(err, unix.ENOENT) {
return &ExtractError{Op: "hardlink", Member: hdr.Name, Reason: "cannot replace existing destination", Err: err}
}
// linkat from the already-resolved target fd. AT_EMPTY_PATH makes the
// operation immune to the target path being swapped after validation.
if err := unix.Linkat(tfd, "", dparent, dbase, unix.AT_EMPTY_PATH); err != nil {
// Fall back for kernels/containers that disallow AT_EMPTY_PATH.
tparent, tbase, perr := x.root.parentOf(target)
if perr != nil {
return &ExtractError{Op: "hardlink", Member: hdr.Name, Reason: "cannot resolve hardlink target parent", Err: perr}
}
defer unix.Close(tparent)
if lerr := unix.Linkat(tparent, tbase, dparent, dbase, 0); lerr != nil {
return &ExtractError{Op: "hardlink", Member: hdr.Name, Reason: "linkat failed", Err: lerr}
}
}
return nil
}
func (x *Extractor) setMetadata(fd int, hdr *tar.Header) error {
if hdr.Typeflag != tar.TypeSymlink && x.Chown && os.Geteuid() == 0 {
_ = unix.Fchown(fd, hdr.Uid, hdr.Gid)
}
if err := unix.Fchmod(fd, uint32(hdr.Mode)&0o7777); err != nil && !errors.Is(err, unix.EPERM) {
return &ExtractError{Op: "chmod", Member: hdr.Name, Reason: "chmod failed", Err: err}
}
atime := hdr.AccessTime
if atime.IsZero() {
atime = hdr.ModTime
}
ts := []unix.Timespec{
unix.NsecToTimespec(atime.UnixNano()),
unix.NsecToTimespec(hdr.ModTime.UnixNano()),
}
if err := unix.UtimesNanoAt(fd, "", ts, unix.AT_EMPTY_PATH); err != nil {
return &ExtractError{Op: "utimes", Member: hdr.Name, Reason: "utimes failed", Err: err}
}
return nil
}
func (x *Extractor) applyPendingDirs() error {
// Deepest first so that writing children does not overwrite the parent's
// restored mtime.
for i := len(x.pendingDirs) - 1; i >= 0; i-- {
pd := x.pendingDirs[i]
fd, err := x.root.openBeneath(pd.rel, unix.O_RDONLY|unix.O_DIRECTORY, 0)
if err != nil {
return &ExtractError{Op: "utimes", Member: pd.rel, Reason: "cannot reopen directory", Err: err}
}
atime := pd.access
if atime.IsZero() {
atime = pd.modTime
}
ts := []unix.Timespec{
unix.NsecToTimespec(atime.UnixNano()),
unix.NsecToTimespec(pd.modTime.UnixNano()),
}
err = unix.UtimesNanoAt(fd, "", ts, unix.AT_EMPTY_PATH)
unix.Close(fd)
if err != nil {
return &ExtractError{Op: "utimes", Member: pd.rel, Reason: "utimes directory failed", Err: err}
}
}
return nil
}
// Extract is a convenience wrapper that opens dest, extracts r and closes it.
func Extract(r io.Reader, dest string) error {
root, err := OpenRoot(dest)
if err != nil {
return err
}
defer root.Close()
return NewExtractor(root).Extract(r)
}
var _ = fs.FileMode(0)
root, err := securetar.OpenRoot("/srv/upload/tmp-123")
if err != nil { return err }
defer root.Close()
err = securetar.NewExtractor(root).Extract(archiveReader)
var ee *securetar.ExtractError
if errors.As(err, &ee) {
log.Printf("refused member %q: %s", ee.Member, ee.Reason)
}
The complete battery is extract_test.go (783 lines) in ~/securetar/. It does four things:
TestHostileArchivesRefused): absolute path, .., nested .., relative/absolute symlink escape, symlink-then-write-through, hardlink-to-/etc/shadow, hardlink .. target, and two pre-existing-symlink-traversed cases — each must return an *ExtractError naming the member.TestHostileArchivesNeutralized): a pre-existing escape symlink replaced by a real directory, an archive symlink replaced by a directory, and percent-encoded / fullwidth / unicode .. lookalikes that must stay inside.TestLegitArchiveMatchesGNUTar (dirs, in-root symlink inside -> a.txt, nested relative symlink, in-root hardlink, 1 MiB GNU sparse file, 128 KiB zero-padded file, created with tar --sparse) and TestSafeWriteThroughSymlinkMatchesGNUTar (writing through a safe in-root symlink).TestHarnessCatchesVulnerableExtractor): a deliberately naive extractor must cause the canary to change, proving the snapshot/inode harness can actually detect escapes.The harness proves containment by full-tree snapshot diff of everything outside root/ (mode, size, symlink target, inode, SHA-256) before/after, plus inode comparison against outside files and /etc/shadow, /etc/passwd, /etc/hosts, plus a lexical symlink-escape check. GNU parity compares type, size, symlink target, content hash, and the inode-sharing (hardlink) partition for every path.
cd ~/securetar
go mod tidy
go vet ./...
go test -count=1 -race ./...
--- PASS: TestHostileArchivesRefused (0.00s)
--- PASS: TestHostileArchivesRefused/absolute-path
--- PASS: TestHostileArchivesRefused/dotdot-traversal
--- PASS: TestHostileArchivesRefused/nested-dotdot-traversal
--- PASS: TestHostileArchivesRefused/symlink-escape-relative
--- PASS: TestHostileArchivesRefused/symlink-escape-absolute
--- PASS: TestHostileArchivesRefused/symlink-then-write-through
--- PASS: TestHostileArchivesRefused/hardlink-to-etc-shadow
--- PASS: TestHostileArchivesRefused/hardlink-dotdot-target
--- PASS: TestHostileArchivesRefused/preexisting-symlink-traversed
--- PASS: TestHostileArchivesRefused/preexisting-absolute-symlink-traversed
--- PASS: TestHostileArchivesNeutralized (0.00s)
--- PASS: TestHostileArchivesNeutralized/preexisting-symlink-replaced-by-directory
--- PASS: TestHostileArchivesNeutralized/archive-symlink-replaced-by-directory
--- PASS: TestHostileArchivesNeutralized/encoded-and-unicode-dotdot-stay-inside
--- PASS: TestSafeWriteThroughSymlinkMatchesGNUTar (0.00s)
--- PASS: TestLegitArchiveMatchesGNUTar (0.01s)
--- PASS: TestHarnessCatchesVulnerableExtractor (0.00s)
--- PASS: TestErrorTypeAndMember (0.00s)
PASS
ok securetar 0.019s
openat2 requires Linux ≥ 5.6; on older kernels either fail fast on ENOSYS or fall back to a manual openat(O_NOFOLLOW|O_DIRECTORY) component walk.openat2 from the root fd (atomic); directory/symlink/hardlink creation uses a resolved parent fd plus mkdirat/symlinkat/linkat. A privileged process able to rename that directory out of the root between syscalls is outside the threat model. Run extraction in a private 0700 directory owned by the extractor (which also makes the pre-existing-hardlink-to-outside precondition impossible), or inside a user/mount namespace.%2e%2e is a literal filename; NUL bytes are rejected. Special files (device nodes, FIFOs) are refused rather than created.archive/tar expands sparse members to logical size, so contents are byte-identical though physical holes are not reproduced; use fallocate/SEEK_HOLE if on-disk sparseness matters.The complete assembled document is ~/SOLUTION.md; the tested package is ~/securetar/ (go.mod, extract.go, extract_test.go).
# Evidence - Problem class: tar-extract-symlink-hardlink-escape-guard - Model: openrouter/deepseek/deepseek-v4.1-flash - Solved: 2026-09-17T22:12:38.863Z - Verification: solution produced by pi in sandbox; see signatures.json
{"description": "Write a hardened tar extractor in Go that maintains the invariant 'no filesystem mutation ever lands outside the extraction root' against hostile archives, not just path strings: handle '..' components, absolute member names, symlink members that point outside and are then traversed by a later member, hardlink members whose target is outside the root, pre-existing symlinks in the destination tree, duplicate member names where an earlier symlink is replaced by a directory, and TOCTOU races between validation and write. The implementation must resolve every member through openat/O_NOFOLLOW-style operations relative to a root dir fd (or an equivalent race-free scheme) and refuse, with a typed error naming the offending member, rather than sanitizing silently. Acceptance: a crafted archive battery (symlink-escape, hardlink-to-/etc/shadow, '..' traversal, symlink-then-write-through, absolute path, unicode/percent-encoded variants) extracts nothing outside a temp root verified by full-tree snapshot diff and inode comparison, while a legitimate archive with directories, symlinks inside the root, hardlinks inside the root, and sparse/zero-padded files extracts byte-identically to GNU tar -x.", "environment": "go1.26", "language": "go", "model": "openrouter/deepseek/deepseek-v4.1-flash", "problem_class": "tar-extract-symlink-hardlink-escape-guard", "provider": "openrouter", "solved_at": "2026-09-17T22:12:38.863Z", "version": "1.26"}I've built and verified the solution. The working package is at ~/securetar/ and the fully assembled markdown is at ~/SOLUTION.md. Here is the solution.
Environment: Go 1.26, Linux (kernel 5.6+ for openat2(2)), golang.org/x/sys/unix (v0.48.0).
Result: all hostile-archive tests refuse with a typed, member-naming error while the outside tree is byte-for-byte and inode-for-inode unchanged; the legitimate archive (dirs, in-root symlinks, in-root hardlinks, GNU sparse file, zero-padded file) matches GNU tar -x 1.35 exactly.
The naive extractor is path-string based:
p := filepath.Join(dest, hdr.Name)
os.MkdirAll(filepath.Dir(p), 0755)
f, _ := os.Create(p) // kernel follows any symlink already at p
io.Copy(f, tr)
The bug is structural: a validated path string is not the object the kernel later opens. Between "check" and "use" the filesystem can differ, and filepath.Join/Clean cannot see symlinks. Every listed attack exploits that gap:
| # | Hostile input | Why string logic fails |
|---|---|---|
| 1 | ../../etc/cron.d/x |
.. escapes dest. |
| 2 | /etc/shadow |
an absolute path makes the directory fd irrelevant. |
| 3 | member s = symlink to /etc, then s/passwd |
os.Create("dest/s/passwd") follows the symlink and writes /etc/passwd; the string looks safe. |
| 4 | hardlink with Linkname = /etc/shadow |
os.Link hard-links the host inode; a later write through it mutates the host file. |
| 5 | a symlink already present in dest |
same as (3) without needing it in the archive. |
| 6 | duplicate names: a symlink, then a/ dir, then a/file |
extraction follows a before replacing it. |
| 7 | TOCTOU: validate dest/x/y, then open(dest/x/y) |
x is swapped for a symlink in between; the check is stale. |
| 8 | %2e%2e/..., fullwidth .., NUL bytes |
any URL-decoding/normalizing layer changes the surface; NUL can truncate a C string. |
The required mental model: express mutations as operations on file descriptors, not on previously-checked strings. openat2(2) with RESOLVE_BENEATH resolves a relative path from a directory fd and fails with EXDEV/ELOOP if any component, symlink, or .. would leave it — atomically, in one syscall. O_NOFOLLOW on final components prevents writing through a symlink. Hard links are made from an already-resolved O_PATH fd with linkat(..., AT_EMPTY_PATH), immune to later renames. Hostile names are still parsed explicitly so they are refused with a typed error, not silently cleaned.
go.modmodule securetar
go 1.26.0
require golang.org/x/sys v0.48.0
extract.go// Package securetar implements a hardened tar extractor whose core invariant is:
//
// no filesystem mutation ever lands outside the extraction root.
//
// Every path is resolved by the kernel relative to an open root directory file
// descriptor with openat2(2) using RESOLVE_BENEATH, so symlinks that leave the
// root, ".." traversal, absolute names and TOCTOU symlink swaps are rejected by
// the kernel itself. Final components are additionally opened with O_NOFOLLOW so
// a member can never be written *through* a symlink. Hard links are created from
// an O_PATH fd of the already-resolved target (linkat + AT_EMPTY_PATH), which is
// immune to a later rename of the target.
//
// The package deliberately refuses hostile members with a typed *ExtractError
// naming the offending member instead of silently sanitizing it.
package securetar
import (
"archive/tar"
"errors"
"fmt"
"io"
"io/fs"
"os"
"path"
"strings"
"time"
"golang.org/x/sys/unix"
)
// ExtractError is returned whenever a member is refused. It always names the
// archive member that triggered the failure.
type ExtractError struct {
Op string // operation: "validate", "open", "mkdir", "symlink", "hardlink", ...
Member string // offending archive member name
Reason string // human readable reason
Escape bool // true when the member escaped (or tried to escape) the root
Err error // underlying error, if any
}
func (e *ExtractError) Error() string {
if e.Err != nil {
return fmt.Sprintf("securetar: %s %q: %s: %v", e.Op, e.Member, e.Reason, e.Err)
}
return fmt.Sprintf("securetar: %s %q: %s", e.Op, e.Member, e.Reason)
}
func (e *ExtractError) Unwrap() error { return e.Err }
// IsEscape reports whether err is an ExtractError caused by an attempt to
// leave the extraction root, either by our validation or by the kernel's
// RESOLVE_BENEATH enforcement (EXDEV).
func IsEscape(err error) bool {
var ee *ExtractError
if errors.As(err, &ee) && ee.Escape {
return true
}
return errors.Is(err, unix.EXDEV)
}
func escapeFrom(err error) bool {
return errors.Is(err, unix.EXDEV) || errors.Is(err, unix.ELOOP)
}
// Root is a race-free handle to an extraction root directory.
type Root struct {
fd int
name string
}
// OpenRoot opens dir as an extraction root. dir should be a dedicated, private
// directory. The returned Root must be closed when done.
func OpenRoot(dir string) (*Root, error) {
fd, err := unix.Open(dir, unix.O_RDONLY|unix.O_DIRECTORY|unix.O_CLOEXEC, 0)
if err != nil {
return nil, fmt.Errorf("securetar: open root %q: %w", dir, err)
}
return &Root{fd: fd, name: dir}, nil
}
func (r *Root) Close() error { return unix.Close(r.fd) }
// openBeneath resolves rel relative to the root (or to dirfd) with
// RESOLVE_BENEATH. Any component that would escape the starting directory
// (absolute symlink, "..", magic link) is rejected by the kernel with EXDEV or
// ELOOP.
func openBeneathAt(dirfd int, rel string, flags int, mode uint32) (int, error) {
how := &unix.OpenHow{
Flags: uint64(flags) | unix.O_CLOEXEC,
Mode: uint64(mode),
Resolve: unix.RESOLVE_BENEATH,
}
return unix.Openat2(dirfd, rel, how)
}
// openBeneath opens a path relative to the extraction root.
func (r *Root) openBeneath(rel string, flags int, mode uint32) (int, error) {
return openBeneathAt(r.fd, rel, flags, mode)
}
// ensureDir opens rel, creating missing directory components one at a time.
// Existing intermediate symlinks are followed by the kernel but only if they
// stay beneath the directory that contains them.
func (r *Root) ensureDir(rel string) (int, error) {
rel = strings.TrimSuffix(rel, "/")
if rel == "" || rel == "." {
return unix.Dup(r.fd)
}
cur, err := unix.Dup(r.fd)
if err != nil {
return -1, err
}
for _, comp := range strings.Split(rel, "/") {
next, err := openBeneathAt(cur, comp, unix.O_RDONLY|unix.O_DIRECTORY, 0)
if err != nil {
if !errors.Is(err, unix.ENOENT) {
unix.Close(cur)
return -1, err
}
if merr := unix.Mkdirat(cur, comp, 0o700); merr != nil && !errors.Is(merr, unix.EEXIST) {
unix.Close(cur)
return -1, merr
}
next, err = openBeneathAt(cur, comp, unix.O_RDONLY|unix.O_DIRECTORY, 0)
if err != nil {
unix.Close(cur)
return -1, err
}
}
unix.Close(cur)
cur = next
}
return cur, nil
}
// parentOf returns a directory fd for the parent of rel plus the final base
// name. Missing parents are created.
func (r *Root) parentOf(rel string) (int, string, error) {
dir, base := path.Split(rel)
dir = strings.TrimSuffix(dir, "/")
if base == "" {
return -1, "", &ExtractError{Op: "validate", Member: rel, Reason: "empty final path component"}
}
if dir == "" {
fd, err := unix.Dup(r.fd)
return fd, base, err
}
fd, err := r.ensureDir(dir)
return fd, base, err
}
// lstatBeneath lstat()s rel without following the final component.
func (r *Root) lstatBeneath(rel string) (*unix.Stat_t, error) {
fd, err := r.openBeneath(rel, unix.O_PATH|unix.O_NOFOLLOW, 0)
if err != nil {
return nil, err
}
defer unix.Close(fd)
var st unix.Stat_t
if err := unix.Fstat(fd, &st); err != nil {
return nil, err
}
return &st, nil
}
// removeFinal unlinks the final component of rel (never a directory).
func (r *Root) removeFinal(rel string) error {
parent, base, err := r.parentOf(rel)
if err != nil {
return err
}
defer unix.Close(parent)
return unlinkAt(parent, base)
}
func unlinkAt(parent int, base string) error {
err := unix.Unlinkat(parent, base, 0)
if err == nil {
return nil
}
if errors.Is(err, unix.EISDIR) || errors.Is(err, unix.EPERM) {
// Try removing an empty directory, but never recurse.
return unix.Unlinkat(parent, base, unix.AT_REMOVEDIR)
}
return err
}
// ---------------------------------------------------------------------------
// Path validation
// ---------------------------------------------------------------------------
// safeRel validates an archive member name. It rejects absolute names, NUL
// bytes and any ".." component instead of cleaning them away. Returns the
// cleaned relative path ("" for the root itself).
func safeRel(name string) (string, error) {
if name == "" {
return "", errors.New("empty member name")
}
if strings.IndexByte(name, 0) >= 0 {
return "", errors.New("member name contains NUL byte")
}
if strings.HasPrefix(name, "/") {
return "", errors.New("absolute member name")
}
comps := strings.Split(name, "/")
out := make([]string, 0, len(comps))
for _, c := range comps {
switch c {
case "", ".":
// benign
case "..":
return "", errors.New(`member name contains ".." component`)
default:
out = append(out, c)
}
}
return strings.Join(out, "/"), nil
}
// linkTargetStaysInside reports whether a symlink target, interpreted relative
// to memberDir, lexically stays inside the root. This is a policy check on top
// of the kernel's RESOLVE_BENEATH guarantee (which is the actual enforcement).
func linkTargetStaysInside(memberDir, target string) bool {
if target == "" || strings.IndexByte(target, 0) >= 0 {
return false
}
if strings.HasPrefix(target, "/") {
return false
}
stack := []string{}
if memberDir != "" && memberDir != "." {
stack = append(stack, strings.Split(memberDir, "/")...)
}
for _, c := range strings.Split(target, "/") {
switch c {
case "", ".":
default:
if c == ".." {
if len(stack) == 0 {
return false
}
stack = stack[:len(stack)-1]
continue
}
stack = append(stack, c)
}
}
return true
}
// ---------------------------------------------------------------------------
// Extractor
// ---------------------------------------------------------------------------
// Extractor extracts a tar stream into a Root.
type Extractor struct {
root *Root
pendingDirs []pendingDir
// Chown, when true (and euid==0), restores ownership.
Chown bool
}
type pendingDir struct {
rel string
modTime time.Time
access time.Time
change time.Time
}
// NewExtractor creates an Extractor that writes into root.
func NewExtractor(root *Root) *Extractor { return &Extractor{root: root} }
// Extract extracts every member of r into root and returns the first refused
// member as a typed error.
func (x *Extractor) Extract(r io.Reader) error {
tr := tar.NewReader(r)
for {
hdr, err := tr.Next()
if err == io.EOF {
break
}
if err != nil {
return err
}
if err := x.extractOne(tr, hdr); err != nil {
return err
}
}
return x.applyPendingDirs()
}
func (x *Extractor) extractOne(tr *tar.Reader, hdr *tar.Header) error {
rel, err := safeRel(hdr.Name)
if err != nil {
return &ExtractError{Op: "validate", Member: hdr.Name, Reason: err.Error(), Escape: true, Err: err}
}
if rel == "" { // "." root entry
return nil
}
switch hdr.Typeflag {
case tar.TypeDir:
return x.extractDir(hdr, rel)
case tar.TypeReg, tar.TypeRegA, tar.TypeGNUSparse:
return x.extractReg(tr, hdr, rel)
case tar.TypeSymlink:
return x.extractSymlink(hdr, rel)
case tar.TypeLink:
return x.extractHardlink(hdr, rel)
default:
return &ExtractError{
Op: "extract",
Member: hdr.Name,
Reason: fmt.Sprintf("unsupported tar entry type %q", hdr.Typeflag),
}
}
}
func (x *Extractor) extractReg(tr *tar.Reader, hdr *tar.Header, rel string) error {
if dir := path.Dir(rel); dir != "." {
if _, err := x.root.ensureDir(dir); err != nil {
return &ExtractError{Op: "mkdir", Member: hdr.Name, Reason: "cannot create parent directory", Err: err}
}
}
flags := unix.O_WRONLY | unix.O_CREAT | unix.O_TRUNC | unix.O_NOFOLLOW
mode := uint32(hdr.Mode) & 0o7777
fd, err := x.root.openBeneath(rel, flags, mode)
if errors.Is(err, unix.ELOOP) {
// Existing symlink at the final component: unlink it, never follow it.
if rerr := x.root.removeFinal(rel); rerr == nil {
fd, err = x.root.openBeneath(rel, flags, mode)
}
}
if err != nil {
return &ExtractError{Op: "open", Member: hdr.Name, Reason: "cannot create regular file", Escape: escapeFrom(err), Err: err}
}
f := os.NewFile(uintptr(fd), hdr.Name)
defer f.Close()
n, err := io.Copy(f, tr)
if err != nil {
return &ExtractError{Op: "write", Member: hdr.Name, Reason: "write failed", Err: err}
}
if hdr.Size >= 0 && n != hdr.Size {
// Sparse files are expanded to their logical size by archive/tar; a
// short copy would mean a malformed stream.
if hdr.Typeflag != tar.TypeGNUSparse && !hasGNUsparse(hdr) {
return &ExtractError{Op: "write", Member: hdr.Name, Reason: "short write: stream ended early"}
}
}
if err := x.setMetadata(fd, hdr); err != nil {
return err
}
return nil
}
func hasGNUsparse(hdr *tar.Header) bool {
if hdr.PAXRecords == nil {
return false
}
_, ok := hdr.PAXRecords["GNU.sparse.map"]
if ok {
return true
}
_, ok = hdr.PAXRecords["GNU.sparse.major"]
return ok
}
func (x *Extractor) extractDir(hdr *tar.Header, rel string) error {
// If the final component already exists as a symlink (e.g. a hostile
// symlink member followed by a directory member of the same name), replace
// it with a real directory instead of following it.
if st, err := x.root.lstatBeneath(rel); err == nil && st.Mode&unix.S_IFMT == unix.S_IFLNK {
if err := x.root.removeFinal(rel); err != nil {
return &ExtractError{Op: "mkdir", Member: hdr.Name, Reason: "cannot replace symlink with directory", Err: err}
}
}
fd, err := x.root.ensureDir(rel)
if err != nil {
return &ExtractError{Op: "mkdir", Member: hdr.Name, Reason: "cannot create directory", Err: err}
}
defer unix.Close(fd)
if err := unix.Fchmod(fd, uint32(hdr.Mode)&0o7777); err != nil && !errors.Is(err, unix.EPERM) {
return &ExtractError{Op: "chmod", Member: hdr.Name, Reason: "chmod directory", Err: err}
}
if x.Chown && os.Geteuid() == 0 {
_ = unix.Fchown(fd, hdr.Uid, hdr.Gid)
}
x.pendingDirs = append(x.pendingDirs, pendingDir{
rel: rel,
modTime: hdr.ModTime,
access: hdr.AccessTime,
change: hdr.ChangeTime,
})
return nil
}
func (x *Extractor) extractSymlink(hdr *tar.Header, rel string) error {
memberDir := path.Dir(rel)
if !linkTargetStaysInside(memberDir, hdr.Linkname) {
return &ExtractError{
Op: "symlink",
Member: hdr.Name,
Reason: fmt.Sprintf("symlink target %q escapes extraction root", hdr.Linkname),
Escape: true,
}
}
parent, base, err := x.root.parentOf(rel)
if err != nil {
return &ExtractError{Op: "symlink", Member: hdr.Name, Reason: "cannot create parent directory", Err: err}
}
defer unix.Close(parent)
if err := unix.Symlinkat(hdr.Linkname, parent, base); err != nil {
if errors.Is(err, unix.EEXIST) {
if rerr := unlinkAt(parent, base); rerr == nil {
err = unix.Symlinkat(hdr.Linkname, parent, base)
}
}
if err != nil {
return &ExtractError{Op: "symlink", Member: hdr.Name, Reason: "symlinkat failed", Err: err}
}
}
return nil
}
func (x *Extractor) extractHardlink(hdr *tar.Header, rel string) error {
target, err := safeRel(hdr.Linkname)
if err != nil || target == "" {
return &ExtractError{
Op: "hardlink",
Member: hdr.Name,
Reason: fmt.Sprintf("hardlink target %q escapes extraction root", hdr.Linkname),
Escape: true,
Err: err,
}
}
// Resolve the target atomically beneath the root and require a regular
// file. O_NOFOLLOW means a symlink target is refused rather than linked to.
tfd, err := x.root.openBeneath(target, unix.O_PATH|unix.O_NOFOLLOW, 0)
if err != nil {
return &ExtractError{Op: "hardlink", Member: hdr.Name, Reason: fmt.Sprintf("hardlink target %q not accessible", hdr.Linkname), Escape: escapeFrom(err), Err: err}
}
defer unix.Close(tfd)
var st unix.Stat_t
if err := unix.Fstat(tfd, &st); err != nil {
return &ExtractError{Op: "hardlink", Member: hdr.Name, Reason: "cannot stat hardlink target", Err: err}
}
if st.Mode&unix.S_IFMT != unix.S_IFREG {
return &ExtractError{Op: "hardlink", Member: hdr.Name, Reason: fmt.Sprintf("hardlink target %q is not a regular file", hdr.Linkname)}
}
dparent, dbase, err := x.root.parentOf(rel)
if err != nil {
return &ExtractError{Op: "hardlink", Member: hdr.Name, Reason: "cannot create parent directory", Err: err}
}
defer unix.Close(dparent)
if err := unlinkAt(dparent, dbase); err != nil && !errors.Is(err, unix.ENOENT) {
return &ExtractError{Op: "hardlink", Member: hdr.Name, Reason: "cannot replace existing destination", Err: err}
}
// linkat from the already-resolved target fd. AT_EMPTY_PATH makes the
// operation immune to the target path being swapped after validation.
if err := unix.Linkat(tfd, "", dparent, dbase, unix.AT_EMPTY_PATH); err != nil {
// Fall back for kernels/containers that disallow AT_EMPTY_PATH.
tparent, tbase, perr := x.root.parentOf(target)
if perr != nil {
return &ExtractError{Op: "hardlink", Member: hdr.Name, Reason: "cannot resolve hardlink target parent", Err: perr}
}
defer unix.Close(tparent)
if lerr := unix.Linkat(tparent, tbase, dparent, dbase, 0); lerr != nil {
return &ExtractError{Op: "hardlink", Member: hdr.Name, Reason: "linkat failed", Err: lerr}
}
}
return nil
}
func (x *Extractor) setMetadata(fd int, hdr *tar.Header) error {
if hdr.Typeflag != tar.TypeSymlink && x.Chown && os.Geteuid() == 0 {
_ = unix.Fchown(fd, hdr.Uid, hdr.Gid)
}
if err := unix.Fchmod(fd, uint32(hdr.Mode)&0o7777); err != nil && !errors.Is(err, unix.EPERM) {
return &ExtractError{Op: "chmod", Member: hdr.Name, Reason: "chmod failed", Err: err}
}
atime := hdr.AccessTime
if atime.IsZero() {
atime = hdr.ModTime
}
ts := []unix.Timespec{
unix.NsecToTimespec(atime.UnixNano()),
unix.NsecToTimespec(hdr.ModTime.UnixNano()),
}
if err := unix.UtimesNanoAt(fd, "", ts, unix.AT_EMPTY_PATH); err != nil {
return &ExtractError{Op: "utimes", Member: hdr.Name, Reason: "utimes failed", Err: err}
}
return nil
}
func (x *Extractor) applyPendingDirs() error {
// Deepest first so that writing children does not overwrite the parent's
// restored mtime.
for i := len(x.pendingDirs) - 1; i >= 0; i-- {
pd := x.pendingDirs[i]
fd, err := x.root.openBeneath(pd.rel, unix.O_RDONLY|unix.O_DIRECTORY, 0)
if err != nil {
return &ExtractError{Op: "utimes", Member: pd.rel, Reason: "cannot reopen directory", Err: err}
}
atime := pd.access
if atime.IsZero() {
atime = pd.modTime
}
ts := []unix.Timespec{
unix.NsecToTimespec(atime.UnixNano()),
unix.NsecToTimespec(pd.modTime.UnixNano()),
}
err = unix.UtimesNanoAt(fd, "", ts, unix.AT_EMPTY_PATH)
unix.Close(fd)
if err != nil {
return &ExtractError{Op: "utimes", Member: pd.rel, Reason: "utimes directory failed", Err: err}
}
}
return nil
}
// Extract is a convenience wrapper that opens dest, extracts r and closes it.
func Extract(r io.Reader, dest string) error {
root, err := OpenRoot(dest)
if err != nil {
return err
}
defer root.Close()
return NewExtractor(root).Extract(r)
}
var _ = fs.FileMode(0)
root, err := securetar.OpenRoot("/srv/upload/tmp-123")
if err != nil { return err }
defer root.Close()
err = securetar.NewExtractor(root).Extract(archiveReader)
var ee *securetar.ExtractError
if errors.As(err, &ee) {
log.Printf("refused member %q: %s", ee.Member, ee.Reason)
}
The complete battery is extract_test.go (783 lines) in ~/securetar/. It does four things:
TestHostileArchivesRefused): absolute path, .., nested .., relative/absolute symlink escape, symlink-then-write-through, hardlink-to-/etc/shadow, hardlink .. target, and two pre-existing-symlink-traversed cases — each must return an *ExtractError naming the member.TestHostileArchivesNeutralized): a pre-existing escape symlink replaced by a real directory, an archive symlink replaced by a directory, and percent-encoded / fullwidth / unicode .. lookalikes that must stay inside.TestLegitArchiveMatchesGNUTar (dirs, in-root symlink inside -> a.txt, nested relative symlink, in-root hardlink, 1 MiB GNU sparse file, 128 KiB zero-padded file, created with tar --sparse) and TestSafeWriteThroughSymlinkMatchesGNUTar (writing through a safe in-root symlink).TestHarnessCatchesVulnerableExtractor): a deliberately naive extractor must cause the canary to change, proving the snapshot/inode harness can actually detect escapes.The harness proves containment by full-tree snapshot diff of everything outside root/ (mode, size, symlink target, inode, SHA-256) before/after, plus inode comparison against outside files and /etc/shadow, /etc/passwd, /etc/hosts, plus a lexical symlink-escape check. GNU parity compares type, size, symlink target, content hash, and the inode-sharing (hardlink) partition for every path.
cd ~/securetar
go mod tidy
go vet ./...
go test -count=1 -race ./...
--- PASS: TestHostileArchivesRefused (0.00s)
--- PASS: TestHostileArchivesRefused/absolute-path
--- PASS: TestHostileArchivesRefused/dotdot-traversal
--- PASS: TestHostileArchivesRefused/nested-dotdot-traversal
--- PASS: TestHostileArchivesRefused/symlink-escape-relative
--- PASS: TestHostileArchivesRefused/symlink-escape-absolute
--- PASS: TestHostileArchivesRefused/symlink-then-write-through
--- PASS: TestHostileArchivesRefused/hardlink-to-etc-shadow
--- PASS: TestHostileArchivesRefused/hardlink-dotdot-target
--- PASS: TestHostileArchivesRefused/preexisting-symlink-traversed
--- PASS: TestHostileArchivesRefused/preexisting-absolute-symlink-traversed
--- PASS: TestHostileArchivesNeutralized (0.00s)
--- PASS: TestHostileArchivesNeutralized/preexisting-symlink-replaced-by-directory
--- PASS: TestHostileArchivesNeutralized/archive-symlink-replaced-by-directory
--- PASS: TestHostileArchivesNeutralized/encoded-and-unicode-dotdot-stay-inside
--- PASS: TestSafeWriteThroughSymlinkMatchesGNUTar (0.00s)
--- PASS: TestLegitArchiveMatchesGNUTar (0.01s)
--- PASS: TestHarnessCatchesVulnerableExtractor (0.00s)
--- PASS: TestErrorTypeAndMember (0.00s)
PASS
ok securetar 0.019s
openat2 requires Linux ≥ 5.6; on older kernels either fail fast on ENOSYS or fall back to a manual openat(O_NOFOLLOW|O_DIRECTORY) component walk.openat2 from the root fd (atomic); directory/symlink/hardlink creation uses a resolved parent fd plus mkdirat/symlinkat/linkat. A privileged process able to rename that directory out of the root between syscalls is outside the threat model. Run extraction in a private 0700 directory owned by the extractor (which also makes the pre-existing-hardlink-to-outside precondition impossible), or inside a user/mount namespace.%2e%2e is a literal filename; NUL bytes are rejected. Special files (device nodes, FIFOs) are refused rather than created.archive/tar expands sparse members to logical size, so contents are byte-identical though physical holes are not reproduced; use fallocate/SEEK_HOLE if on-disk sparseness matters.The complete assembled document is ~/SOLUTION.md; the tested package is ~/securetar/ (go.mod, extract.go, extract_test.go).
# Evidence - Problem class: tar-extract-symlink-hardlink-escape-guard - Model: openrouter/deepseek/deepseek-v4.1-flash - Solved: 2026-09-17T22:12:38.863Z - Verification: solution produced by pi in sandbox; see signatures.json
{"description": "Write a hardened tar extractor in Go that maintains the invariant 'no filesystem mutation ever lands outside the extraction root' against hostile archives, not just path strings: handle '..' components, absolute member names, symlink members that point outside and are then traversed by a later member, hardlink members whose target is outside the root, pre-existing symlinks in the destination tree, duplicate member names where an earlier symlink is replaced by a directory, and TOCTOU races between validation and write. The implementation must resolve every member through openat/O_NOFOLLOW-style operations relative to a root dir fd (or an equivalent race-free scheme) and refuse, with a typed error naming the offending member, rather than sanitizing silently. Acceptance: a crafted archive battery (symlink-escape, hardlink-to-/etc/shadow, '..' traversal, symlink-then-write-through, absolute path, unicode/percent-encoded variants) extracts nothing outside a temp root verified by full-tree snapshot diff and inode comparison, while a legitimate archive with directories, symlinks inside the root, hardlinks inside the root, and sparse/zero-padded files extracts byte-identically to GNU tar -x.", "environment": "go1.26", "language": "go", "model": "openrouter/deepseek/deepseek-v4.1-flash", "problem_class": "tar-extract-symlink-hardlink-escape-guard", "provider": "openrouter", "solved_at": "2026-09-17T22:12:38.863Z", "version": "1.26"}