-
Notifications
You must be signed in to change notification settings - Fork 3
/
Copy pathrustaudit.go
298 lines (249 loc) · 6.96 KB
/
rustaudit.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
package rustaudit
import (
"bytes"
"compress/zlib"
"debug/elf"
"debug/macho"
"debug/pe"
"encoding/binary"
"encoding/json"
"errors"
"fmt"
"io"
)
// This struct is embedded in dependencies produced with rust-audit:
// https://github.com/Shnatsel/rust-audit/blob/bc805a8fdd1492494179bd01a598a26ec22d44fe/auditable-serde/src/lib.rs#L89
type VersionInfo struct {
Packages []Package `json:"packages"`
}
type DependencyKind string
const (
Build DependencyKind = "build"
Runtime DependencyKind = "runtime"
)
type Package struct {
Name string `json:"name"`
Version string `json:"version"`
Source string `json:"source"`
Kind DependencyKind `json:"kind"`
Dependencies []uint `json:"dependencies"`
Features []string `json:"features"` // Removed in cargo-auditable 0.5.0
Root bool `json:"root"`
}
// Default the Kind to Runtime during unmarshalling
func (p *Package) UnmarshalJSON(text []byte) error {
type pkgty Package
pkg := pkgty{
Kind: Runtime,
}
if err := json.Unmarshal(text, &pkg); err != nil {
return err
}
*p = Package(pkg)
return nil
}
var (
// Returned if an executable is not a known format
ErrUnknownFileFormat = errors.New("unknown file format")
// errNoRustDepInfo is returned when an executable file doesn't contain Rust dependency information
ErrNoRustDepInfo = errors.New("rust dependency information not found")
// Headers for different binary types
elfHeader = []byte("\x7FELF")
peHeader = []byte("MZ")
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) {
// Read file header
header := make([]byte, 16)
n, err := r.ReadAt(header, 0)
if n < len(header) || err != nil {
return VersionInfo{}, ErrUnknownFileFormat
}
var x exe
switch {
case bytes.HasPrefix(header, elfHeader):
f, err := elf.NewFile(r)
if err != nil {
return VersionInfo{}, ErrUnknownFileFormat
}
x = &elfExe{f}
case bytes.HasPrefix(header, peHeader):
f, err := pe.NewFile(r)
if err != nil {
return VersionInfo{}, ErrUnknownFileFormat
}
x = &peExe{f}
case bytes.HasPrefix(header, machoHeader) || bytes.HasPrefix(header[1:], machoHeaderLittleEndian) || bytes.HasPrefix(header, machoUniversalHeader):
f, err := macho.NewFile(r)
if err != nil {
return VersionInfo{}, ErrUnknownFileFormat
}
x = &machoExe{f}
case bytes.HasPrefix(header, wasmHeader):
x = &wasmReader{r}
default:
return VersionInfo{}, ErrUnknownFileFormat
}
data, err := x.ReadRustDepSection()
if err != nil {
return VersionInfo{}, err
}
// The json is compressed using zlib, so decompress it
b := bytes.NewReader(data)
reader, err := zlib.NewReader(b)
if err != nil {
return VersionInfo{}, fmt.Errorf("section not compressed: %w", err)
}
buf, err := io.ReadAll(reader)
reader.Close()
if err != nil {
return VersionInfo{}, fmt.Errorf("failed to decompress JSON: %w", err)
}
var versionInfo VersionInfo
err = json.Unmarshal(buf, &versionInfo)
if err != nil {
return VersionInfo{}, fmt.Errorf("failed to unmarshall JSON: %w", err)
}
return versionInfo, nil
}
// Interface for binaries that may have a Rust dependencies section
type exe interface {
ReadRustDepSection() ([]byte, error)
}
type elfExe struct {
f *elf.File
}
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(cargoAuditableSectionName)
if depInfo != nil {
return depInfo.Data()
}
depInfo = x.f.Section(cargoAuditableLegacySectionName)
if depInfo == nil {
return nil, ErrNoRustDepInfo
}
return depInfo.Data()
}
type peExe struct {
f *pe.File
}
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(cargoAuditableSectionName)
if depInfo != nil {
return depInfo.Data()
}
depInfo = x.f.Section("rdep-v0")
if depInfo == nil {
return nil, ErrNoRustDepInfo
}
return depInfo.Data()
}
type machoExe struct {
f *macho.File
}
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(cargoAuditableSectionName)
if depInfo != nil {
return depInfo.Data()
}
depInfo = x.f.Section("rust-deps-v0")
if depInfo == nil {
return nil, ErrNoRustDepInfo
}
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
}
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 {
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
}