package updater import ( "archive/tar" "compress/gzip" "fmt" "io" "os" "path/filepath" "strings" ) // expectedReleaseEntries is the canonical set of files a release // tarball must contain. Anything missing OR anything extra is // rejected — banger update should not unpack arbitrary files into // the staging dir. var expectedReleaseEntries = []string{ "banger", "bangerd", "banger-vsock-agent", } // StagedRelease describes the result of unpacking a release tarball // into a staging directory. type StagedRelease struct { BangerPath string BangerdPath string VsockAgentPath string } // StageTarball reads the gzipped tar at tarballPath and extracts the // expected three banger binaries into stagingDir. Any extra entries, // any path-traversal members, any non-regular-file members, and any // missing required entry are rejected. // // The extracted binaries are mode 0o755 regardless of what the // tarball claims — banger update is a privileged operation; we // don't honour weird modes from the wire. func StageTarball(tarballPath, stagingDir string) (StagedRelease, error) { if err := os.MkdirAll(stagingDir, 0o700); err != nil { return StagedRelease{}, err } f, err := os.Open(tarballPath) if err != nil { return StagedRelease{}, err } defer f.Close() gz, err := gzip.NewReader(f) if err != nil { return StagedRelease{}, fmt.Errorf("open gzip: %w", err) } defer gz.Close() expected := map[string]struct{}{} for _, name := range expectedReleaseEntries { expected[name] = struct{}{} } seen := map[string]string{} tr := tar.NewReader(gz) for { hdr, err := tr.Next() if err == io.EOF { break } if err != nil { return StagedRelease{}, fmt.Errorf("read tar: %w", err) } rel := filepath.Clean(hdr.Name) if rel == "." || rel == string(filepath.Separator) { continue } if filepath.IsAbs(rel) || rel == ".." || strings.HasPrefix(rel, ".."+string(filepath.Separator)) { return StagedRelease{}, fmt.Errorf("unsafe path in tarball: %q", hdr.Name) } if _, ok := expected[rel]; !ok { return StagedRelease{}, fmt.Errorf("unexpected entry in release tarball: %q (allowed: %v)", hdr.Name, expectedReleaseEntries) } if hdr.Typeflag != tar.TypeReg { return StagedRelease{}, fmt.Errorf("entry %q is not a regular file (typeflag %d)", hdr.Name, hdr.Typeflag) } dst := filepath.Join(stagingDir, rel) out, err := os.OpenFile(dst, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0o755) if err != nil { return StagedRelease{}, err } if _, err := io.Copy(out, tr); err != nil { _ = out.Close() return StagedRelease{}, err } if err := out.Close(); err != nil { return StagedRelease{}, err } seen[rel] = dst } for _, want := range expectedReleaseEntries { if _, ok := seen[want]; !ok { return StagedRelease{}, fmt.Errorf("release tarball is missing required entry %q", want) } } return StagedRelease{ BangerPath: seen["banger"], BangerdPath: seen["bangerd"], VsockAgentPath: seen["banger-vsock-agent"], }, nil }