Skip to content

fix-medium: keep the linked-monitor expression's own copy of its string - #5272

Open
nabbi wants to merge 1 commit into
ZoneMinder:masterfrom
nabbi:fix/b81-monitorlink-own-expression
Open

nabbi wants to merge 1 commit into
ZoneMinder:masterfrom
nabbi:fix/b81-monitorlink-own-expression

Conversation

@nabbi

@nabbi nabbi commented Oct 12, 2026 •

Copy link
Copy Markdown
Contributor

Impact: fix-medium (use-after-free in the linked-monitor expression after zmu -L (admin action, read at debug level 1+))

Automated PR, tested in a zm-holodeck sandbox. An AI agent (Claude Opus 5.5) reproduced
the bug on a disposable ZoneMinder built from upstream master 233a15828 with
simulated cameras, wrote this fix and ran the checks below on the same lab. Minimal
human review before opening: please review it as you would any contributor's PR.

TL;DR

  • Fixes: zmc reads freed memory in MonitorLinkExpression::visit after a zmu reload changes LinkedMonitors #5271: zmc reads freed memory in MonitorLinkExpression::visit after a zmu -L reload changes LinkedMonitors.
  • Change: MonitorLinkExpression copies the expression into a const std::string member and points its string_view (and so every token) at that copy; copying the object is deleted so the views can't follow a moved string; 3 files, +50/-1
  • Risk: parsing and evaluation are unchanged (same characters, same views); Token is untouched. The events check's linked-monitor scenario passed.
  • Tested: ASan heap-use-after-free before, none after (zmc keeps running); new unit test fails before (on a plain build too) and passes after; checks covering the diff green
Details: reproduction, change, checks

Before

Upstream master 233a15828, clean ASan build ("head": "233a1582829edc49554a0f1a9bc36d61dd951aaf", "branch": "master", "dirty": false):

zmc -m 2 pid after: 1679 (was 1573)
==1573==ERROR: AddressSanitizer: heap-use-after-free on address 0x503000045c70 at pc 0x55a882ce9472 bp 0x7f432eb7e7b0 sp 0x7f432eb7e7a8
    #7 0x55a882ce9f96 in MonitorLinkExpression::visit(MonitorLinkExpression::Node const&) /src/src/zm_monitorlink_expression.cpp:76

New unit test on master's code (plain build): the freed bytes were parsed as a monitor id:

terminate called after throwing an instance of 'std::invalid_argument'
  what():  stoul
/src/tests/zm_monitorlink_expression.cpp:31: FAILED:
due to a fatal error condition:
  SIGABRT - Abort (abnormal termination) signal

Change

  • src/zm_monitorlink_expression.h: new member const std::string expression_string_, declared
    before expression_ and initialised from the constructor argument; expression_ views it.
    Copy constructor and assignment deleted (this also suppresses the implicit move, which would
    leave the views pointing into the moved-from string); the class is only ever created with new.
  • Test: tests/zm_monitorlink_expression.cpp (new, added to tests/CMakeLists.txt): builds an
    expression from a heap-sized string of delimiters (no monitor ids, so no database), reassigns
    and destroys the string, then parse() and result(). Fails before (abort or ASan report),
    passes after.

Behaviour changes

Change Who sees it Intended / collateral Alternatives looked into Chosen, why Tested by
The expression no longer reads freed memory after a zmu -L reload zmc with LinkedMonitors at debug level >= 1 intended re-parse in Reload() (would also apply the new LinkedMonitors at once, but deletes the expression the analysis thread may be evaluating: a new use-after-free unless Reload and Analyse are synchronised); std::string in Token (drops the constexpr members, the -Winvalid-constexpr problem #5219 met) own copy in the expression: no thread or constexpr change ASan probe, unit test
After zmu -L, the old linked monitors stay in use until the next reconnect (as before) users of LinkedMonitors no change left as is, noted in the issue ASan probe log: the tokens still print the old values

Side fixes in this PR: none.

After

Branch fix/b81-monitorlink-own-expression at 2f7ace2c2 (base 233a15828), tested in a merge with
four other fixes from the same lab session (none touches linked monitors):

"head": "4756d41976f5fa95dcb5f2ca318aca0f14063941", "branch": "combo", "dirty": false (ASan)
zmc -m 2 pid 656; asan files before: 1      (the one file is master's asan.1573 from the run above)
zmu rc=0
zmc -m 2 pid after: 656 (was 656)
no asan file for pid 656
... 'Received reload indication at count 173' ...
zmc_m2[689].DB1-zm_monitorlink_expression.cpp/76 [visit: Node: 0x506000006680 Token: 1 value 3]
Check Result
holodeck checks for the diff (check-changed.py on the plain -Werror combo 505d0a5ff, which had this fix as c9a7f7be3; 2f7ace2c2 only moves the tests/CMakeLists.txt line) unit-tests ok (All tests passed (14899 assertions in 263 test cases)), script-tests PASS=32, smoke PASS=11 XPASS=1 (stock zmonvif-probe.pl discovery), resilience PASS=5, events PASS=10 (incl. linked), dblog --quick PASS=1 XFAIL=2 (known on master, #5197/#5208), zmscmd --quick PASS=4 XFAIL=1 XPASS=3 (XFAIL speed = B75/#5229; XPASS = B66/B73/B74 fixed on master)
this branch alone, -Werror build + unit tests built (/usr/share/zm-holodeck/werror), All tests passed (14778 assertions in 260 test cases)
pyzmNg 75a68dd / ha-zoneminder 69dc880 / zmNinjaNg 3f90042 61 passed, 4 skipped, 1 deselected / 20 passed, 1 skipped (flaky test_monitor_entities_match_server passed on re-run) / passed=39
contract baselines (master) not run: no PHP/API change

Not run: the deep set. Backport to release-1.38: same code (zm_monitorlink_expression.h:94,102 on 1a830d7d1), read; the patch should apply (not built).

Tested as a998c6235; 2f7ace2c2 is the same tree with fixes #5271 added to the message.

fixes #5271

🤖 Generated with Claude Code

MonitorLinkExpression kept a string_view of the string it was built
from, Monitor::linked_monitors_string, and the tokens parse() builds are
views into it. Monitor::Reload() calls Load(), which reassigns that
string without re-parsing the expression, so once the new value no
longer fits the old buffer every token points into freed memory. They
are read by the Debug(1, ...) calls in MonitorLinkExpression::visit(),
which runs on every analysed frame of a monitor with LinkedMonitors set
when its debug level is 1 or more.

Copy the string into the expression and point the views at the copy.

fixes ZoneMinder#5271

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@nabbi nabbi changed the title fix: keep the linked-monitor expression's own copy of its string fix-medium: keep the linked-monitor expression's own copy of its string Oct 12, 2026

This branch has not been deployed

No deployments
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.

zmc reads freed memory in MonitorLinkExpression::visit after a zmu reload changes LinkedMonitors

1 participant