Skip to content

fix: pass pagination limit to log-reading callables to prevent OOM#860

Open
JeremiahChurch wants to merge 1 commit intopfrest:masterfrom
JeremiahChurch:fix/log-pagination-memory-exhaustion
Open

fix: pass pagination limit to log-reading callables to prevent OOM#860
JeremiahChurch wants to merge 1 commit intopfrest:masterfrom
JeremiahChurch:fix/log-pagination-memory-exhaustion

Conversation

@JeremiahChurch
Copy link

Summary

Fixes #806. Passes the pagination limit from Model::read_all() down through get_internal_objects() to log-reading callables so they can stop reading early instead of loading entire log histories into memory.

  • LogFileModelTraits::read_log() gains optional $limit parameter. When set, reads newest files first using memory-efficient readers and stops once enough lines are collected
  • Plain text files: reverse-reads from EOF via fseek - O(limit) memory
  • Compressed files: streams via PHP stream wrappers (compress.zlib://, compress.bzip2://) and popen (xz) with a ring buffer - O(limit) memory for all formats
  • gather_log_filepaths() now uses glob($base.'.*') to avoid matching unrelated files (e.g. filter.logrotate), sorts newest-first by rotation number extracted from the suffix after the base path
  • Model::get_internal_objects() accepts optional $limit hint, forwards to callables via cached reflection (backward-compatible with callables that don't accept it)
  • Model::read_all() passes limit + offset down; skips the optimization when $reverse=true since that needs the full dataset
  • Unbounded reads ($limit=0) follow the exact same code path as before
  • All 6 log model callables updated to accept and forward limit

Tested on live pfSense (FreeBSD 15.0, REST API v2.5)

28MB filter.log + 31 rotated .bz2 files:

Metric Before After
?limit=50 memory 512MB+ (OOM crash) 22MB
?limit=10 response time N/A (crash) 1ms
Rotated files touched All 31 Zero (current log had enough)

Files changed (9 files, +624 -48)

  • LogFileModelTraits.inc - core fix: bounded reading with tail/streaming readers
  • Model.inc - plumbing: forward limit hint through get_internal_objects() to callables
  • FirewallLog.inc, SystemLog.inc, DHCPLog.inc, AuthLog.inc, OpenVPNLog.inc, RESTAPILog.inc - trivial: add int $limit = 0 parameter to each callable
  • APIModelTraitsLogFileModelTraitsTestCase.inc - 10 new test cases

Test plan

  • All 5 existing LogFileModelTraits tests pass unchanged (unbounded reads)
  • 10 new test cases: bounded uncompressed, bounded gzip, bounded bzip2, current-file-only, limit=1, empty current log, mixed compression, limit exceeds total, unbounded backward-compat, unrelated file prefix filtering
  • Live test: Model::read_all(limit: 10) on FirewallLog returns correct results without OOM
  • Live test: unbounded read_log() still returns all entries (backward compat)
  • CI test suite

Log endpoints load ALL log files (current + every rotated/compressed
copy) into memory before Model::read_all() applies pagination via
array_slice(). Even ?limit=1 loads the entire dataset, causing PHP
memory exhaustion (512MB) on systems with moderate log volumes.

This passes the pagination limit from Model::read_all() through
get_internal_objects() to log-reading callables so they stop early.

LogFileModelTraits changes:
- read_log() accepts optional $limit parameter
- When limit > 0, reads newest files first and stops when satisfied
- Plain files: reverse-reads from EOF via fseek - O(limit) memory
- Compressed files: streams via PHP stream wrappers
  (compress.zlib://, compress.bzip2://) and popen (xz) with a ring
  buffer - O(limit) memory for all formats
- gather_log_filepaths() uses glob($base.'.*') to avoid matching
  unrelated files, sorts newest-first by rotation number
- Unbounded reads (limit=0) preserve existing behavior exactly

Model.inc changes:
- get_internal_objects() accepts optional $limit hint
- Forwards limit to callables via cached reflection check
  (backward-compatible with callables that don't accept it)
- read_all() passes limit+offset down; skips optimization when
  reverse=true since that needs the full dataset

All 6 log model callables updated to accept and forward limit.
10 new test cases covering bounded reads, edge cases, mixed
compression, and unrelated file filtering.

Tested on live pfSense (FreeBSD 15.0, 28MB filter.log + 31 rotated
bz2 files): memory dropped from 512MB+ (OOM) to 22MB, response
time 1ms for ?limit=10.

Fixes pfrest#806
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.

PHP memory exhaustion when reading large log files via LogFileModelTraits

1 participant