Skip to content

Commit 1157bf8

Browse files
authored
Protect pony-lint against oversize ignore files (#5140)
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
1 parent c3106b8 commit 1157bf8

File tree

5 files changed

+109
-2
lines changed

5 files changed

+109
-2
lines changed
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
## Protect pony-lint against oversize ignore files
2+
3+
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.

tools/pony-lint/ignore_matcher.pony

Lines changed: 38 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ class ref IgnoreMatcher
4646
let _root: String val
4747
let _in_git_repo: Bool
4848
let _rules: Array[(IgnorePattern val, String val)]
49+
let _errors: Array[(String val, String val)]
4950

5051
new ref create(file_auth: FileAuth, root: (String val | None)) =>
5152
"""
@@ -63,6 +64,7 @@ class ref IgnoreMatcher
6364
_in_git_repo = false
6465
end
6566
_rules = Array[(IgnorePattern val, String val)]
67+
_errors = Array[(String val, String val)]
6668

6769
fun ref load_directory(dir_path: String val) =>
6870
"""
@@ -75,15 +77,47 @@ class ref IgnoreMatcher
7577
end
7678
_load_file(Path.join(dir_path, ".ignore"), dir_path)
7779

80+
fun errors(): this->Array[(String val, String val)] =>
81+
"""
82+
Return errors accumulated during `load_directory` calls. Each entry is
83+
a `(message, file_path)` tuple. Call `clear_errors` after draining.
84+
"""
85+
_errors
86+
87+
fun ref clear_errors() =>
88+
"""
89+
Remove all accumulated errors. Call after draining `errors()`.
90+
"""
91+
_errors.clear()
92+
7893
fun ref _load_file(file_path: String val, base_dir: String val) =>
7994
"""
8095
Parse all lines from an ignore file and append the resulting rules.
96+
97+
Rejects files that cannot be opened or are larger than 64 KB to prevent
98+
unexpected memory consumption, especially with hierarchical ignore
99+
loading where each directory can have its own `.gitignore` and `.ignore`
100+
files.
81101
"""
82102
let fp = FilePath(_file_auth, file_path)
83103
if not fp.exists() then return end
84104
let file = File.open(fp)
85-
if not file.valid() then return end
86-
let content: String val = file.read_string(file.size())
105+
if not file.valid() then
106+
_errors.push((
107+
"could not open ignore file: " + file_path,
108+
file_path))
109+
return
110+
end
111+
let size = file.size()
112+
if size > _max_ignore_file_size() then
113+
file.dispose()
114+
_errors.push((
115+
"ignore file too large (" + size.string() + " bytes, max "
116+
+ _max_ignore_file_size().string() + "): " + file_path,
117+
file_path))
118+
return
119+
end
120+
let content: String val = file.read_string(size)
87121
file.dispose()
88122
// Normalize line endings and parse
89123
let clean_content: String val =
@@ -168,3 +202,5 @@ class ref IgnoreMatcher
168202
else
169203
false
170204
end
205+
206+
fun _max_ignore_file_size(): USize => 65_536

tools/pony-lint/linter.pony

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -480,6 +480,17 @@ class val Linter
480480
end
481481
end
482482

483+
// Surface errors from intermediate ignore file loading
484+
for (msg, path) in matcher.errors().values() do
485+
config_errors.push(Diagnostic(
486+
"lint/ignore-error",
487+
msg,
488+
try Path.rel(_cwd, path)? else path end,
489+
0,
490+
0))
491+
end
492+
matcher.clear_errors()
493+
483494
// Pre-load subdirectory configs from hierarchy root through intermediate
484495
// directories to each target. Uses _root_dir (config hierarchy root),
485496
// not the git root.
@@ -674,6 +685,17 @@ class ref _FileCollector is WalkHandler
674685
// Load ignore files for this directory
675686
_matcher.load_directory(dir_path.path)
676687

688+
// Surface errors from ignore file loading
689+
for (msg, path) in _matcher.errors().values() do
690+
_config_errors.push(Diagnostic(
691+
"lint/ignore-error",
692+
msg,
693+
try Path.rel(_cwd, path)? else path end,
694+
0,
695+
0))
696+
end
697+
_matcher.clear_errors()
698+
677699
// Load subdirectory config if present
678700
_load_config(dir_path.path)
679701

tools/pony-lint/test/_test_ignore.pony

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -424,6 +424,51 @@ class \nodoc\ _TestIgnoreMatcherHierarchical is UnitTest
424424
h.fail("could not create temp directory")
425425
end
426426

427+
class \nodoc\ _TestIgnoreMatcherOversizedFile is UnitTest
428+
"""Oversized ignore file produces an error and loads no rules."""
429+
fun name(): String => "IgnoreMatcher: oversized file -> error"
430+
431+
fun apply(h: TestHelper) =>
432+
let auth = h.env.root
433+
try
434+
let tmp = FilePath.mkdtemp(FileAuth(auth), "ignore-test")?
435+
let git_dir =
436+
FilePath(FileAuth(auth), Path.join(tmp.path, ".git"))
437+
git_dir.mkdir()
438+
// Write a .gitignore that exceeds the 64 KB limit
439+
let gitignore_path =
440+
Path.join(tmp.path, ".gitignore")
441+
let gitignore =
442+
File(FilePath(FileAuth(auth), gitignore_path))
443+
let content = recover val String(65_537) .> append("x" * 65_537) end
444+
gitignore.print(content)
445+
gitignore.dispose()
446+
447+
let matcher = lint.IgnoreMatcher(FileAuth(auth), tmp.path)
448+
matcher.load_directory(tmp.path)
449+
450+
// Should not have loaded any rules
451+
h.assert_false(
452+
matcher.is_ignored(
453+
Path.join(tmp.path, "anything"), "anything", false))
454+
455+
// Should have recorded an error
456+
let errs = matcher.errors()
457+
h.assert_eq[USize](1, errs.size())
458+
try
459+
(let msg, _) = errs(0)?
460+
h.assert_true(msg.contains("too large"))
461+
else
462+
h.fail("could not read error")
463+
end
464+
465+
FilePath(FileAuth(auth), gitignore_path).remove()
466+
git_dir.remove()
467+
tmp.remove()
468+
else
469+
h.fail("could not create temp directory")
470+
end
471+
427472
class \nodoc\ _TestIgnoreMatcherAnchoredPattern is UnitTest
428473
"""Anchored patterns match against the relative path from base_dir."""
429474
fun name(): String => "IgnoreMatcher: anchored pattern"

tools/pony-lint/test/main.pony

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,7 @@ actor \nodoc\ Main is TestList
169169
test(_TestIgnoreMatcherNonGitIgnoresGitignore)
170170
test(_TestIgnoreMatcherHierarchical)
171171
test(_TestIgnoreMatcherAnchoredPattern)
172+
test(_TestIgnoreMatcherOversizedFile)
172173

173174
// Linter tests
174175
test(_TestLinterSingleFile)

0 commit comments

Comments
 (0)