1
0
mirror of https://github.com/golang/go synced 2024-11-17 19:44:43 -07:00

archive/tar: check for permissible output formats first

The current logic in writeHeader attempts to encode the Header in one
format and if it discovered that it could not it would attempt to
switch to a different format mid-way through. This makes it very
hard to reason about what format will be used in the end and whether
it will even be a valid format.

Instead, we should verify from the start what formats are allowed
to encode the given input Header. If no formats are possible,
then we can return immediately, rejecting the Header.

For now, we continue on to the hairy logic in writeHeader, but
a future CL can split that logic up and specialize them for each
format now that we know what is possible.

Update #9683
Update #12594

Change-Id: I8406ea855dfcb8b478a03a7058ddf8b2b09d46dc
Reviewed-on: https://go-review.googlesource.com/54433
Run-TryBot: Joe Tsai <thebrokentoaster@gmail.com>
Reviewed-by: Ian Lance Taylor <iant@golang.org>
This commit is contained in:
Joe Tsai 2017-08-09 13:12:50 -07:00 committed by Joe Tsai
parent 49ab0dba56
commit ead6255ce3
3 changed files with 245 additions and 1 deletions

View File

@ -17,6 +17,7 @@ import (
"fmt"
"os"
"path"
"strconv"
"time"
)
@ -67,6 +68,106 @@ func (h *Header) FileInfo() os.FileInfo {
return headerFileInfo{h}
}
// allowedFormats determines which formats can be used. The value returned
// is the logical OR of multiple possible formats. If the value is
// formatUnknown, then the input Header cannot be encoded.
//
// As a by-product of checking the fields, this function returns paxHdrs, which
// contain all fields that could not be directly encoded.
func (h *Header) allowedFormats() (format int, paxHdrs map[string]string) {
format = formatUSTAR | formatPAX | formatGNU
paxHdrs = make(map[string]string)
verifyString := func(s string, size int, gnuLong bool, paxKey string) {
// NUL-terminator is optional for path and linkpath.
// Technically, it is required for uname and gname,
// but neither GNU nor BSD tar checks for it.
tooLong := len(s) > size
if !isASCII(s) || (tooLong && !gnuLong) {
// TODO(dsnet): GNU supports UTF-8 (without NUL) for strings.
format &^= formatGNU // No GNU
}
if !isASCII(s) || tooLong {
// TODO(dsnet): If the path is splittable, it is possible to still
// use the USTAR format.
format &^= formatUSTAR // No USTAR
if paxKey == paxNone {
format &^= formatPAX // No PAX
} else {
paxHdrs[paxKey] = s
}
}
}
verifyNumeric := func(n int64, size int, paxKey string) {
if !fitsInBase256(size, n) {
format &^= formatGNU // No GNU
}
if !fitsInOctal(size, n) {
format &^= formatUSTAR // No USTAR
if paxKey == paxNone {
format &^= formatPAX // No PAX
} else {
paxHdrs[paxKey] = strconv.FormatInt(n, 10)
}
}
}
verifyTime := func(ts time.Time, size int, ustarField bool, paxKey string) {
if ts.IsZero() {
return // Always okay
}
needsNano := ts.Nanosecond() != 0
if !fitsInBase256(size, ts.Unix()) || needsNano {
format &^= formatGNU // No GNU
}
if !fitsInOctal(size, ts.Unix()) || needsNano || !ustarField {
format &^= formatUSTAR // No USTAR
if paxKey == paxNone {
format &^= formatPAX // No PAX
} else {
// TODO(dsnet): Support PAX time here.
// paxHdrs[paxKey] = formatPAXTime(ts)
}
}
}
// TODO(dsnet): Add GNU long name support.
const supportGNULong = false
var blk block
var v7 = blk.V7()
var ustar = blk.USTAR()
verifyString(h.Name, len(v7.Name()), supportGNULong, paxPath)
verifyString(h.Linkname, len(v7.LinkName()), supportGNULong, paxLinkpath)
verifyString(h.Uname, len(ustar.UserName()), false, paxUname)
verifyString(h.Gname, len(ustar.GroupName()), false, paxGname)
verifyNumeric(h.Mode, len(v7.Mode()), paxNone)
verifyNumeric(int64(h.Uid), len(v7.UID()), paxUid)
verifyNumeric(int64(h.Gid), len(v7.GID()), paxGid)
verifyNumeric(h.Size, len(v7.Size()), paxSize)
verifyNumeric(h.Devmajor, len(ustar.DevMajor()), paxNone)
verifyNumeric(h.Devminor, len(ustar.DevMinor()), paxNone)
verifyTime(h.ModTime, len(v7.ModTime()), true, paxMtime)
// TODO(dsnet): Support atime and ctime fields.
// verifyTime(h.AccessTime, len(gnu.AccessTime()), false, paxAtime)
// verifyTime(h.ChangeTime, len(gnu.ChangeTime()), false, paxCtime)
if !isHeaderOnlyType(h.Typeflag) && h.Size < 0 {
return formatUnknown, nil
}
if len(h.Xattrs) > 0 {
for k, v := range h.Xattrs {
paxHdrs[paxXattr+k] = v
}
format &= formatPAX // PAX only
}
for k, v := range paxHdrs {
if !validPAXRecord(k, v) {
return formatUnknown, nil // Invalid PAX key
}
}
return format, paxHdrs
}
// headerFileInfo implements os.FileInfo.
type headerFileInfo struct {
h *Header

View File

@ -8,6 +8,7 @@ import (
"bytes"
"internal/testenv"
"io/ioutil"
"math"
"os"
"path"
"path/filepath"
@ -329,3 +330,129 @@ func TestHeaderRoundTrip(t *testing.T) {
}
}
}
func TestHeaderAllowedFormats(t *testing.T) {
prettyFormat := func(f int) string {
if f == formatUnknown {
return "(formatUnknown)"
}
var fs []string
if f&formatUSTAR > 0 {
fs = append(fs, "formatUSTAR")
}
if f&formatPAX > 0 {
fs = append(fs, "formatPAX")
}
if f&formatGNU > 0 {
fs = append(fs, "formatGNU")
}
return "(" + strings.Join(fs, " | ") + ")"
}
vectors := []struct {
header *Header // Input header
paxHdrs map[string]string // Expected PAX headers that may be needed
formats int // Expected formats that can encode the header
}{{
header: &Header{},
formats: formatUSTAR | formatPAX | formatGNU,
}, {
header: &Header{Size: 077777777777},
formats: formatUSTAR | formatPAX | formatGNU,
}, {
header: &Header{Size: 077777777777 + 1},
paxHdrs: map[string]string{paxSize: "8589934592"},
formats: formatPAX | formatGNU,
}, {
header: &Header{Mode: 07777777},
formats: formatUSTAR | formatPAX | formatGNU,
}, {
header: &Header{Mode: 07777777 + 1},
formats: formatGNU,
}, {
header: &Header{Devmajor: -123},
formats: formatGNU,
}, {
header: &Header{Devmajor: 1<<56 - 1},
formats: formatGNU,
}, {
header: &Header{Devmajor: 1 << 56},
formats: formatUnknown,
}, {
header: &Header{Devmajor: -1 << 56},
formats: formatGNU,
}, {
header: &Header{Devmajor: -1<<56 - 1},
formats: formatUnknown,
}, {
header: &Header{Name: "用戶名", Devmajor: -1 << 56},
formats: formatUnknown,
}, {
header: &Header{Size: math.MaxInt64},
paxHdrs: map[string]string{paxSize: "9223372036854775807"},
formats: formatPAX | formatGNU,
}, {
header: &Header{Size: math.MinInt64},
paxHdrs: map[string]string{paxSize: "-9223372036854775808"},
formats: formatUnknown,
}, {
header: &Header{Uname: "0123456789abcdef0123456789abcdef"},
formats: formatUSTAR | formatPAX | formatGNU,
}, {
header: &Header{Uname: "0123456789abcdef0123456789abcdefx"},
paxHdrs: map[string]string{paxUname: "0123456789abcdef0123456789abcdefx"},
formats: formatPAX,
}, {
header: &Header{Name: "foobar"},
formats: formatUSTAR | formatPAX | formatGNU,
}, {
header: &Header{Name: strings.Repeat("a", nameSize)},
formats: formatUSTAR | formatPAX | formatGNU,
}, {
header: &Header{Linkname: "用戶名"},
paxHdrs: map[string]string{paxLinkpath: "用戶名"},
formats: formatPAX,
}, {
header: &Header{Linkname: strings.Repeat("用戶名\x00", nameSize)},
paxHdrs: map[string]string{paxLinkpath: strings.Repeat("用戶名\x00", nameSize)},
formats: formatUnknown,
}, {
header: &Header{Linkname: "\x00hello"},
paxHdrs: map[string]string{paxLinkpath: "\x00hello"},
formats: formatUnknown,
}, {
header: &Header{Uid: 07777777},
formats: formatUSTAR | formatPAX | formatGNU,
}, {
header: &Header{Uid: 07777777 + 1},
paxHdrs: map[string]string{paxUid: "2097152"},
formats: formatPAX | formatGNU,
}, {
header: &Header{Xattrs: nil},
formats: formatUSTAR | formatPAX | formatGNU,
}, {
header: &Header{Xattrs: map[string]string{"foo": "bar"}},
paxHdrs: map[string]string{paxXattr + "foo": "bar"},
formats: formatPAX,
}, {
header: &Header{Xattrs: map[string]string{"用戶名": "\x00hello"}},
paxHdrs: map[string]string{paxXattr + "用戶名": "\x00hello"},
formats: formatPAX,
}, {
header: &Header{ModTime: time.Unix(0, 0)},
formats: formatUSTAR | formatPAX | formatGNU,
}, {
header: &Header{ModTime: time.Unix(077777777777, 0)},
formats: formatUSTAR | formatPAX | formatGNU,
}}
for i, v := range vectors {
formats, paxHdrs := v.header.allowedFormats()
if formats != v.formats {
t.Errorf("test %d, allowedFormats(...): got %v, want %v", i, prettyFormat(formats), prettyFormat(v.formats))
}
if formats&formatPAX > 0 && !reflect.DeepEqual(paxHdrs, v.paxHdrs) && !(len(paxHdrs) == 0 && len(v.paxHdrs) == 0) {
t.Errorf("test %d, allowedFormats(...):\ngot %v\nwant %s", i, paxHdrs, v.paxHdrs)
}
}
}

View File

@ -77,7 +77,23 @@ var (
// WriteHeader calls Flush if it is not the first header.
// Calling after a Close will return ErrWriteAfterClose.
func (tw *Writer) WriteHeader(hdr *Header) error {
return tw.writeHeader(hdr, true)
// TODO(dsnet): Add PAX timestamps with nanosecond support.
hdrCpy := *hdr
hdrCpy.ModTime = hdrCpy.ModTime.Truncate(time.Second)
switch allowedFormats, _ := hdrCpy.allowedFormats(); {
case allowedFormats&formatUSTAR > 0:
// TODO(dsnet): Implement and call specialized writeUSTARHeader.
return tw.writeHeader(&hdrCpy, true)
case allowedFormats&formatPAX > 0:
// TODO(dsnet): Implement and call specialized writePAXHeader.
return tw.writeHeader(&hdrCpy, true)
case allowedFormats&formatGNU > 0:
// TODO(dsnet): Implement and call specialized writeGNUHeader.
return tw.writeHeader(&hdrCpy, true)
default:
return ErrHeader
}
}
// WriteHeader writes hdr and prepares to accept the file's contents.