Skip to content

fix: stop a stack overflow on absolute zip names and repair CI - #1777

Merged
egibs merged 1 commit into
chainguard-dev:mainfrom
egibs:fix/zip-absolute-names-guarddog-ecosystem
Oct 27, 2026
Merged

egibs merged 1 commit into
chainguard-dev:mainfrom
egibs:fix/zip-absolute-names-guarddog-ecosystem

Conversation

@egibs

@egibs egibs commented Oct 27, 2026

Copy link
Copy Markdown
Member

Summary

Fixes the fatal stack overflow during melange scans of the PHP -src packages, the failing scheduled third-party updates, and the failing Zizmor job. Also bumps Go and tooling, and fixes test failures on darwin.

Stack overflow scanning php-src (fatal, not recoverable)

The crash happened in zip extraction nested inside the PHP source tarball. The xz decoder wasn't involved. php-src test fixtures contain zip entries with absolute names. On case-sensitive filesystems those reached entryRoots.createFile, and DirRoots.open walked up parents until ".". The parents of /foo/bar end at /, and filepath.Dir("/") == "/", so open asked the root cache for its own parent until the goroutine hit the 1 GB stack limit. A stack overflow is a fatal runtime error, so recoverExtractor cannot catch it.

  • pkg/archive/zip.go: extract absolute names beneath the extraction directory, as unzip, Python's zipfile, and PHP's ZipArchive do, so their contents are scanned rather than skipped. Before this change, an absolute directory entry aborted the whole zip extraction.
  • pkg/file/rootcache.go: make DirRoots.open iterative. It finds the nearest parent with an open root, then opens each missing parent through the last one and caches it. Opening a directory at any depth now takes constant stack and holds at most two roots; non-local paths go straight to the root, which rejects them. Syscall count and caching match the recursive version.

I also listed every recursive cycle in the module from a VTA call graph. The remaining ones are bounded: rule references between bundled rules, programkind with constant arguments, and the directory walkers, which hold one descriptor per level and so hit the fd limit long before 1 GB of stack.

Third-party rule updates

GuardDog's v3 rules now gate two rules on an ecosystem external variable, so compiling them failed with E009 (runs 40331427435, 40425187888, 40501567121). pkg/compile now defines ecosystem as "", matching GuardDog's own default, so those conditions stay off. With GuardDog's current upstream files dropped in, the full bundled rule set compiles.

Zizmor

zizmor-action v0.7.1 (#1766) installs zizmor from PyPI instead of a container image, and harden-runner blocked the download, so the job has failed since 2026-10-20. This allows pypi.org and files.pythonhosted.org, drops the unused container endpoints, and fixes the pedantic findings that showed up once zizmor ran again: chainguard-dev/actions pins commented # main actually point at tag v1.7.1, and CodeQL now uses the runner's rustup instead of dtolnay/rust-toolchain.

macOS /private paths

Reports handled macOS's /private in several places, each differently, so library callers saw mixed keys and several tests failed on darwin. report.DisplayPath (which replaces TrimPrefixes) is now the only place that knows about /private. It drops /private before etc, tmp, and var only, compares trim prefixes in the same form, and behaves the same on every platform. Every key and display path goes through it. The CLI no longer appends /private to the trim prefixes. Linux output doesn't change.

Build and tests

  • Go 1.27.3, go get -u ./..., golangci-lint v2.15.0 (install script unchanged), gosec -exclude-generated.
  • darwin: the compile cache tests use compileIsolateCache, and content_unix_test.go uses unix.Mkfifo (x/sys has no Mkfifoat on darwin, so pkg/action tests did not build there).

Testing

  • New tests fail without each fix: the zip absolute-name test (both case-folding modes), TestZipEntryPath, the DirRoots absolute-path cases (they reproduce the production fatal error: stack overflow), TestDirRootsOpensDeepDirectoriesInBoundedStack (1,000 levels under a 128 KiB stack cap), TestRecursiveLeavesEcosystemEmpty (reproduces E009), and the TestDisplayPath cases.
  • make verify passes on darwin.
  • make refresh-sample-testdata (run on Linux, as CI does) refreshed all 396 samples with no changes.
  • zizmor --persona pedantic (1.31.1) and actionlint report no findings.

Follow-up

Measuring zips that nest a file 32,765 directories deep (the most a zip name allows) on Linux shows no stack growth, and mal scan reaches the file. Two older limits remain: the recursive walkers keep every ancestor's full path alive, using memory quadratic in depth (3.6 GiB RSS at that depth), and nested archives deeper than PATH_MAX fail to extract because archive.OpenTree sniffs them by absolute path. Both need walker and OpenTree changes, which belong in their own PR.

A zip entry named by an absolute path, as in php-src test fixtures,
reached entryRoots.createFile on case-sensitive filesystems.
DirRoots.open walked up parents until ".", but the parents of an
absolute path end at "/", and filepath.Dir("/") is "/", so open asked
the root cache for its own parent until the goroutine exceeded the
1 GB stack limit. A stack overflow is fatal, so recoverExtractor could
not catch it.

- Extract absolute zip names beneath the extraction directory, as
  unzip, Python's zipfile, and PHP's ZipArchive do, so their contents
  are scanned. An absolute directory entry previously aborted the
  whole extraction.
- Make DirRoots.open iterative: open each missing parent through the
  nearest open one and keep it for reuse, so a directory at any depth
  takes constant stack and at most two roots. Non-local paths go
  straight to the root, which rejects them.
- Define GuardDog's `ecosystem` global as "", its own default, so the
  scheduled third-party update compiles rules gated on it (E009).
- Zizmor: zizmor-action v0.7.1 installs from PyPI, which harden-runner
  blocked; allow PyPI, correct the chainguard-dev/actions pin comments
  (v1.7.1, not main), and install Rust in CodeQL with rustup.
- Add report.DisplayPath, the one place that handles macOS /private:
  it drops /private before etc, tmp, and var on every platform and
  compares trim prefixes in the same form. Every key and display path
  goes through it, replacing TrimPrefixes, the archive-only stripping,
  and the CLI's darwin-only trim prefix, which disagreed with one
  another.
- Bump Go to 1.27.3, dependencies, and golangci-lint to v2.15.0; pass
  gosec -exclude-generated, since it now flags the cgo yara-x binding.
- Fix darwin test failures: use compileIsolateCache in the split cache
  tests and unix.Mkfifo in content_unix_test.go.

Signed-off-by: egibs <20933572+egibs@users.noreply.github.com>
@egibs
egibs requested a review from mattlorimor October 27, 2026 14:00
@egibs
egibs enabled auto-merge (squash) October 27, 2026 14:08
@egibs
egibs merged commit 21f4cae into chainguard-dev:main Oct 27, 2026
20 checks passed
@egibs
egibs deleted the fix/zip-absolute-names-guarddog-ecosystem branch October 27, 2026 16:30
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants