mirror of
https://github.com/anchore/syft.git
synced 2026-11-04 08:26:38 +01:00
test(golang): make the UPX fixture exercise loader padding (#5354)
UPX pads to a 4 byte boundary before it writes the loader stub. The image-small-upx build happened to need no padding, so the cataloger tests over it passed with or without the fix in #5347. - build the fixture with -X main.Version=1.0.11, which needs 2 bytes of padding, so the existing cataloger tests fail without the fix - add TestImageSmallUPXNeedsLoaderPadding, which fails if a change to the fixture's inputs means it no longer needs padding - run the crafted loader test on both sides of the boundary, so a skip that always rounds up is caught too Signed-off-by: Will Murphy <willmurphyscode@users.noreply.github.com>
This commit is contained in:
@@ -59,9 +59,10 @@ func Test_PackageCataloger_Binary(t *testing.T) {
|
|||||||
{
|
{
|
||||||
name: "upx compressed binary",
|
name: "upx compressed binary",
|
||||||
fixture: "image-small-upx",
|
fixture: "image-small-upx",
|
||||||
|
// v1.0.11, not v1.0.0: see the fixture's Dockerfile
|
||||||
wantErr: require.NoError,
|
wantErr: require.NoError,
|
||||||
expectedPkgs: []string{
|
expectedPkgs: []string{
|
||||||
"anchore.io/not/real @ v1.0.0 (/run-me)",
|
"anchore.io/not/real @ v1.0.11 (/run-me)",
|
||||||
"github.com/andybalholm/brotli @ v1.1.1 (/run-me)",
|
"github.com/andybalholm/brotli @ v1.1.1 (/run-me)",
|
||||||
"github.com/dsnet/compress @ v0.0.2-0.20210315054119-f66993602bf5 (/run-me)",
|
"github.com/dsnet/compress @ v0.0.2-0.20210315054119-f66993602bf5 (/run-me)",
|
||||||
"github.com/golang/snappy @ v0.0.4 (/run-me)",
|
"github.com/golang/snappy @ v0.0.4 (/run-me)",
|
||||||
@@ -75,17 +76,17 @@ func Test_PackageCataloger_Binary(t *testing.T) {
|
|||||||
"github.com/anchore/archiver/v3 @ v3.5.3-0.20241210171143-5b1d8d1c7c51 (/run-me)",
|
"github.com/anchore/archiver/v3 @ v3.5.3-0.20241210171143-5b1d8d1c7c51 (/run-me)",
|
||||||
},
|
},
|
||||||
expectedRels: []string{
|
expectedRels: []string{
|
||||||
"github.com/andybalholm/brotli @ v1.1.1 (/run-me) [dependency-of] anchore.io/not/real @ v1.0.0 (/run-me)",
|
"github.com/andybalholm/brotli @ v1.1.1 (/run-me) [dependency-of] anchore.io/not/real @ v1.0.11 (/run-me)",
|
||||||
"github.com/dsnet/compress @ v0.0.2-0.20210315054119-f66993602bf5 (/run-me) [dependency-of] anchore.io/not/real @ v1.0.0 (/run-me)",
|
"github.com/dsnet/compress @ v0.0.2-0.20210315054119-f66993602bf5 (/run-me) [dependency-of] anchore.io/not/real @ v1.0.11 (/run-me)",
|
||||||
"github.com/golang/snappy @ v0.0.4 (/run-me) [dependency-of] anchore.io/not/real @ v1.0.0 (/run-me)",
|
"github.com/golang/snappy @ v0.0.4 (/run-me) [dependency-of] anchore.io/not/real @ v1.0.11 (/run-me)",
|
||||||
"github.com/klauspost/compress @ v1.17.11 (/run-me) [dependency-of] anchore.io/not/real @ v1.0.0 (/run-me)",
|
"github.com/klauspost/compress @ v1.17.11 (/run-me) [dependency-of] anchore.io/not/real @ v1.0.11 (/run-me)",
|
||||||
"github.com/klauspost/pgzip @ v1.2.6 (/run-me) [dependency-of] anchore.io/not/real @ v1.0.0 (/run-me)",
|
"github.com/klauspost/pgzip @ v1.2.6 (/run-me) [dependency-of] anchore.io/not/real @ v1.0.11 (/run-me)",
|
||||||
"github.com/anchore/archiver/v3 @ v3.5.3-0.20241210171143-5b1d8d1c7c51 (/run-me) [dependency-of] anchore.io/not/real @ v1.0.0 (/run-me)",
|
"github.com/anchore/archiver/v3 @ v3.5.3-0.20241210171143-5b1d8d1c7c51 (/run-me) [dependency-of] anchore.io/not/real @ v1.0.11 (/run-me)",
|
||||||
"github.com/nwaples/rardecode @ v1.1.3 (/run-me) [dependency-of] anchore.io/not/real @ v1.0.0 (/run-me)",
|
"github.com/nwaples/rardecode @ v1.1.3 (/run-me) [dependency-of] anchore.io/not/real @ v1.0.11 (/run-me)",
|
||||||
"github.com/pierrec/lz4/v4 @ v4.1.21 (/run-me) [dependency-of] anchore.io/not/real @ v1.0.0 (/run-me)",
|
"github.com/pierrec/lz4/v4 @ v4.1.21 (/run-me) [dependency-of] anchore.io/not/real @ v1.0.11 (/run-me)",
|
||||||
"github.com/ulikunitz/xz @ v0.5.12 (/run-me) [dependency-of] anchore.io/not/real @ v1.0.0 (/run-me)",
|
"github.com/ulikunitz/xz @ v0.5.12 (/run-me) [dependency-of] anchore.io/not/real @ v1.0.11 (/run-me)",
|
||||||
"github.com/xi2/xz @ v0.0.0-20171230120015-48954b6210f8 (/run-me) [dependency-of] anchore.io/not/real @ v1.0.0 (/run-me)",
|
"github.com/xi2/xz @ v0.0.0-20171230120015-48954b6210f8 (/run-me) [dependency-of] anchore.io/not/real @ v1.0.11 (/run-me)",
|
||||||
"stdlib @ go1.23.2 (/run-me) [dependency-of] anchore.io/not/real @ v1.0.0 (/run-me)",
|
"stdlib @ go1.23.2 (/run-me) [dependency-of] anchore.io/not/real @ v1.0.11 (/run-me)",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -9,7 +9,9 @@ COPY go.mod go.sum ./
|
|||||||
RUN go mod download
|
RUN go mod download
|
||||||
COPY main.go main.go
|
COPY main.go main.go
|
||||||
|
|
||||||
RUN CGO_ENABLED=0 GOOS=linux go build -ldflags "-X main.Version=1.0.0" -o run-me .
|
# 1.0.11 is chosen so UPX has to pad before its loader stub (1.0.0 needed none, which hid a bug).
|
||||||
|
# TestImageSmallUPXNeedsLoaderPadding checks the padding is still there.
|
||||||
|
RUN CGO_ENABLED=0 GOOS=linux go build -ldflags "-X main.Version=1.0.11" -o run-me .
|
||||||
RUN upx --best --lzma --exact run-me
|
RUN upx --best --lzma --exact run-me
|
||||||
|
|
||||||
FROM scratch
|
FROM scratch
|
||||||
|
|||||||
@@ -718,33 +718,57 @@ func TestDecompressUPX_TailExtentsArePlacedPastTheLoader(t *testing.T) {
|
|||||||
// front of it. Every other crafted fixture here has l_lsize == 0, so without this skipLoader was only
|
// front of it. Every other crafted fixture here has l_lsize == 0, so without this skipLoader was only
|
||||||
// reachable through the Docker-backed fixture.
|
// reachable through the Docker-backed fixture.
|
||||||
//
|
//
|
||||||
// UPX pads to a 4 byte boundary before it writes the stub and l_lsize counts from there, so the head
|
// UPX pads to a 4 byte boundary before it writes the stub and l_lsize counts from there, so each case
|
||||||
// extent here deliberately ends off that boundary: its end plus l_lsize lands inside the stub.
|
// ends the head extent the given number of bytes short of a boundary. The head is a stored block so its
|
||||||
|
// length alone decides where it ends; a compressed one ends wherever the encoder happens to stop.
|
||||||
//
|
//
|
||||||
// note: block 1 here is not an ELF, so there are no PT_LOAD offsets and the tail lands sequentially.
|
// note: block 1 here is not an ELF, so there are no PT_LOAD offsets and the tail lands sequentially.
|
||||||
// The hole-filling path this feeds in production is covered by TestFirstHole and by the sparse
|
// The hole-filling path this feeds in production is covered by TestFirstHole and by the sparse
|
||||||
// fixtures in upx_bounds_regression_test.go.
|
// fixtures in upx_bounds_regression_test.go.
|
||||||
const loaderSize = 64
|
const loaderSize = 64
|
||||||
head := bytes.Repeat([]byte("H"), 128)
|
|
||||||
tail := bytes.Repeat([]byte("T"), 32)
|
tail := bytes.Repeat([]byte("T"), 32)
|
||||||
|
|
||||||
data := buildUPXHeaderWithLoader(4096, 4096, loaderSize)
|
tests := []struct {
|
||||||
data = append(data, blockFor(t, head)...)
|
name string
|
||||||
require.NotZero(t, len(data)%4, "the head extent has to end off a 4 byte boundary for the padding to exist")
|
padding int
|
||||||
data = append(data, make([]byte, 4-len(data)%4)...) // the padding UPX writes in front of the stub
|
}{
|
||||||
data = append(data, make([]byte, 12)...) // end marker: closes the first run of extents
|
{name: "no padding", padding: 0},
|
||||||
data = append(data, make([]byte, loaderSize-12)...) // the loader stub the chain skips over
|
{name: "1 byte of padding", padding: 1},
|
||||||
data = append(data, blockFor(t, tail)...)
|
{name: "2 bytes of padding", padding: 2},
|
||||||
data = append(data, make([]byte, 12)...) // end marker for the tail run
|
{name: "3 bytes of padding", padding: 3},
|
||||||
data = padTo(data, 1024)
|
}
|
||||||
|
|
||||||
out, err := unpack(t, data)
|
for _, tt := range tests {
|
||||||
// 160 bytes of a declared 4096, short by construction like every other crafted fixture here
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
require.ErrorIs(t, err, errUPXPartial)
|
data := buildUPXHeaderWithLoader(4096, 4096, loaderSize)
|
||||||
got := readAll(t, out)
|
|
||||||
|
|
||||||
assert.Equal(t, head, got[:len(head)], "the head extent stays where it was placed")
|
// trim a 128 byte head so that it, plus its b_info, ends tt.padding bytes short of a boundary
|
||||||
assert.Contains(t, string(got), string(tail), "the extent behind the loader must be placed too")
|
headLen := 128 - (len(data)+12+128+tt.padding)%4
|
||||||
|
head := bytes.Repeat([]byte("H"), headLen)
|
||||||
|
stored := make([]byte, 12)
|
||||||
|
binary.LittleEndian.PutUint32(stored[0:4], uint32(headLen)) // sz_unc
|
||||||
|
binary.LittleEndian.PutUint32(stored[4:8], uint32(headLen)) // sz_cpr, equal for a stored block
|
||||||
|
stored[8] = upxMethodStored
|
||||||
|
|
||||||
|
data = append(data, stored...)
|
||||||
|
data = append(data, head...)
|
||||||
|
require.Equal(t, tt.padding, (4-len(data)%4)%4, "the head should end tt.padding bytes short of a boundary")
|
||||||
|
data = append(data, make([]byte, tt.padding)...) // the padding UPX writes in front of the stub
|
||||||
|
data = append(data, make([]byte, 12)...) // end marker: closes the first run of extents
|
||||||
|
data = append(data, make([]byte, loaderSize-12)...) // the loader stub the chain skips over
|
||||||
|
data = append(data, blockFor(t, tail)...)
|
||||||
|
data = append(data, make([]byte, 12)...) // end marker for the tail run
|
||||||
|
data = padTo(data, 1024)
|
||||||
|
|
||||||
|
out, err := unpack(t, data)
|
||||||
|
// a few hundred bytes of a declared 4096, short by construction like every other crafted fixture here
|
||||||
|
require.ErrorIs(t, err, errUPXPartial)
|
||||||
|
got := readAll(t, out)
|
||||||
|
|
||||||
|
assert.Equal(t, head, got[:len(head)], "the head extent stays where it was placed")
|
||||||
|
assert.Contains(t, string(got), string(tail), "the extent behind the loader must be placed too")
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestDecompressUPX_UnsupportedMethodIsNotReportable(t *testing.T) {
|
func TestDecompressUPX_UnsupportedMethodIsNotReportable(t *testing.T) {
|
||||||
|
|||||||
@@ -2,10 +2,14 @@ package golang
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"io"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
|
stereoscopeFile "github.com/anchore/stereoscope/pkg/file"
|
||||||
|
"github.com/anchore/stereoscope/pkg/imagetest"
|
||||||
)
|
)
|
||||||
|
|
||||||
// every case here is one the magic scan must reject, asserted on parseUPXInfo's errNotUPX result.
|
// every case here is one the magic scan must reject, asserted on parseUPXInfo's errNotUPX result.
|
||||||
@@ -104,3 +108,33 @@ func TestParseUPXInfo_ValidHeader(t *testing.T) {
|
|||||||
assert.Equal(t, uint8(22), info.format)
|
assert.Equal(t, uint8(22), info.format)
|
||||||
assert.Equal(t, uint32(0x100000), info.originalSize)
|
assert.Equal(t, uint32(0x100000), info.originalSize)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestImageSmallUPXNeedsLoaderPadding checks the fixture rather than the code. UPX pads to a 4 byte
|
||||||
|
// boundary before its loader stub, and the cataloger tests over image-small-upx only catch an unpacker that
|
||||||
|
// ignores that padding if there is some. A build that needs none passes either way, which is how the bug
|
||||||
|
// went unnoticed. The case without padding is covered by TestDecompressUPX_TailExtentsArePlacedPastTheLoader.
|
||||||
|
func TestImageSmallUPXNeedsLoaderPadding(t *testing.T) {
|
||||||
|
img := imagetest.GetFixtureImage(t, "docker-archive", "image-small-upx")
|
||||||
|
rc, err := img.OpenPathFromSquash(stereoscopeFile.Path("/run-me"))
|
||||||
|
require.NoError(t, err)
|
||||||
|
data, err := io.ReadAll(rc)
|
||||||
|
require.NoError(t, rc.Close())
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
r := bytes.NewReader(data)
|
||||||
|
info, err := parseUPXInfo(r, int64(len(data)))
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NotZero(t, info.loaderSize, "real UPX output carries a loader stub")
|
||||||
|
|
||||||
|
// walk the first run of extents to the end marker in front of the stub
|
||||||
|
extentsEnd := info.firstBlockOff
|
||||||
|
for blockNum := 0; ; blockNum++ {
|
||||||
|
block, _, err := readChainBlock(r, extentsEnd, blockNum, info.inputLen)
|
||||||
|
require.NoError(t, err)
|
||||||
|
if block == nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
extentsEnd = block.dataOffset + int64(block.compressedSize)
|
||||||
|
}
|
||||||
|
require.NotZero(t, extentsEnd%4, "the fixture no longer needs padding: change the version string in its Dockerfile")
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user