From e5730df1964d0d51571935ca8406b707581e7e49 Mon Sep 17 00:00:00 2001 From: "Sean T. Allen" Date: Sun, 5 Apr 2026 18:11:24 -0400 Subject: [PATCH] Protect pony-lint against oversize ignore files IgnoreMatcher._load_file reads .gitignore and .ignore files without checking size first. With hierarchical ignore loading, each directory in the walk can have its own ignore files, so an oversized one could cause unexpected memory consumption. Add a 64 KB size check (matching the config file bound) and also report files that exist but cannot be opened. Errors surface as lint/ignore-error diagnostics with exit code 2, following the same pattern as config file errors. Closes #5137 --- .release-notes/protect-ignore-file-size.md | 3 ++ tools/pony-lint/ignore_matcher.pony | 40 ++++++++++++++++++- tools/pony-lint/linter.pony | 22 +++++++++++ tools/pony-lint/test/_test_ignore.pony | 45 ++++++++++++++++++++++ tools/pony-lint/test/main.pony | 1 + 5 files changed, 109 insertions(+), 2 deletions(-) create mode 100644 .release-notes/protect-ignore-file-size.md diff --git a/.release-notes/protect-ignore-file-size.md b/.release-notes/protect-ignore-file-size.md new file mode 100644 index 0000000000..6293584654 --- /dev/null +++ b/.release-notes/protect-ignore-file-size.md @@ -0,0 +1,3 @@ +## Protect pony-lint against oversize ignore files + +pony-lint now rejects `.gitignore` and `.ignore` files larger than 64 KB. With hierarchical ignore loading, each directory in a project can have its own ignore files — an unexpectedly large file could cause excessive memory consumption. Ignore files that exceed the limit or that cannot be opened produce a `lint/ignore-error` diagnostic with exit code 2. diff --git a/tools/pony-lint/ignore_matcher.pony b/tools/pony-lint/ignore_matcher.pony index 8ddbfd7870..d6c863c3c3 100644 --- a/tools/pony-lint/ignore_matcher.pony +++ b/tools/pony-lint/ignore_matcher.pony @@ -46,6 +46,7 @@ class ref IgnoreMatcher let _root: String val let _in_git_repo: Bool let _rules: Array[(IgnorePattern val, String val)] + let _errors: Array[(String val, String val)] new ref create(file_auth: FileAuth, root: (String val | None)) => """ @@ -63,6 +64,7 @@ class ref IgnoreMatcher _in_git_repo = false end _rules = Array[(IgnorePattern val, String val)] + _errors = Array[(String val, String val)] fun ref load_directory(dir_path: String val) => """ @@ -75,15 +77,47 @@ class ref IgnoreMatcher end _load_file(Path.join(dir_path, ".ignore"), dir_path) + fun errors(): this->Array[(String val, String val)] => + """ + Return errors accumulated during `load_directory` calls. Each entry is + a `(message, file_path)` tuple. Call `clear_errors` after draining. + """ + _errors + + fun ref clear_errors() => + """ + Remove all accumulated errors. Call after draining `errors()`. + """ + _errors.clear() + fun ref _load_file(file_path: String val, base_dir: String val) => """ Parse all lines from an ignore file and append the resulting rules. + + Rejects files that cannot be opened or are larger than 64 KB to prevent + unexpected memory consumption, especially with hierarchical ignore + loading where each directory can have its own `.gitignore` and `.ignore` + files. """ let fp = FilePath(_file_auth, file_path) if not fp.exists() then return end let file = File.open(fp) - if not file.valid() then return end - let content: String val = file.read_string(file.size()) + if not file.valid() then + _errors.push(( + "could not open ignore file: " + file_path, + file_path)) + return + end + let size = file.size() + if size > _max_ignore_file_size() then + file.dispose() + _errors.push(( + "ignore file too large (" + size.string() + " bytes, max " + + _max_ignore_file_size().string() + "): " + file_path, + file_path)) + return + end + let content: String val = file.read_string(size) file.dispose() // Normalize line endings and parse let clean_content: String val = @@ -168,3 +202,5 @@ class ref IgnoreMatcher else false end + + fun _max_ignore_file_size(): USize => 65_536 diff --git a/tools/pony-lint/linter.pony b/tools/pony-lint/linter.pony index f8ca9247e8..43deb416a9 100644 --- a/tools/pony-lint/linter.pony +++ b/tools/pony-lint/linter.pony @@ -480,6 +480,17 @@ class val Linter end end + // Surface errors from intermediate ignore file loading + for (msg, path) in matcher.errors().values() do + config_errors.push(Diagnostic( + "lint/ignore-error", + msg, + try Path.rel(_cwd, path)? else path end, + 0, + 0)) + end + matcher.clear_errors() + // Pre-load subdirectory configs from hierarchy root through intermediate // directories to each target. Uses _root_dir (config hierarchy root), // not the git root. @@ -674,6 +685,17 @@ class ref _FileCollector is WalkHandler // Load ignore files for this directory _matcher.load_directory(dir_path.path) + // Surface errors from ignore file loading + for (msg, path) in _matcher.errors().values() do + _config_errors.push(Diagnostic( + "lint/ignore-error", + msg, + try Path.rel(_cwd, path)? else path end, + 0, + 0)) + end + _matcher.clear_errors() + // Load subdirectory config if present _load_config(dir_path.path) diff --git a/tools/pony-lint/test/_test_ignore.pony b/tools/pony-lint/test/_test_ignore.pony index f73479a1a9..4a648e9afa 100644 --- a/tools/pony-lint/test/_test_ignore.pony +++ b/tools/pony-lint/test/_test_ignore.pony @@ -424,6 +424,51 @@ class \nodoc\ _TestIgnoreMatcherHierarchical is UnitTest h.fail("could not create temp directory") end +class \nodoc\ _TestIgnoreMatcherOversizedFile is UnitTest + """Oversized ignore file produces an error and loads no rules.""" + fun name(): String => "IgnoreMatcher: oversized file -> error" + + fun apply(h: TestHelper) => + let auth = h.env.root + try + let tmp = FilePath.mkdtemp(FileAuth(auth), "ignore-test")? + let git_dir = + FilePath(FileAuth(auth), Path.join(tmp.path, ".git")) + git_dir.mkdir() + // Write a .gitignore that exceeds the 64 KB limit + let gitignore_path = + Path.join(tmp.path, ".gitignore") + let gitignore = + File(FilePath(FileAuth(auth), gitignore_path)) + let content = recover val String(65_537) .> append("x" * 65_537) end + gitignore.print(content) + gitignore.dispose() + + let matcher = lint.IgnoreMatcher(FileAuth(auth), tmp.path) + matcher.load_directory(tmp.path) + + // Should not have loaded any rules + h.assert_false( + matcher.is_ignored( + Path.join(tmp.path, "anything"), "anything", false)) + + // Should have recorded an error + let errs = matcher.errors() + h.assert_eq[USize](1, errs.size()) + try + (let msg, _) = errs(0)? + h.assert_true(msg.contains("too large")) + else + h.fail("could not read error") + end + + FilePath(FileAuth(auth), gitignore_path).remove() + git_dir.remove() + tmp.remove() + else + h.fail("could not create temp directory") + end + class \nodoc\ _TestIgnoreMatcherAnchoredPattern is UnitTest """Anchored patterns match against the relative path from base_dir.""" fun name(): String => "IgnoreMatcher: anchored pattern" diff --git a/tools/pony-lint/test/main.pony b/tools/pony-lint/test/main.pony index de50329fa2..0d412bc5a5 100644 --- a/tools/pony-lint/test/main.pony +++ b/tools/pony-lint/test/main.pony @@ -169,6 +169,7 @@ actor \nodoc\ Main is TestList test(_TestIgnoreMatcherNonGitIgnoresGitignore) test(_TestIgnoreMatcherHierarchical) test(_TestIgnoreMatcherAnchoredPattern) + test(_TestIgnoreMatcherOversizedFile) // Linter tests test(_TestLinterSingleFile)