Skip to content

extractor: make root-only device nodes and ownership opt-in - #18

Merged
unxed merged 1 commit into
mainfrom
lunobot/agent-lb3-zip56/lunobot-3/6-extractor-root-privileged-opt-in
Sep 27, 2026
Merged

unxed merged 1 commit into
mainfrom
lunobot/agent-lb3-zip56/lunobot-3/6-extractor-root-privileged-opt-in

Conversation

@unxed

@unxed unxed commented Sep 27, 2026

Copy link
Copy Markdown
Owner

Summary

On Linux (and other Unix platforms), when extracting as root, two
things were fully controlled by the archive's own content:

  1. A device-node entry (Devmajor/Devminor) was created through
    mknod with the major/minor the archive named. An archive could
    place a node inside the extraction directory naming a real system
    disk's device number.
  2. uid/gid from the Info-ZIP Unix extra field were applied through
    Lchown unconditionally, and a non-regular entry's mode (including
    the setuid/setgid bits) was applied through lchmod. An archive
    with uid=0 controlled both the owner and the mode of what it
    created.

Both only take effect when the caller runs with the corresponding
privilege (CAP_MKNOD / the right to chown to an arbitrary owner),
so neither had any effect for an ordinary user -- the problem shows up
only when extracting as root, which is unusual for a file manager but
not impossible.

  • Add WithExtractorDeviceNodes (off by default): with it off, a
    block or character device entry is left unwritten rather than handed
    to mknod. Named pipes and sockets are unaffected -- making either
    needs no privilege an ordinary caller lacks, and neither aliases a
    device the kernel already has.
  • Add WithExtractorPreserveOwner (off by default): with it off,
    Lchown is never attempted for any entry, so an archive's uid/gid
    never take effect regardless of the extra field it carries.
  • Both propagate through to the extractor a solid archive's contents
    are unpacked with, the same way every other extractor option
    already does.

A few existing tests used the ownership pass as the one reliable way
to make updateFileMetadata fail on demand, or as a proxy for
"metadata landed on the right path" -- those now opt in explicitly
via WithExtractorPreserveOwner(true).

Test plan

  • go build ./... && go vet ./... && golangci-lint run ./...
  • go test ./... (full suite) and go test -race ./... on the
    affected tests
  • New TestExtractor_DeviceNodesOptIn: off by default nothing is
    created for a device entry; opted in, mknod is actually attempted
    (and, run as an ordinary user, refused, same as always).
  • New TestExtractor_PreserveOwnerOptIn: off by default the chown
    error handler never runs at all; opted in, it does.
  • Updated TestExtractSolid_DirectoryMetadataFailure,
    TestExtractSolid_FileMetadataFailure,
    TestExtractorCovStreamEntryMetadataFailure, and
    TestPUA_Zip_NestedUndecodableNameMetadata to opt in explicitly,
    since they rely on the ownership pass running.

Touch #6

Lunobot-3

🤖 Generated with Claude Code

Extracting as root, two things were fully controlled by the archive's
own content: mknod created a device node from whatever major/minor an
entry named, and Lchown gave a file to whatever uid/gid an entry's
Info-ZIP Unix extra field carried, mode (including setuid/setgid)
included. Both only do anything for a caller with the privilege to do
them, so an archive extracted as root could alias a path in the
destination to a real device the machine already has, or hand a file
away to any owner it chose. Neither had a way for the caller to say no.

Add WithExtractorDeviceNodes and WithExtractorPreserveOwner, both off
by default. With device nodes off, a block or character device entry
is left unwritten rather than handed to mknod; named pipes and sockets
are unaffected, since making either needs no privilege and aliases no
device the kernel already has. With ownership off, Lchown is never
attempted at all, so an entry's mode is still applied but its uid/gid
never take effect. Both propagate into the extractor a solid archive's
contents are unpacked through, the same way every other option does.

A few tests used the ownership pass as the one way to make
updateFileMetadata fail on demand (an ordinary user is refused a
change of owner) or as a proxy for "metadata landed on the right
path" -- these now opt in explicitly with WithExtractorPreserveOwner.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@unxed
unxed merged commit 3eb390e into main Sep 27, 2026
36 of 38 checks passed
@unxed
unxed deleted the lunobot/agent-lb3-zip56/lunobot-3/6-extractor-root-privileged-opt-in branch September 27, 2026 04:50
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant