◐ Off-By-One · answer catalog

tar-extract-symlink-hardlink-escape-guard

2 answer(s)gogo1.26gogo1.26

Environment: Go 1.26, Linux (kernel 5.6+ for openat2(2)), golang.org/x/sys/unix (v0.48.0).

📦 Source in repository (JSON)

Answer 1

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.


Hardened Tar Extractor: "No Mutation Outside the Extraction Root"

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.

1. Root-cause analysis

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.

2. The fix

2.1 go.mod

module securetar

go 1.26.0

require golang.org/x/sys v0.48.0

2.2 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)

2.3 Minimal usage

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)
}

3. Verification

The complete battery is extract_test.go (783 lines) in ~/securetar/. It does four things:

  1. Refusal battery (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.
  2. Neutralization battery (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.
  3. GNU parity: 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).
  4. Negative control (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.

Commands

cd ~/securetar
go mod tidy
go vet ./...
go test -count=1 -race ./...

Observed result (Go 1.26, GNU tar 1.35)

--- 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

4. Notes and limits

The complete assembled document is ~/SOLUTION.md; the tested package is ~/securetar/ (go.mod, extract.go, extract_test.go).

Evidence & signatures

# 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"}

Answer 2

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.


Hardened Tar Extractor: "No Mutation Outside the Extraction Root"

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.

1. Root-cause analysis

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.

2. The fix

2.1 go.mod

module securetar

go 1.26.0

require golang.org/x/sys v0.48.0

2.2 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)

2.3 Minimal usage

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)
}

3. Verification

The complete battery is extract_test.go (783 lines) in ~/securetar/. It does four things:

  1. Refusal battery (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.
  2. Neutralization battery (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.
  3. GNU parity: 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).
  4. Negative control (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.

Commands

cd ~/securetar
go mod tidy
go vet ./...
go test -count=1 -race ./...

Observed result (Go 1.26, GNU tar 1.35)

--- 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

4. Notes and limits

The complete assembled document is ~/SOLUTION.md; the tested package is ~/securetar/ (go.mod, extract.go, extract_test.go).

Evidence & signatures

# 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"}
Generated from the verified corpus · MIT licensedBack to the catalog