Skip to content

Commit 0e2abec

Browse files
author
Tom Fay
authored
extract dep info from wasm modules (#6)
1 parent 204dfee commit 0e2abec

File tree

4 files changed

+148
-6
lines changed

4 files changed

+148
-6
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,3 +15,4 @@
1515
# vendor/
1616

1717
crate_with_features_bin
18+
wasm_crate.wasm

rustaudit.go

Lines changed: 107 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66
"debug/elf"
77
"debug/macho"
88
"debug/pe"
9+
"encoding/binary"
910
"encoding/json"
1011
"errors"
1112
"fmt"
@@ -60,6 +61,11 @@ var (
6061
machoHeader = []byte("\xFE\xED\xFA")
6162
machoHeaderLittleEndian = []byte("\xFA\xED\xFE")
6263
machoUniversalHeader = []byte("\xCA\xFE\xBA\xBE")
64+
// https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#binary-magic
65+
wasmHeader = []byte("\x00asm\x01\x00\x00\x00")
66+
67+
cargoAuditableSectionName = ".dep-v0"
68+
cargoAuditableLegacySectionName = ".rust-deps-v0"
6369
)
6470

6571
func GetDependencyInfo(r io.ReaderAt) (VersionInfo, error) {
@@ -90,6 +96,8 @@ func GetDependencyInfo(r io.ReaderAt) (VersionInfo, error) {
9096
return VersionInfo{}, ErrUnknownFileFormat
9197
}
9298
x = &machoExe{f}
99+
case bytes.HasPrefix(header, wasmHeader):
100+
x = &wasmReader{r}
93101
default:
94102
return VersionInfo{}, ErrUnknownFileFormat
95103
}
@@ -135,13 +143,13 @@ type elfExe struct {
135143
func (x *elfExe) ReadRustDepSection() ([]byte, error) {
136144
// Try .dep-v0 first, falling back to .rust-deps-v0 as used in
137145
// in rust-audit 0.1.0
138-
depInfo := x.f.Section(".dep-v0")
146+
depInfo := x.f.Section(cargoAuditableSectionName)
139147

140148
if depInfo != nil {
141149
return depInfo.Data()
142150
}
143151

144-
depInfo = x.f.Section(".rust-deps-v0")
152+
depInfo = x.f.Section(cargoAuditableLegacySectionName)
145153

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

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

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

194202
return depInfo.Data()
195203
}
204+
205+
type wasmReader struct {
206+
r io.ReaderAt
207+
}
208+
209+
func (x *wasmReader) ReadRustDepSection() ([]byte, error) {
210+
r := x.r
211+
var offset int64 = 0
212+
213+
// Check the preamble (magic number and version)
214+
buf := make([]byte, 8)
215+
_, err := r.ReadAt(buf, offset)
216+
offset += 8
217+
if err != nil || !bytes.Equal(buf, wasmHeader) {
218+
return nil, ErrUnknownFileFormat
219+
}
220+
221+
// https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#custom-section%E2%91%A0
222+
// Look through the sections until we find a custom .dep-v0 section or EOF
223+
for {
224+
// Read single byte section ID
225+
sectionId := make([]byte, 1)
226+
_, err = r.ReadAt(sectionId, offset)
227+
offset += 1
228+
if err == io.EOF {
229+
return nil, ErrNoRustDepInfo
230+
} else if err != nil {
231+
return nil, ErrUnknownFileFormat
232+
}
233+
234+
// Read section size
235+
buf = make([]byte, 4)
236+
_, err = r.ReadAt(buf, offset)
237+
if err != nil {
238+
return nil, ErrUnknownFileFormat
239+
}
240+
sectionSize, n, err := readUint32(buf)
241+
if err != nil {
242+
return nil, ErrUnknownFileFormat
243+
}
244+
offset += n
245+
nextSection := offset + int64(sectionSize)
246+
247+
// Custom sections have a zero section ID
248+
if sectionId[0] != 0 {
249+
offset = nextSection
250+
continue
251+
}
252+
253+
// The custom section has a variable length name
254+
// followed by the data
255+
_, err = r.ReadAt(buf, offset)
256+
if err != nil {
257+
return nil, ErrUnknownFileFormat
258+
}
259+
nameSize, n, err := readUint32(buf)
260+
if err != nil {
261+
return nil, ErrUnknownFileFormat
262+
}
263+
offset += n
264+
265+
// Read section name
266+
name := make([]byte, nameSize)
267+
_, err = r.ReadAt(name, offset)
268+
if err != nil {
269+
return nil, ErrUnknownFileFormat
270+
}
271+
offset += int64(nameSize)
272+
273+
// Is this our custom section?
274+
if string(name) != cargoAuditableSectionName {
275+
offset = nextSection
276+
continue
277+
}
278+
279+
// Read audit data
280+
data := make([]byte, nextSection-offset)
281+
_, err = r.ReadAt(data, offset)
282+
if err != nil {
283+
return nil, ErrUnknownFileFormat
284+
}
285+
return data, nil
286+
287+
}
288+
}
289+
290+
// wrap binary.Uvarint to return uint32, checking for overflow
291+
// https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#integers%E2%91%A4
292+
func readUint32(buf []byte) (uint32, int64, error) {
293+
v, n := binary.Uvarint(buf)
294+
if n <= 0 || v > uint64(^uint32(0)) {
295+
return 0, 0, fmt.Errorf("overflow decoding uint32")
296+
}
297+
return uint32(v), int64(n), nil
298+
}

rustaudit_test.go

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package rustaudit
22

33
import (
4+
"bytes"
45
"log"
56
"os"
67
"testing"
@@ -22,3 +23,36 @@ func TestLinuxRustDependencies(t *testing.T) {
2223
assert.Equal(t, Package{Name: "crate_with_features", Version: "0.1.0", Source: "local", Kind: "runtime", Dependencies: []uint{1}, Root: true}, versionInfo.Packages[0])
2324
assert.Equal(t, false, versionInfo.Packages[1].Root)
2425
}
26+
27+
func TestWasmRustDependencies(t *testing.T) {
28+
// Generate this with `DOCKER_BUILDKIT=1 docker build -f test/Dockerfile -o . .`
29+
r, err := os.Open("wasm_crate.wasm")
30+
if err != nil {
31+
log.Fatal(err)
32+
}
33+
versionInfo, err := GetDependencyInfo(r)
34+
if err != nil {
35+
log.Fatal(err)
36+
}
37+
assert.Equal(t, 18, len(versionInfo.Packages))
38+
assert.Equal(t, Package{Name: "bumpalo", Version: "3.16.0", Source: "crates.io", Kind: "runtime", Dependencies: nil, Root: false}, versionInfo.Packages[0])
39+
assert.Equal(t, false, versionInfo.Packages[1].Root)
40+
}
41+
42+
func FuzzWasm(f *testing.F) {
43+
// Use the test fixture as a seed
44+
data, err := os.ReadFile("wasm_crate.wasm")
45+
if err != nil {
46+
log.Fatal(err)
47+
}
48+
f.Add(data)
49+
f.Fuzz(func(t *testing.T, input []byte) {
50+
r := bytes.NewReader(input)
51+
w := wasmReader{r}
52+
53+
_, err := w.ReadRustDepSection()
54+
if err != ErrNoRustDepInfo && err != ErrUnknownFileFormat && err != nil {
55+
t.Errorf("Unexpected error: %v", err)
56+
}
57+
})
58+
}

test/Dockerfile

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,14 @@
1-
FROM rust:1.62.1-bullseye as builder
1+
FROM rust:1.79.0-bullseye as builder
22
RUN git clone https://github.com/rust-secure-code/cargo-auditable.git
33
WORKDIR /cargo-auditable/cargo-auditable
44
RUN cargo build
5+
RUN rustup target add wasm32-unknown-unknown
6+
WORKDIR /cargo-auditable/cargo-auditable/tests/fixtures/wasm_crate
7+
RUN /cargo-auditable/target/debug/cargo-auditable auditable build --target wasm32-unknown-unknown
58
WORKDIR /cargo-auditable/cargo-auditable/tests/fixtures/workspace
69
RUN /cargo-auditable/target/debug/cargo-auditable auditable build
710
FROM scratch
811
COPY --from=builder \
12+
/cargo-auditable/cargo-auditable/tests/fixtures/wasm_crate/target/wasm32-unknown-unknown/debug/wasm_crate.wasm \
913
/cargo-auditable/cargo-auditable/tests/fixtures/workspace/target/debug/crate_with_features_bin \
10-
/crate_with_features_bin
14+
/

0 commit comments

Comments
 (0)