mirror of
https://github.com/zap-proto/zip.git
synced 2026-10-19 15:38:57 +00:00
A plugin host is where a service becomes an arbitrary-code-execution vector:
whatever it starts runs with the host's authority — verifying blocks, signing,
serving. Sum existed but only gated URL, so the case that looked dangerous was
checked and the case that is was not. A binary read off disk is not more
trustworthy than a downloaded one; it is the same bytes with the download
already done by somebody else.
Now every source takes the same path, and the check is worth something because
what runs is what was checked:
- The binary is COPIED into a directory only the host can write, and the copy
is hashed and executed. Verifying in place and then running leaves whoever
can write that file free to change it afterwards, and starting a plugin is
an observable event, so they get to choose when.
- Holding a descriptor to the original would not have closed that, which the
test proves rather than assumes: a rename swaps the inode, but a WRITE keeps
it, and a write is visible through an already-open descriptor. Only the copy
puts the bytes out of reach.
- The copy is executed through its own descriptor (/proc/self/fd on Linux), so
no path is resolved a second time. It is reopened read-only first — Linux
refuses to execute a file anyone holds open for writing.
- A mismatch refuses. A host that logs and carries on has performed the check
and kept the risk.
What this cannot decide is where a trustworthy Sum comes from. A digest in the
same config an attacker just edited proves nothing, so the anchor has to be
something they do not control. For a network that already agrees on things by
construction, that is consensus.
26 lines
876 B
Go
26 lines
876 B
Go
//go:build linux
|
|
|
|
package zip
|
|
|
|
import (
|
|
"os"
|
|
"os/exec"
|
|
)
|
|
|
|
// exec points cmd at the ALREADY-VERIFIED descriptor rather than at a path.
|
|
//
|
|
// Linux resolves /proc/self/fd/N in the child between fork and execve, so the
|
|
// image that runs is the one whose bytes were hashed — not whatever the path
|
|
// names by then. Verifying a path and then executing that path is a check an
|
|
// attacker can step around by replacing the file in between, and they get to
|
|
// choose when, because starting a plugin is an observable event.
|
|
//
|
|
// argv[0] stays the plugin's name: the descriptor is how the image is found,
|
|
// not what the program should believe it is called.
|
|
func (b *binary) exec(args []string) *exec.Cmd {
|
|
cmd := exec.Command("/proc/self/fd/3", args...)
|
|
cmd.Args = append([]string{b.name}, args...)
|
|
cmd.ExtraFiles = []*os.File{b.f} // becomes fd 3 in the child
|
|
return cmd
|
|
}
|