Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

extract dep info from wasm modules #6

Merged
merged 1 commit into from
Aug 20, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,4 @@
# vendor/

crate_with_features_bin
wasm_crate.wasm
111 changes: 107 additions & 4 deletions rustaudit.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"debug/elf"
"debug/macho"
"debug/pe"
"encoding/binary"
"encoding/json"
"errors"
"fmt"
Expand Down Expand Up @@ -60,6 +61,11 @@ var (
machoHeader = []byte("\xFE\xED\xFA")
machoHeaderLittleEndian = []byte("\xFA\xED\xFE")
machoUniversalHeader = []byte("\xCA\xFE\xBA\xBE")
// https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#binary-magic
wasmHeader = []byte("\x00asm\x01\x00\x00\x00")

cargoAuditableSectionName = ".dep-v0"
cargoAuditableLegacySectionName = ".rust-deps-v0"
)

func GetDependencyInfo(r io.ReaderAt) (VersionInfo, error) {
Expand Down Expand Up @@ -90,6 +96,8 @@ func GetDependencyInfo(r io.ReaderAt) (VersionInfo, error) {
return VersionInfo{}, ErrUnknownFileFormat
}
x = &machoExe{f}
case bytes.HasPrefix(header, wasmHeader):
x = &wasmReader{r}
default:
return VersionInfo{}, ErrUnknownFileFormat
}
Expand Down Expand Up @@ -135,13 +143,13 @@ type elfExe struct {
func (x *elfExe) ReadRustDepSection() ([]byte, error) {
// Try .dep-v0 first, falling back to .rust-deps-v0 as used in
// in rust-audit 0.1.0
depInfo := x.f.Section(".dep-v0")
depInfo := x.f.Section(cargoAuditableSectionName)

if depInfo != nil {
return depInfo.Data()
}

depInfo = x.f.Section(".rust-deps-v0")
depInfo = x.f.Section(cargoAuditableLegacySectionName)

if depInfo == nil {
return nil, ErrNoRustDepInfo
Expand All @@ -157,7 +165,7 @@ type peExe struct {
func (x *peExe) ReadRustDepSection() ([]byte, error) {
// Try .dep-v0 first, falling back to rdep-v0 as used in
// in rust-audit 0.1.0
depInfo := x.f.Section(".dep-v0")
depInfo := x.f.Section(cargoAuditableSectionName)

if depInfo != nil {
return depInfo.Data()
Expand All @@ -179,7 +187,7 @@ type machoExe struct {
func (x *machoExe) ReadRustDepSection() ([]byte, error) {
// Try .dep-v0 first, falling back to rust-deps-v0 as used in
// in rust-audit 0.1.0
depInfo := x.f.Section(".dep-v0")
depInfo := x.f.Section(cargoAuditableSectionName)

if depInfo != nil {
return depInfo.Data()
Expand All @@ -193,3 +201,98 @@ func (x *machoExe) ReadRustDepSection() ([]byte, error) {

return depInfo.Data()
}

type wasmReader struct {
r io.ReaderAt
}

func (x *wasmReader) ReadRustDepSection() ([]byte, error) {
r := x.r
var offset int64 = 0

// Check the preamble (magic number and version)
buf := make([]byte, 8)
_, err := r.ReadAt(buf, offset)
offset += 8
if err != nil || !bytes.Equal(buf, wasmHeader) {
return nil, ErrUnknownFileFormat
}

// https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#custom-section%E2%91%A0
// Look through the sections until we find a custom .dep-v0 section or EOF
for {
// Read single byte section ID
sectionId := make([]byte, 1)
_, err = r.ReadAt(sectionId, offset)
offset += 1
if err == io.EOF {
return nil, ErrNoRustDepInfo
} else if err != nil {
return nil, ErrUnknownFileFormat
}

// Read section size
buf = make([]byte, 4)
_, err = r.ReadAt(buf, offset)
if err != nil {
return nil, ErrUnknownFileFormat
tofay marked this conversation as resolved.
Show resolved Hide resolved
}
sectionSize, n, err := readUint32(buf)
if err != nil {
return nil, ErrUnknownFileFormat
}
offset += n
nextSection := offset + int64(sectionSize)

// Custom sections have a zero section ID
if sectionId[0] != 0 {
offset = nextSection
continue
}

// The custom section has a variable length name
// followed by the data
_, err = r.ReadAt(buf, offset)
if err != nil {
return nil, ErrUnknownFileFormat
}
nameSize, n, err := readUint32(buf)
if err != nil {
return nil, ErrUnknownFileFormat
}
offset += n

// Read section name
name := make([]byte, nameSize)
_, err = r.ReadAt(name, offset)
if err != nil {
return nil, ErrUnknownFileFormat
}
offset += int64(nameSize)

// Is this our custom section?
if string(name) != cargoAuditableSectionName {
tofay marked this conversation as resolved.
Show resolved Hide resolved
offset = nextSection
continue
}

// Read audit data
data := make([]byte, nextSection-offset)
_, err = r.ReadAt(data, offset)
if err != nil {
return nil, ErrUnknownFileFormat
}
return data, nil

}
}

// wrap binary.Uvarint to return uint32, checking for overflow
// https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#integers%E2%91%A4
func readUint32(buf []byte) (uint32, int64, error) {
v, n := binary.Uvarint(buf)
if n <= 0 || v > uint64(^uint32(0)) {
return 0, 0, fmt.Errorf("overflow decoding uint32")
}
return uint32(v), int64(n), nil
}
34 changes: 34 additions & 0 deletions rustaudit_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package rustaudit

import (
"bytes"
"log"
"os"
"testing"
Expand All @@ -22,3 +23,36 @@ func TestLinuxRustDependencies(t *testing.T) {
assert.Equal(t, Package{Name: "crate_with_features", Version: "0.1.0", Source: "local", Kind: "runtime", Dependencies: []uint{1}, Root: true}, versionInfo.Packages[0])
assert.Equal(t, false, versionInfo.Packages[1].Root)
}

func TestWasmRustDependencies(t *testing.T) {
// Generate this with `DOCKER_BUILDKIT=1 docker build -f test/Dockerfile -o . .`
r, err := os.Open("wasm_crate.wasm")
if err != nil {
log.Fatal(err)
}
versionInfo, err := GetDependencyInfo(r)
if err != nil {
log.Fatal(err)
}
assert.Equal(t, 18, len(versionInfo.Packages))
assert.Equal(t, Package{Name: "bumpalo", Version: "3.16.0", Source: "crates.io", Kind: "runtime", Dependencies: nil, Root: false}, versionInfo.Packages[0])
assert.Equal(t, false, versionInfo.Packages[1].Root)
}

func FuzzWasm(f *testing.F) {
// Use the test fixture as a seed
data, err := os.ReadFile("wasm_crate.wasm")
if err != nil {
log.Fatal(err)
}
f.Add(data)
f.Fuzz(func(t *testing.T, input []byte) {
r := bytes.NewReader(input)
w := wasmReader{r}

_, err := w.ReadRustDepSection()
if err != ErrNoRustDepInfo && err != ErrUnknownFileFormat && err != nil {
t.Errorf("Unexpected error: %v", err)
}
})
}
8 changes: 6 additions & 2 deletions test/Dockerfile
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
FROM rust:1.62.1-bullseye as builder
FROM rust:1.79.0-bullseye as builder
RUN git clone https://github.com/rust-secure-code/cargo-auditable.git
WORKDIR /cargo-auditable/cargo-auditable
RUN cargo build
RUN rustup target add wasm32-unknown-unknown
WORKDIR /cargo-auditable/cargo-auditable/tests/fixtures/wasm_crate
RUN /cargo-auditable/target/debug/cargo-auditable auditable build --target wasm32-unknown-unknown
WORKDIR /cargo-auditable/cargo-auditable/tests/fixtures/workspace
RUN /cargo-auditable/target/debug/cargo-auditable auditable build
FROM scratch
COPY --from=builder \
/cargo-auditable/cargo-auditable/tests/fixtures/wasm_crate/target/wasm32-unknown-unknown/debug/wasm_crate.wasm \
/cargo-auditable/cargo-auditable/tests/fixtures/workspace/target/debug/crate_with_features_bin \
/crate_with_features_bin
/
Loading