* fix(dotnet): bound the bundle signature search by the file, not the headers The single-file bundle marker search sized its buffer from `calculatePEEndOffset` / `calculateELFEndOffset`, both of which sum user-controlled header fields. A tiny file declaring 4GB sections drove an 8.6GB allocation, and on the ELF side the sum could overflow negative and panic in `make`. The search now clamps its window to the real file length before sizing anything. The PE and ELF paths had grown two copies of the same find-and-read sequence, so they collapse into one `bundle.ExtractDepsJSON`, which lets the signature search and the header reader both go unexported. Also guards `read7BitEncodedInt` against returning a negative length, which on a 32-bit `int` would seek a manifest walk backwards and re-read the same bytes per declared file. Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com> * fix(dotnet): stop a truncated version resource from spinning forever `readIntoStruct` reported a truncated read as success and left the destination struct zeroed. Its callers advance their loop counters by the offsets it updates, so the string table loop consumed nothing, advanced nothing, and never reached its length bound: a version resource whose header claims more content than the blob carries hung the parse. A truncated read is now an error, and the two places where hitting the end of a blob is normal say so explicitly. That distinction is what keeps a well-formed resource whose last child is `VarFileInfo` from losing its `FileVersion` fallback. io.EOF and io.ErrUnexpectedEOF mean the same thing here, so both take the same path. The string table walk moves into its own function, since the two new arms put `parseVersionResourceSection` over the funlen limit. Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com> * fix(dotnet): bound PE resource parsing by what the file actually holds Resource sizes and RVAs are user-controlled uint32s that sized buffers before anything checked the file held that many bytes, so a small crafted section could reserve up to 4GB. The walk now carries the section's reader and its base RVA for its whole lifetime, so `reader.Size()` is the single authoritative bound and one `offsetOf` helper rejects every RVA the section does not hold. That replaces the per-node section value the recursion used to synthesize, whose size argument was only correct by convention. Bounding the allocations alone turns the OOM into a hang, though: nothing in the format stops entries from aliasing, so thousands of them may name one fat blob and every offset still checks out. A 256KB section built that way drove 24GB of allocation over two minutes with peak memory flat. Capping entry *count* does not help since the cost is per byte read, so the walk now charges every read against a budget of a few times the section size. Real binaries come in at about 1x, since a well-formed section's entries partition it. A resource tree that stops early no longer fails the whole file. It says nothing about the CLR directory or an embedded deps.json, and dropping the package from the SBOM over one malformed section is worse than reporting the fields we did get. Also widens the entry counts before summing them (two uint16s can sum past 0xFFFF and wrap), and bounds the name length and section header count against the reader for the same reason. Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com> * fix(dotnet): bound resource recursion depth and cap declared sizes absolutely Review follow-ups on this branch. The headline one is a real crash: the resource walk had no depth bound, and the `budget` comment claiming depth "falls out for free" was wrong, since the budget scales with the section while the stack does not. A chain of directories at distinct RVAs (which the `dirs` set does not catch) recursed until the goroutine stack gave out. A 13.7MB crafted PE took the whole process down with `fatal error: stack overflow`, which is unrecoverable, so unlike every other bug on this branch no caller could log-and-continue past it. Now capped at 32 levels, versus the 3 the format actually uses. The other bounds moved from "the file" to a real ceiling. Clamping an allocation to the bytes remaining in the file is not enough on its own: a mostly empty file costs almost nothing inside a compressed layer, so a cheap artifact could still authorize a multi-gigabyte read. Both the PE data directories and the bundle marker search now cap the same way `elfutil` already does. Also hardened in the bundle reader: - `headerOffset` is validated against the file. It comes straight out of the file bytes and everything downstream seeks to it, so it was the least trustworthy value in there and the only one not being checked. - a manifest entry count that could not fit in the remaining bytes is rejected rather than walked for hundreds of millions of reads. - a non-positive `searchLimit` falls back to the whole file instead of reading nothing. It is only ever an optimization hint, so treating it as authoritative let one bogus ELF `p_offset` hide a bundle from us entirely. - a short read no longer aborts the search. `unionreader` can return fewer bytes than the file reports, and the marker may well be in what we did get. `pe.Read` now reports partial parses instead of choosing between dropping the package and hiding the problem. It was inconsistent: a malformed resource tree was non-fatal while an unreadable resource *section* one call away was fatal, and the non-fatal path only reached a trace log, so the unknowns task could not see that a file had been cataloged on incomplete evidence. Both now land on a new `File.ParseErr`, which the dotnet and PE catalogers join into their unknowns and still keep the package. Worth noting for changelog purposes: the `isTruncated` guard fixes a user-visible bug beyond the hardening. A version resource leaving 1 to 5 trailing bytes cuts the next struct in half, which reports as `io.ErrUnexpectedEOF` rather than `io.EOF`; that used to propagate out and skip the `VS_FIXEDFILEINFO` fallback, so those binaries lost their version entirely. Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com> * fix(dotnet): treat a zero bundle offset as unbundled, not malformed The apphost ships the 32-byte bundle signature compiled in with its 8-byte header offset left as zero, and only `HostWriter.SetAsBundle` fills that slot in when you publish as a single file. So a zero offset means "never bundled", which is what every framework-dependent .NET executable looks like. The bounds work on this branch folded that case in with the hostile ones, so the most common shape a .NET binary takes started reporting a parse failure. It surfaces through the PE cataloger rather than the dotnet one, since an apphost carries no CLR metadata of its own: the package still comes out, with a spurious unknown attached to it. `main` had this right and said so in a comment; the regression is on this branch. Reproduces on a fixture already in the repo. Of the 53 PE files in `image-net8-app`, `/app/dotnetapp.exe` was the only one reporting anything, and the whole image is clean again now. The three outcomes are now distinct, since collapsing any two of them loses something the caller needs: zero means there is no bundle, an error means we could not tell, and a positive offset is the answer. `Test_Read_DotNetDetection` gains the apphost and, more usefully, an assertion that a well-formed binary reports no partial parse at all. That one line is what would have caught this, and it now covers all six fixtures. The unit case that pinned the old behavior asserted the bug, so it flips. Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com> * fix(dotnet): measure readers with the shared helper and report refusals Two things, both about not re-deriving what the campaign already has. `internal/file.ReaderSize` exists on `main` for exactly this job, and `upx.go` already uses it. Its doc comment is the problem statement here almost verbatim: a declared size is worth nothing until you weigh it against a byte count the reader can actually back, which it does by reading the last byte. This branch was hand-rolling `Seek(0, io.SeekEnd)` in three places, one of them reimplementing the helper's save-and-restore dance verbatim, and then absorbing the reader-lies case downstream by tolerating short reads. Adopting it means the bundle reader and `readDataFromRVA` now take the reader types they were always being handed (`unionreader.UnionReader` and `io.ReaderAt`), and the reads move to `ReadAt`, which leaves the caller's cursor alone and drops two seeks. Net effect is a smaller diff than what it replaces, and the case the `shortReader` fake was invented for is now caught up front instead of tolerated: the fake was modeling a lie about length, which is not what a short squashfs read actually does. It now models the real shape (honest length, read stops early) and a second reader covers the lie. Three places also still returned "there is nothing here" when what they meant was "I could not tell", which is the thing `File.ParseErr` was added to stop: - an ELF that `elfutil` refused to parse read as "not an ELF". `ErrDeclaredSizeExceeded` is exported precisely so a refusal can be told apart from a broken file, and the caller upstream already turns an error into an unknown. - a `deps.json` past the 3MB cap returned "". That is the file's entire dependency list, so this handed back an SBOM that looked complete and silently was not. - the marker search clamped at 512MB and then reported no bundle. It cannot claim there is no bundle when it declined to look everywhere one could be, so a miss after clamping is now an error. `readSizeRecorder` records `ReadAt` alongside `Read`, or the allocation-bound tests would have quietly stopped measuring anything. Also renames `findSignatureOffset` back to `findBundleHeaderOffset`, since it returns a header offset and reading it as "no signature" is what the last bug was, and fixes the stale `ExtractDepsJSONFromBundle` doc comment. Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com> * test(dotnet): assert allocation sizes directly in the bundle search guards The PE and ELF search guards asserted the largest read the reader was asked for. That happened to track the allocation, since the whole buffer goes to ReadAt, but nothing said so. Both now measure the bytes allocated with the same `measureAlloc` helper the resource guards use, and keep the read-size check as a separate claim. Also names the reason a reader can report a size it cannot back: `debug/elf` and `debug/pe` hand out `io.NewSectionReader(r, 0, 1<<63-1)`, whose `Size()` is nominal rather than measured, so answering a size is not on its own evidence the bytes are there. Renames `lyingReader` to `unbackedSizeReader` and regroups the two reader implementations, which had their methods interleaved. Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com> --------- Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>
Syft
A CLI tool and Go library for generating a Software Bill of Materials (SBOM) from container images and filesystems. Exceptional for vulnerability detection when used with a scanner like Grype.
Features
- Generates SBOMs for container images, filesystems, archives (see the docs for a full list of supported scan targets)
- Supports dozens of packaging ecosystems (e.g. Alpine (apk), Debian (dpkg), RPM, Go, Python, Java, JavaScript, Ruby, Rust, PHP, .NET, and many more)
- Supports OCI, Docker, Singularity, and more image formats
- Works seamlessly with Grype for vulnerability scanning
- Multiple output formats (CycloneDX, SPDX, Syft JSON, and more) including the ability to convert between SBOM formats
- Create signed SBOM attestations using the in-toto specification
Tip
New to Syft? Check out the Getting Started guide for a walkthrough!
Installation
The quickest way to get up and going:
curl -sSfL https://get.anchore.io/syft | sudo sh -s -- -b /usr/local/bin
Tip
See Installation docs for more ways to get Syft, including Homebrew, Docker, Scoop, Chocolatey, Nix, and more!
The basics
See the packages within a container image or directory:
# container image
syft alpine:latest
# directory
syft ./my-project
To get an SBOM, specify one or more output formats:
# SBOM to stdout
syft <image> -o cyclonedx-json
# Multiple SBOMs to files
syft <image> -o spdx-json=./spdx.json -o cyclonedx-json=./cdx.json
Tip
Check out the Getting Started guide to explore all of the capabilities and features.
Want to know all of the ins-and-outs of Syft? Check out the CLI docs, configuration docs, and JSON schema.
Contributing
We encourage users to help make these tools better by submitting issues when you find a bug or want a new feature. Check out our contributing overview and developer-specific documentation if you are interested in providing code contributions.
Syft development is sponsored by Anchore, and is released under the Apache-2.0 License.
The Syft logo by Anchore is licensed under CC BY 4.0
For commercial support options with Syft or Grype, please contact Anchore.
Come talk to us!
The Syft Team holds regular community meetings online. All are welcome to join to bring topics for discussion.
- Check the calendar for the next meeting date.
- Add items to the agenda (join this group for write access to the agenda)
- See you there!

