fix: prevent crash from a trigger that endlessly feeds itself (#9368)
#### Brief overview of PR changes/additions
Abort with a Lua error (naming the trigger) when trigger processing
recurses past a depth limit, instead of overflowing the stack. Covers
both `feedTriggers()` and `feedTelnet()` loops, each with a limit sized
to its stack usage.
#### Motivation for adding to Mudlet
A trigger whose action calls `feedTriggers()` with text that re-matches
it recurses the C++ stack until Mudlet hard-crashes (Sentry `fbda193d`,
`EXCEPTION_STACK_OVERFLOW`); this turns that into a clear, recoverable
script error. Review found `feedTelnet()` crashes the same way, so it
gets the same treatment.
#### Other info (issues closed, discussion etc)
Crash seen on Windows in Sentry. The same loop on Linux/macOS only
reaches Lua's own `C stack overflow` guard (~200 nested C calls) -
cryptic, but no crash; Windows' smaller ~1 MB stack dies first. The
fix's depth limit (50) trips before either, so every platform gets the
same clear, named error.
`feedTelnet()` needs a much lower limit (5): each nested telnet
processing level holds ~100KB of stack buffers, so a 1MB stack dies
after ~8 levels. Fixing this also surfaced a reentrancy bug where nested
`feedTelnet()` re-posted the ancestors' pending `mMudData`, duplicating
output lines and abort errors; `postData()` now detaches the data before
posting.
**Test case:**
1. New regex trigger: pattern `^loopme$`, script
`feedTriggers("loopme\n")`.
2. Run `feedTriggers("loopme\n")`.
3. Before: crash (Windows) / `<C stack overflow>` (Linux). After: it
stops with an error naming the trigger and stays running.
4. Same with `feedTelnet("loopme\n")` in the trigger and prompt (on a
disconnected profile): stops with a single error instead of crashing.
---------
Signed-off-by: Vadim Peretokin <vperetokin@hey.com>
2026-07-13 20:14:21 +02:00
/***************************************************************************
* Copyright ( C ) 2026 by Vadim Peretokin - vadim . peretokin @ mudlet . org *
* *
* This program is free software ; you can redistribute it and / or modify *
* it under the terms of the GNU General Public License as published by *
* the Free Software Foundation ; either version 2 of the License , or *
* ( at your option ) any later version . *
* *
* This program is distributed in the hope that it will be useful , *
* but WITHOUT ANY WARRANTY ; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE . See the *
* GNU General Public License for more details . *
* *
* You should have received a copy of the GNU General Public License *
* along with this program ; if not , write to the *
* Free Software Foundation , Inc . , *
* 59 Temple Place - Suite 330 , Boston , MA 02111 - 1307 , USA . *
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
# include <QtTest/QtTest>
infrastructure: use std::chrono literals for time durations (#9493)
#### Brief overview of PR changes/additions
Convert raw millisecond integer literals at time-duration call sites to
`std::chrono` literals, and add `#include <chrono>` to each touched
translation unit. Examples:
- `QTimer::singleShot(0, ...)` → `QTimer::singleShot(0ms, ...)`
- `mpTimerReplay->setInterval(1000)` → `setInterval(1s)`
- `mPendingTimer.start(60000)` → `start(1min)`
- `QObject::startTimer(50)` → `startTimer(50ms)`
- `QTest::qWait(100)` → `QTest::qWait(100ms)`
- `QThread::msleep(10)` → `QThread::sleep(10ms)`
This is a semantics-preserving refactor - every duration is kept exactly
equal to before (e.g. `1000` ms becomes `1s`, `60000` ms becomes
`1min`). No behavioural change.
#### Motivation for adding to Mudlet
Chrono literals make time durations self-documenting and type-safe. `1s`
/ `100ms` read unambiguously where a bare `1000` / `100` forces the
reader to remember each API's unit, and the compiler now rejects unit
mismatches. Only genuine duration arguments were converted - loop
counts, scroll-line counts, sizes, ports and the like were deliberately
left as plain integers.
All targeted APIs provide `std::chrono` overloads in the minimum
supported Qt (6.8.2): `QTimer::singleShot`/`start`/`setInterval` (5.8),
`QObject::startTimer` (5.9), `QThread::sleep(std::chrono::nanoseconds)`
(6.6) and `QTest::qWait(std::chrono::milliseconds)` (6.7).
#### Other info (issues closed, discussion etc)
Test case: the full application builds cleanly and the entire functional
`ctest` suite passes. The only failing test is the known, pre-existing
`PasswordMigrationTest` LSan exit-leak (GTK3/fontconfig noise), which is
unrelated to this change.
Assisted-by: Claude:claude-opus-4-8
2026-07-25 20:24:31 +02:00
# include <chrono>
fix: prevent crash from a trigger that endlessly feeds itself (#9368)
#### Brief overview of PR changes/additions
Abort with a Lua error (naming the trigger) when trigger processing
recurses past a depth limit, instead of overflowing the stack. Covers
both `feedTriggers()` and `feedTelnet()` loops, each with a limit sized
to its stack usage.
#### Motivation for adding to Mudlet
A trigger whose action calls `feedTriggers()` with text that re-matches
it recurses the C++ stack until Mudlet hard-crashes (Sentry `fbda193d`,
`EXCEPTION_STACK_OVERFLOW`); this turns that into a clear, recoverable
script error. Review found `feedTelnet()` crashes the same way, so it
gets the same treatment.
#### Other info (issues closed, discussion etc)
Crash seen on Windows in Sentry. The same loop on Linux/macOS only
reaches Lua's own `C stack overflow` guard (~200 nested C calls) -
cryptic, but no crash; Windows' smaller ~1 MB stack dies first. The
fix's depth limit (50) trips before either, so every platform gets the
same clear, named error.
`feedTelnet()` needs a much lower limit (5): each nested telnet
processing level holds ~100KB of stack buffers, so a 1MB stack dies
after ~8 levels. Fixing this also surfaced a reentrancy bug where nested
`feedTelnet()` re-posted the ancestors' pending `mMudData`, duplicating
output lines and abort errors; `postData()` now detaches the data before
posting.
**Test case:**
1. New regex trigger: pattern `^loopme$`, script
`feedTriggers("loopme\n")`.
2. Run `feedTriggers("loopme\n")`.
3. Before: crash (Windows) / `<C stack overflow>` (Linux). After: it
stops with an error naming the trigger and stays running.
4. Same with `feedTelnet("loopme\n")` in the trigger and prompt (on a
disconnected profile): stops with a single error instead of crashing.
---------
Signed-off-by: Vadim Peretokin <vperetokin@hey.com>
2026-07-13 20:14:21 +02:00
# include "Host.h"
# include "MudletInstanceCoordinator.h"
# include "TLuaInterpreter.h"
# include "TMainConsole.h"
# include "TriggerUnit.h"
# include "TelnetServerStub.h"
# include "ctelnet.h"
# include "dlgConnectionProfiles.h"
# include "mudlet.h"
infrastructure: use std::chrono literals for time durations (#9493)
#### Brief overview of PR changes/additions
Convert raw millisecond integer literals at time-duration call sites to
`std::chrono` literals, and add `#include <chrono>` to each touched
translation unit. Examples:
- `QTimer::singleShot(0, ...)` → `QTimer::singleShot(0ms, ...)`
- `mpTimerReplay->setInterval(1000)` → `setInterval(1s)`
- `mPendingTimer.start(60000)` → `start(1min)`
- `QObject::startTimer(50)` → `startTimer(50ms)`
- `QTest::qWait(100)` → `QTest::qWait(100ms)`
- `QThread::msleep(10)` → `QThread::sleep(10ms)`
This is a semantics-preserving refactor - every duration is kept exactly
equal to before (e.g. `1000` ms becomes `1s`, `60000` ms becomes
`1min`). No behavioural change.
#### Motivation for adding to Mudlet
Chrono literals make time durations self-documenting and type-safe. `1s`
/ `100ms` read unambiguously where a bare `1000` / `100` forces the
reader to remember each API's unit, and the compiler now rejects unit
mismatches. Only genuine duration arguments were converted - loop
counts, scroll-line counts, sizes, ports and the like were deliberately
left as plain integers.
All targeted APIs provide `std::chrono` overloads in the minimum
supported Qt (6.8.2): `QTimer::singleShot`/`start`/`setInterval` (5.8),
`QObject::startTimer` (5.9), `QThread::sleep(std::chrono::nanoseconds)`
(6.6) and `QTest::qWait(std::chrono::milliseconds)` (6.7).
#### Other info (issues closed, discussion etc)
Test case: the full application builds cleanly and the entire functional
`ctest` suite passes. The only failing test is the known, pre-existing
`PasswordMigrationTest` LSan exit-leak (GTK3/fontconfig noise), which is
unrelated to this change.
Assisted-by: Claude:claude-opus-4-8
2026-07-25 20:24:31 +02:00
using namespace std : : chrono_literals ;
fix: prevent crash from a trigger that endlessly feeds itself (#9368)
#### Brief overview of PR changes/additions
Abort with a Lua error (naming the trigger) when trigger processing
recurses past a depth limit, instead of overflowing the stack. Covers
both `feedTriggers()` and `feedTelnet()` loops, each with a limit sized
to its stack usage.
#### Motivation for adding to Mudlet
A trigger whose action calls `feedTriggers()` with text that re-matches
it recurses the C++ stack until Mudlet hard-crashes (Sentry `fbda193d`,
`EXCEPTION_STACK_OVERFLOW`); this turns that into a clear, recoverable
script error. Review found `feedTelnet()` crashes the same way, so it
gets the same treatment.
#### Other info (issues closed, discussion etc)
Crash seen on Windows in Sentry. The same loop on Linux/macOS only
reaches Lua's own `C stack overflow` guard (~200 nested C calls) -
cryptic, but no crash; Windows' smaller ~1 MB stack dies first. The
fix's depth limit (50) trips before either, so every platform gets the
same clear, named error.
`feedTelnet()` needs a much lower limit (5): each nested telnet
processing level holds ~100KB of stack buffers, so a 1MB stack dies
after ~8 levels. Fixing this also surfaced a reentrancy bug where nested
`feedTelnet()` re-posted the ancestors' pending `mMudData`, duplicating
output lines and abort errors; `postData()` now detaches the data before
posting.
**Test case:**
1. New regex trigger: pattern `^loopme$`, script
`feedTriggers("loopme\n")`.
2. Run `feedTriggers("loopme\n")`.
3. Before: crash (Windows) / `<C stack overflow>` (Linux). After: it
stops with an error naming the trigger and stays running.
4. Same with `feedTelnet("loopme\n")` in the trigger and prompt (on a
disconnected profile): stops with a single error instead of crashing.
---------
Signed-off-by: Vadim Peretokin <vperetokin@hey.com>
2026-07-13 20:14:21 +02:00
extern void qInitResources_mudlet ( ) ;
extern void qInitResources_qm ( ) ;
extern void qInitResources_additional_splash_screens ( ) ;
extern void qInitResources_mudlet_fonts_common ( ) ;
extern void qInitResources_mudlet_fonts_posix ( ) ;
void initializeQRCResources ( ) ;
// Validates the guards that stop a self-feeding trigger (one whose action calls
// feedTriggers() or feedTelnet() with text that re-matches it) from recursing the
// C++ stack into an EXCEPTION_STACK_OVERFLOW crash - see Sentry event fbda193d.
// The guards must abort the loop with a catchable Lua error while leaving
// legitimate feedTriggers()/feedTelnet() use untouched.
class TFeedTriggersRecursionTest : public QObject
{
Q_OBJECT
private :
TelnetServerStub * mpServer = nullptr ;
const QString mpHostname = " Test-FeedTriggersRecursion " ;
2026-07-20 09:01:51 +02:00
QString mpPort ; // assigned the stub's actual ephemeral port in init()
fix: prevent crash from a trigger that endlessly feeds itself (#9368)
#### Brief overview of PR changes/additions
Abort with a Lua error (naming the trigger) when trigger processing
recurses past a depth limit, instead of overflowing the stack. Covers
both `feedTriggers()` and `feedTelnet()` loops, each with a limit sized
to its stack usage.
#### Motivation for adding to Mudlet
A trigger whose action calls `feedTriggers()` with text that re-matches
it recurses the C++ stack until Mudlet hard-crashes (Sentry `fbda193d`,
`EXCEPTION_STACK_OVERFLOW`); this turns that into a clear, recoverable
script error. Review found `feedTelnet()` crashes the same way, so it
gets the same treatment.
#### Other info (issues closed, discussion etc)
Crash seen on Windows in Sentry. The same loop on Linux/macOS only
reaches Lua's own `C stack overflow` guard (~200 nested C calls) -
cryptic, but no crash; Windows' smaller ~1 MB stack dies first. The
fix's depth limit (50) trips before either, so every platform gets the
same clear, named error.
`feedTelnet()` needs a much lower limit (5): each nested telnet
processing level holds ~100KB of stack buffers, so a 1MB stack dies
after ~8 levels. Fixing this also surfaced a reentrancy bug where nested
`feedTelnet()` re-posted the ancestors' pending `mMudData`, duplicating
output lines and abort errors; `postData()` now detaches the data before
posting.
**Test case:**
1. New regex trigger: pattern `^loopme$`, script
`feedTriggers("loopme\n")`.
2. Run `feedTriggers("loopme\n")`.
3. Before: crash (Windows) / `<C stack overflow>` (Linux). After: it
stops with an error naming the trigger and stays running.
4. Same with `feedTelnet("loopme\n")` in the trigger and prompt (on a
disconnected profile): stops with a single error instead of crashing.
---------
Signed-off-by: Vadim Peretokin <vperetokin@hey.com>
2026-07-13 20:14:21 +02:00
const QString mpLocalhost = " localhost " ;
private slots :
void initTestCase ( ) { initializeQRCResources ( ) ; }
void init ( )
{
mpServer = new TelnetServerStub ( qApp ) ;
2026-07-20 09:01:51 +02:00
mpServer - > start ( mpLocalhost , 0 ) ; // ephemeral OS-assigned port avoids collisions across concurrent test runs
mpPort = QString : : number ( mpServer - > serverPort ( ) ) ;
fix: prevent crash from a trigger that endlessly feeds itself (#9368)
#### Brief overview of PR changes/additions
Abort with a Lua error (naming the trigger) when trigger processing
recurses past a depth limit, instead of overflowing the stack. Covers
both `feedTriggers()` and `feedTelnet()` loops, each with a limit sized
to its stack usage.
#### Motivation for adding to Mudlet
A trigger whose action calls `feedTriggers()` with text that re-matches
it recurses the C++ stack until Mudlet hard-crashes (Sentry `fbda193d`,
`EXCEPTION_STACK_OVERFLOW`); this turns that into a clear, recoverable
script error. Review found `feedTelnet()` crashes the same way, so it
gets the same treatment.
#### Other info (issues closed, discussion etc)
Crash seen on Windows in Sentry. The same loop on Linux/macOS only
reaches Lua's own `C stack overflow` guard (~200 nested C calls) -
cryptic, but no crash; Windows' smaller ~1 MB stack dies first. The
fix's depth limit (50) trips before either, so every platform gets the
same clear, named error.
`feedTelnet()` needs a much lower limit (5): each nested telnet
processing level holds ~100KB of stack buffers, so a 1MB stack dies
after ~8 levels. Fixing this also surfaced a reentrancy bug where nested
`feedTelnet()` re-posted the ancestors' pending `mMudData`, duplicating
output lines and abort errors; `postData()` now detaches the data before
posting.
**Test case:**
1. New regex trigger: pattern `^loopme$`, script
`feedTriggers("loopme\n")`.
2. Run `feedTriggers("loopme\n")`.
3. Before: crash (Windows) / `<C stack overflow>` (Linux). After: it
stops with an error naming the trigger and stays running.
4. Same with `feedTelnet("loopme\n")` in the trigger and prompt (on a
disconnected profile): stops with a single error instead of crashing.
---------
Signed-off-by: Vadim Peretokin <vperetokin@hey.com>
2026-07-13 20:14:21 +02:00
mudlet : : start ( ) ;
mudlet : : self ( ) - > setupConfig ( ) ;
mudlet : : self ( ) - > takeOwnershipOfInstanceCoordinator ( std : : make_unique < MudletInstanceCoordinator > ( " MudletInstanceCoordinator " ) ) ;
mudlet : : self ( ) - > init ( ) ;
mudlet : : self ( ) - > setStorePasswordsSecurely ( false ) ;
deleteProfileDirectory ( mpHostname ) ;
}
// A trigger that feeds itself must be stopped at the depth limit with a
// catchable Lua error, not crash the process via stack overflow.
void test_selfFeedingTriggerIsStopped ( )
{
startProfile ( mpHostname , mpLocalhost , mpPort ) ;
auto * host = mudlet : : self ( ) - > getActiveHost ( ) ;
infrastructure: guard functional-test lookups with QVERIFY (#9524)
#### Brief overview of PR changes/additions
The 12 functional-test sites that dereferenced the result of
`getActiveHost()`, `getArea()`, `getRoom()` or `getHost()` without first
checking it for null now guard the pointer with `QVERIFY(ptr)` before
use. The other lookup sites this PR touched already had a `QVERIFY` and
are left as they were.
#### Motivation for adding to Mudlet
CodeQL's `cpp/inconsistent-null-check` rule flagged these lookups
because some call sites checked the returned pointer for null while
others in the same files dereferenced it directly. Adding `QVERIFY(ptr)`
at the previously-unchecked sites makes the handling consistent: every
lookup result is verified before it is dereferenced, so a null result
fails the test loudly instead of crashing.
`QVERIFY` is the project's accepted idiom for guarding a pointer in a
test, so it is used here rather than an `if (!ptr) { QFAIL(...); }`
block. CodeQL does not recognise `QVERIFY(ptr)` as a null check (the
branch it generates tests `qVerify()`'s return value, not the pointer
itself), so it will keep reporting `cpp/inconsistent-null-check` on
these sites and on the pre-existing `QVERIFY` sites. Those alerts will
be dismissed as false positives rather than changing the test style to
satisfy the checker.
#### Other info (issues closed, discussion etc)
Test files only - no production code is touched. The 12 newly-guarded
sites are:
- `MapRoundTripTest.cpp` - 2 (`pAreaA`, `pAreaB`)
- `TriggerSameLineMatchTest.cpp` - 6 (`host`)
- `TFeedTriggersRecursionTest.cpp` - 4 (`host`)
**Test case:**
Build and run the five affected binaries under the functional-test flock
- all pass:
- `TFeedTriggersRecursionTest` - passed
- `TriggerSameLineMatchTest` - passed
- `MapRoundTripTest` - 6 passed, 0 failed
- `UndoServerWrapTest` - passed
- `UndoServerWrapReplay` - 2 passed, 1 skipped (manual replay tool;
skips without `REPLAY_CAPTURE`/`REPLAY_OUT`)
2026-07-29 13:42:04 +02:00
QVERIFY ( host ) ;
fix: prevent crash from a trigger that endlessly feeds itself (#9368)
#### Brief overview of PR changes/additions
Abort with a Lua error (naming the trigger) when trigger processing
recurses past a depth limit, instead of overflowing the stack. Covers
both `feedTriggers()` and `feedTelnet()` loops, each with a limit sized
to its stack usage.
#### Motivation for adding to Mudlet
A trigger whose action calls `feedTriggers()` with text that re-matches
it recurses the C++ stack until Mudlet hard-crashes (Sentry `fbda193d`,
`EXCEPTION_STACK_OVERFLOW`); this turns that into a clear, recoverable
script error. Review found `feedTelnet()` crashes the same way, so it
gets the same treatment.
#### Other info (issues closed, discussion etc)
Crash seen on Windows in Sentry. The same loop on Linux/macOS only
reaches Lua's own `C stack overflow` guard (~200 nested C calls) -
cryptic, but no crash; Windows' smaller ~1 MB stack dies first. The
fix's depth limit (50) trips before either, so every platform gets the
same clear, named error.
`feedTelnet()` needs a much lower limit (5): each nested telnet
processing level holds ~100KB of stack buffers, so a 1MB stack dies
after ~8 levels. Fixing this also surfaced a reentrancy bug where nested
`feedTelnet()` re-posted the ancestors' pending `mMudData`, duplicating
output lines and abort errors; `postData()` now detaches the data before
posting.
**Test case:**
1. New regex trigger: pattern `^loopme$`, script
`feedTriggers("loopme\n")`.
2. Run `feedTriggers("loopme\n")`.
3. Before: crash (Windows) / `<C stack overflow>` (Linux). After: it
stops with an error naming the trigger and stays running.
4. Same with `feedTelnet("loopme\n")` in the trigger and prompt (on a
disconnected profile): stops with a single error instead of crashing.
---------
Signed-off-by: Vadim Peretokin <vperetokin@hey.com>
2026-07-13 20:14:21 +02:00
host - > mEchoLuaErrors = true ;
host - > getLuaInterpreter ( ) - > compileAndExecuteScript ( qsl ( " loopCount = 0 \n "
" loopTriggerId = tempRegexTrigger('^loopme$', [[loopCount = loopCount + 1; feedTriggers('loopme \\ n')]]) \n "
" feedTriggers('loopme \\ n') \n " ) ) ;
// The whole recursion runs synchronously inside the call above; if the
// guard works we are back here (no crash) with everything unwound.
QCOMPARE ( host - > getTriggerUnit ( ) - > processingDepth ( ) , 0 ) ;
QVERIFY2 ( bufferContains ( qsl ( " stuck in an endless loop " ) ) , " Expected the feedTriggers loop-abort error in the console buffer " ) ;
// The abort message must name the offending trigger (a temp trigger's name
// is its id), proving the name-tracking guard actually identifies the culprit
// rather than silently falling back to the unnamed branch.
lua_State * L = host - > getLuaInterpreter ( ) - > getLuaGlobalState ( ) ;
lua_getglobal ( L , " loopTriggerId " ) ;
const int loopTriggerId = static_cast < int > ( lua_tointeger ( L , - 1 ) ) ;
lua_pop ( L , 1 ) ;
QVERIFY2 ( bufferContains ( qsl ( " trigger '%1' " ) . arg ( loopTriggerId ) ) , " Expected the abort message to name the offending trigger by its id " ) ;
host - > getLuaInterpreter ( ) - > compileAndExecuteScript ( qsl ( " echo('LOOPCOUNT='..loopCount..' \\ n') " ) ) ;
QVERIFY2 ( bufferContains ( qsl ( " LOOPCOUNT=%1 " ) . arg ( TriggerUnit : : scmMaxProcessingDepth ) ) , qPrintable ( qsl ( " Expected the trigger to fire exactly %1 times " ) . arg ( TriggerUnit : : scmMaxProcessingDepth ) ) ) ;
}
// A single, non-self-matching feedTriggers() must still work normally and not
// be flagged as a loop.
void test_normalFeedTriggersIsUnaffected ( )
{
startProfile ( mpHostname , mpLocalhost , mpPort ) ;
auto * host = mudlet : : self ( ) - > getActiveHost ( ) ;
infrastructure: guard functional-test lookups with QVERIFY (#9524)
#### Brief overview of PR changes/additions
The 12 functional-test sites that dereferenced the result of
`getActiveHost()`, `getArea()`, `getRoom()` or `getHost()` without first
checking it for null now guard the pointer with `QVERIFY(ptr)` before
use. The other lookup sites this PR touched already had a `QVERIFY` and
are left as they were.
#### Motivation for adding to Mudlet
CodeQL's `cpp/inconsistent-null-check` rule flagged these lookups
because some call sites checked the returned pointer for null while
others in the same files dereferenced it directly. Adding `QVERIFY(ptr)`
at the previously-unchecked sites makes the handling consistent: every
lookup result is verified before it is dereferenced, so a null result
fails the test loudly instead of crashing.
`QVERIFY` is the project's accepted idiom for guarding a pointer in a
test, so it is used here rather than an `if (!ptr) { QFAIL(...); }`
block. CodeQL does not recognise `QVERIFY(ptr)` as a null check (the
branch it generates tests `qVerify()`'s return value, not the pointer
itself), so it will keep reporting `cpp/inconsistent-null-check` on
these sites and on the pre-existing `QVERIFY` sites. Those alerts will
be dismissed as false positives rather than changing the test style to
satisfy the checker.
#### Other info (issues closed, discussion etc)
Test files only - no production code is touched. The 12 newly-guarded
sites are:
- `MapRoundTripTest.cpp` - 2 (`pAreaA`, `pAreaB`)
- `TriggerSameLineMatchTest.cpp` - 6 (`host`)
- `TFeedTriggersRecursionTest.cpp` - 4 (`host`)
**Test case:**
Build and run the five affected binaries under the functional-test flock
- all pass:
- `TFeedTriggersRecursionTest` - passed
- `TriggerSameLineMatchTest` - passed
- `MapRoundTripTest` - 6 passed, 0 failed
- `UndoServerWrapTest` - passed
- `UndoServerWrapReplay` - 2 passed, 1 skipped (manual replay tool;
skips without `REPLAY_CAPTURE`/`REPLAY_OUT`)
2026-07-29 13:42:04 +02:00
QVERIFY ( host ) ;
fix: prevent crash from a trigger that endlessly feeds itself (#9368)
#### Brief overview of PR changes/additions
Abort with a Lua error (naming the trigger) when trigger processing
recurses past a depth limit, instead of overflowing the stack. Covers
both `feedTriggers()` and `feedTelnet()` loops, each with a limit sized
to its stack usage.
#### Motivation for adding to Mudlet
A trigger whose action calls `feedTriggers()` with text that re-matches
it recurses the C++ stack until Mudlet hard-crashes (Sentry `fbda193d`,
`EXCEPTION_STACK_OVERFLOW`); this turns that into a clear, recoverable
script error. Review found `feedTelnet()` crashes the same way, so it
gets the same treatment.
#### Other info (issues closed, discussion etc)
Crash seen on Windows in Sentry. The same loop on Linux/macOS only
reaches Lua's own `C stack overflow` guard (~200 nested C calls) -
cryptic, but no crash; Windows' smaller ~1 MB stack dies first. The
fix's depth limit (50) trips before either, so every platform gets the
same clear, named error.
`feedTelnet()` needs a much lower limit (5): each nested telnet
processing level holds ~100KB of stack buffers, so a 1MB stack dies
after ~8 levels. Fixing this also surfaced a reentrancy bug where nested
`feedTelnet()` re-posted the ancestors' pending `mMudData`, duplicating
output lines and abort errors; `postData()` now detaches the data before
posting.
**Test case:**
1. New regex trigger: pattern `^loopme$`, script
`feedTriggers("loopme\n")`.
2. Run `feedTriggers("loopme\n")`.
3. Before: crash (Windows) / `<C stack overflow>` (Linux). After: it
stops with an error naming the trigger and stays running.
4. Same with `feedTelnet("loopme\n")` in the trigger and prompt (on a
disconnected profile): stops with a single error instead of crashing.
---------
Signed-off-by: Vadim Peretokin <vperetokin@hey.com>
2026-07-13 20:14:21 +02:00
host - > mEchoLuaErrors = true ;
host - > getLuaInterpreter ( ) - > compileAndExecuteScript ( qsl ( " normalCount = 0 \n "
" tempRegexTrigger('^hello$', [[normalCount = normalCount + 1]]) \n "
" feedTriggers('hello \\ n') \n "
" echo('NORMALCOUNT='..normalCount..' \\ n') \n " ) ) ;
QCOMPARE ( host - > getTriggerUnit ( ) - > processingDepth ( ) , 0 ) ;
QVERIFY2 ( ! bufferContains ( qsl ( " stuck in an endless loop " ) ) , " A normal feedTriggers() call must not be treated as a loop " ) ;
QVERIFY2 ( bufferContains ( qsl ( " NORMALCOUNT=1 " ) ) , " Expected the non-looping trigger to fire exactly once " ) ;
}
// A trigger that re-feeds its own output through feedTelnet() must likewise be
// stopped - at a much lower depth limit, as each nested telnet processing frame
// holds ~100KB of stack, overflowing a 1MB (Windows) stack in only ~8 levels.
void test_selfFeedingTelnetTriggerIsStopped ( )
{
startProfile ( mpHostname , mpLocalhost , mpPort ) ;
auto * host = mudlet : : self ( ) - > getActiveHost ( ) ;
infrastructure: guard functional-test lookups with QVERIFY (#9524)
#### Brief overview of PR changes/additions
The 12 functional-test sites that dereferenced the result of
`getActiveHost()`, `getArea()`, `getRoom()` or `getHost()` without first
checking it for null now guard the pointer with `QVERIFY(ptr)` before
use. The other lookup sites this PR touched already had a `QVERIFY` and
are left as they were.
#### Motivation for adding to Mudlet
CodeQL's `cpp/inconsistent-null-check` rule flagged these lookups
because some call sites checked the returned pointer for null while
others in the same files dereferenced it directly. Adding `QVERIFY(ptr)`
at the previously-unchecked sites makes the handling consistent: every
lookup result is verified before it is dereferenced, so a null result
fails the test loudly instead of crashing.
`QVERIFY` is the project's accepted idiom for guarding a pointer in a
test, so it is used here rather than an `if (!ptr) { QFAIL(...); }`
block. CodeQL does not recognise `QVERIFY(ptr)` as a null check (the
branch it generates tests `qVerify()`'s return value, not the pointer
itself), so it will keep reporting `cpp/inconsistent-null-check` on
these sites and on the pre-existing `QVERIFY` sites. Those alerts will
be dismissed as false positives rather than changing the test style to
satisfy the checker.
#### Other info (issues closed, discussion etc)
Test files only - no production code is touched. The 12 newly-guarded
sites are:
- `MapRoundTripTest.cpp` - 2 (`pAreaA`, `pAreaB`)
- `TriggerSameLineMatchTest.cpp` - 6 (`host`)
- `TFeedTriggersRecursionTest.cpp` - 4 (`host`)
**Test case:**
Build and run the five affected binaries under the functional-test flock
- all pass:
- `TFeedTriggersRecursionTest` - passed
- `TriggerSameLineMatchTest` - passed
- `MapRoundTripTest` - 6 passed, 0 failed
- `UndoServerWrapTest` - passed
- `UndoServerWrapReplay` - 2 passed, 1 skipped (manual replay tool;
skips without `REPLAY_CAPTURE`/`REPLAY_OUT`)
2026-07-29 13:42:04 +02:00
QVERIFY ( host ) ;
fix: prevent crash from a trigger that endlessly feeds itself (#9368)
#### Brief overview of PR changes/additions
Abort with a Lua error (naming the trigger) when trigger processing
recurses past a depth limit, instead of overflowing the stack. Covers
both `feedTriggers()` and `feedTelnet()` loops, each with a limit sized
to its stack usage.
#### Motivation for adding to Mudlet
A trigger whose action calls `feedTriggers()` with text that re-matches
it recurses the C++ stack until Mudlet hard-crashes (Sentry `fbda193d`,
`EXCEPTION_STACK_OVERFLOW`); this turns that into a clear, recoverable
script error. Review found `feedTelnet()` crashes the same way, so it
gets the same treatment.
#### Other info (issues closed, discussion etc)
Crash seen on Windows in Sentry. The same loop on Linux/macOS only
reaches Lua's own `C stack overflow` guard (~200 nested C calls) -
cryptic, but no crash; Windows' smaller ~1 MB stack dies first. The
fix's depth limit (50) trips before either, so every platform gets the
same clear, named error.
`feedTelnet()` needs a much lower limit (5): each nested telnet
processing level holds ~100KB of stack buffers, so a 1MB stack dies
after ~8 levels. Fixing this also surfaced a reentrancy bug where nested
`feedTelnet()` re-posted the ancestors' pending `mMudData`, duplicating
output lines and abort errors; `postData()` now detaches the data before
posting.
**Test case:**
1. New regex trigger: pattern `^loopme$`, script
`feedTriggers("loopme\n")`.
2. Run `feedTriggers("loopme\n")`.
3. Before: crash (Windows) / `<C stack overflow>` (Linux). After: it
stops with an error naming the trigger and stays running.
4. Same with `feedTelnet("loopme\n")` in the trigger and prompt (on a
disconnected profile): stops with a single error instead of crashing.
---------
Signed-off-by: Vadim Peretokin <vperetokin@hey.com>
2026-07-13 20:14:21 +02:00
host - > mEchoLuaErrors = true ;
// feedTelnet() refuses to work unless the profile is offline
host - > mTelnet . disconnectIt ( ) ;
QTRY_COMPARE ( host - > mTelnet . getConnectionState ( ) , QAbstractSocket : : UnconnectedState ) ;
host - > getLuaInterpreter ( ) - > compileAndExecuteScript ( qsl ( " telnetLoopCount = 0 \n "
" telnetLoopTriggerId = tempRegexTrigger('^loopme$', [[telnetLoopCount = telnetLoopCount + 1; feedTelnet('loopme \\ n')]]) \n "
" feedTelnet('loopme \\ n') \n " ) ) ;
QCOMPARE ( host - > mTelnet . loopbackProcessingDepth ( ) , 0 ) ;
QCOMPARE ( host - > getTriggerUnit ( ) - > processingDepth ( ) , 0 ) ;
QVERIFY2 ( bufferContains ( qsl ( " feedTelnet stopped to prevent a crash " ) ) , " Expected the feedTelnet loop-abort error in the console buffer " ) ;
lua_State * L = host - > getLuaInterpreter ( ) - > getLuaGlobalState ( ) ;
lua_getglobal ( L , " telnetLoopTriggerId " ) ;
const int telnetLoopTriggerId = static_cast < int > ( lua_tointeger ( L , - 1 ) ) ;
lua_pop ( L , 1 ) ;
QVERIFY2 ( bufferContains ( qsl ( " trigger '%1' " ) . arg ( telnetLoopTriggerId ) ) , " Expected the abort message to name the offending trigger by its id " ) ;
// Exactly one abort for the whole loop: postData() detaches the pending data
// before posting, so the unwinding ancestor frames must not re-post the line
// and fire the trigger - and its abort error - all over again.
QCOMPARE ( static_cast < int > ( joinedBuffer ( ) . count ( qsl ( " feedTelnet stopped to prevent a crash " ) ) ) , 1 ) ;
// The trigger should have fired exactly up to the limit and no further.
lua_getglobal ( L , " telnetLoopCount " ) ;
const int telnetLoopCount = static_cast < int > ( lua_tointeger ( L , - 1 ) ) ;
lua_pop ( L , 1 ) ;
QCOMPARE ( telnetLoopCount , cTelnet : : scmMaxLoopbackProcessingDepth ) ;
}
// A single, non-self-matching feedTelnet() must still work normally and not be
// flagged as a loop.
void test_normalFeedTelnetIsUnaffected ( )
{
startProfile ( mpHostname , mpLocalhost , mpPort ) ;
auto * host = mudlet : : self ( ) - > getActiveHost ( ) ;
infrastructure: guard functional-test lookups with QVERIFY (#9524)
#### Brief overview of PR changes/additions
The 12 functional-test sites that dereferenced the result of
`getActiveHost()`, `getArea()`, `getRoom()` or `getHost()` without first
checking it for null now guard the pointer with `QVERIFY(ptr)` before
use. The other lookup sites this PR touched already had a `QVERIFY` and
are left as they were.
#### Motivation for adding to Mudlet
CodeQL's `cpp/inconsistent-null-check` rule flagged these lookups
because some call sites checked the returned pointer for null while
others in the same files dereferenced it directly. Adding `QVERIFY(ptr)`
at the previously-unchecked sites makes the handling consistent: every
lookup result is verified before it is dereferenced, so a null result
fails the test loudly instead of crashing.
`QVERIFY` is the project's accepted idiom for guarding a pointer in a
test, so it is used here rather than an `if (!ptr) { QFAIL(...); }`
block. CodeQL does not recognise `QVERIFY(ptr)` as a null check (the
branch it generates tests `qVerify()`'s return value, not the pointer
itself), so it will keep reporting `cpp/inconsistent-null-check` on
these sites and on the pre-existing `QVERIFY` sites. Those alerts will
be dismissed as false positives rather than changing the test style to
satisfy the checker.
#### Other info (issues closed, discussion etc)
Test files only - no production code is touched. The 12 newly-guarded
sites are:
- `MapRoundTripTest.cpp` - 2 (`pAreaA`, `pAreaB`)
- `TriggerSameLineMatchTest.cpp` - 6 (`host`)
- `TFeedTriggersRecursionTest.cpp` - 4 (`host`)
**Test case:**
Build and run the five affected binaries under the functional-test flock
- all pass:
- `TFeedTriggersRecursionTest` - passed
- `TriggerSameLineMatchTest` - passed
- `MapRoundTripTest` - 6 passed, 0 failed
- `UndoServerWrapTest` - passed
- `UndoServerWrapReplay` - 2 passed, 1 skipped (manual replay tool;
skips without `REPLAY_CAPTURE`/`REPLAY_OUT`)
2026-07-29 13:42:04 +02:00
QVERIFY ( host ) ;
fix: prevent crash from a trigger that endlessly feeds itself (#9368)
#### Brief overview of PR changes/additions
Abort with a Lua error (naming the trigger) when trigger processing
recurses past a depth limit, instead of overflowing the stack. Covers
both `feedTriggers()` and `feedTelnet()` loops, each with a limit sized
to its stack usage.
#### Motivation for adding to Mudlet
A trigger whose action calls `feedTriggers()` with text that re-matches
it recurses the C++ stack until Mudlet hard-crashes (Sentry `fbda193d`,
`EXCEPTION_STACK_OVERFLOW`); this turns that into a clear, recoverable
script error. Review found `feedTelnet()` crashes the same way, so it
gets the same treatment.
#### Other info (issues closed, discussion etc)
Crash seen on Windows in Sentry. The same loop on Linux/macOS only
reaches Lua's own `C stack overflow` guard (~200 nested C calls) -
cryptic, but no crash; Windows' smaller ~1 MB stack dies first. The
fix's depth limit (50) trips before either, so every platform gets the
same clear, named error.
`feedTelnet()` needs a much lower limit (5): each nested telnet
processing level holds ~100KB of stack buffers, so a 1MB stack dies
after ~8 levels. Fixing this also surfaced a reentrancy bug where nested
`feedTelnet()` re-posted the ancestors' pending `mMudData`, duplicating
output lines and abort errors; `postData()` now detaches the data before
posting.
**Test case:**
1. New regex trigger: pattern `^loopme$`, script
`feedTriggers("loopme\n")`.
2. Run `feedTriggers("loopme\n")`.
3. Before: crash (Windows) / `<C stack overflow>` (Linux). After: it
stops with an error naming the trigger and stays running.
4. Same with `feedTelnet("loopme\n")` in the trigger and prompt (on a
disconnected profile): stops with a single error instead of crashing.
---------
Signed-off-by: Vadim Peretokin <vperetokin@hey.com>
2026-07-13 20:14:21 +02:00
host - > mEchoLuaErrors = true ;
host - > mTelnet . disconnectIt ( ) ;
QTRY_COMPARE ( host - > mTelnet . getConnectionState ( ) , QAbstractSocket : : UnconnectedState ) ;
host - > getLuaInterpreter ( ) - > compileAndExecuteScript ( qsl ( " normalTelnetCount = 0 \n "
" tempRegexTrigger('^hello$', [[normalTelnetCount = normalTelnetCount + 1]]) \n "
" feedTelnet('hello \\ n') \n "
" echo('NORMALTELNETCOUNT='..normalTelnetCount..' \\ n') \n " ) ) ;
QCOMPARE ( host - > mTelnet . loopbackProcessingDepth ( ) , 0 ) ;
QVERIFY2 ( ! bufferContains ( qsl ( " stuck in an endless loop " ) ) , " A normal feedTelnet() call must not be treated as a loop " ) ;
QVERIFY2 ( bufferContains ( qsl ( " NORMALTELNETCOUNT=1 " ) ) , " Expected the non-looping trigger to fire exactly once " ) ;
}
void cleanup ( )
{
delete mpServer ;
mpServer = nullptr ;
deleteProfileDirectory ( mpHostname ) ;
delete mudlet : : self ( ) ;
}
// Starts a profile the way a user would via the GUI (mirrors the helper in
// TelnetTextDisplayedTest).
void startProfile ( const QString & hostname , const QString & address , const QString & port )
{
infrastructure: use std::chrono literals for time durations (#9493)
#### Brief overview of PR changes/additions
Convert raw millisecond integer literals at time-duration call sites to
`std::chrono` literals, and add `#include <chrono>` to each touched
translation unit. Examples:
- `QTimer::singleShot(0, ...)` → `QTimer::singleShot(0ms, ...)`
- `mpTimerReplay->setInterval(1000)` → `setInterval(1s)`
- `mPendingTimer.start(60000)` → `start(1min)`
- `QObject::startTimer(50)` → `startTimer(50ms)`
- `QTest::qWait(100)` → `QTest::qWait(100ms)`
- `QThread::msleep(10)` → `QThread::sleep(10ms)`
This is a semantics-preserving refactor - every duration is kept exactly
equal to before (e.g. `1000` ms becomes `1s`, `60000` ms becomes
`1min`). No behavioural change.
#### Motivation for adding to Mudlet
Chrono literals make time durations self-documenting and type-safe. `1s`
/ `100ms` read unambiguously where a bare `1000` / `100` forces the
reader to remember each API's unit, and the compiler now rejects unit
mismatches. Only genuine duration arguments were converted - loop
counts, scroll-line counts, sizes, ports and the like were deliberately
left as plain integers.
All targeted APIs provide `std::chrono` overloads in the minimum
supported Qt (6.8.2): `QTimer::singleShot`/`start`/`setInterval` (5.8),
`QObject::startTimer` (5.9), `QThread::sleep(std::chrono::nanoseconds)`
(6.6) and `QTest::qWait(std::chrono::milliseconds)` (6.7).
#### Other info (issues closed, discussion etc)
Test case: the full application builds cleanly and the entire functional
`ctest` suite passes. The only failing test is the known, pre-existing
`PasswordMigrationTest` LSan exit-leak (GTK3/fontconfig noise), which is
unrelated to this change.
Assisted-by: Claude:claude-opus-4-8
2026-07-25 20:24:31 +02:00
QTimer : : singleShot ( 0 ms , qApp , [ hostname , address , port ] ( ) {
fix: prevent crash from a trigger that endlessly feeds itself (#9368)
#### Brief overview of PR changes/additions
Abort with a Lua error (naming the trigger) when trigger processing
recurses past a depth limit, instead of overflowing the stack. Covers
both `feedTriggers()` and `feedTelnet()` loops, each with a limit sized
to its stack usage.
#### Motivation for adding to Mudlet
A trigger whose action calls `feedTriggers()` with text that re-matches
it recurses the C++ stack until Mudlet hard-crashes (Sentry `fbda193d`,
`EXCEPTION_STACK_OVERFLOW`); this turns that into a clear, recoverable
script error. Review found `feedTelnet()` crashes the same way, so it
gets the same treatment.
#### Other info (issues closed, discussion etc)
Crash seen on Windows in Sentry. The same loop on Linux/macOS only
reaches Lua's own `C stack overflow` guard (~200 nested C calls) -
cryptic, but no crash; Windows' smaller ~1 MB stack dies first. The
fix's depth limit (50) trips before either, so every platform gets the
same clear, named error.
`feedTelnet()` needs a much lower limit (5): each nested telnet
processing level holds ~100KB of stack buffers, so a 1MB stack dies
after ~8 levels. Fixing this also surfaced a reentrancy bug where nested
`feedTelnet()` re-posted the ancestors' pending `mMudData`, duplicating
output lines and abort errors; `postData()` now detaches the data before
posting.
**Test case:**
1. New regex trigger: pattern `^loopme$`, script
`feedTriggers("loopme\n")`.
2. Run `feedTriggers("loopme\n")`.
3. Before: crash (Windows) / `<C stack overflow>` (Linux). After: it
stops with an error naming the trigger and stays running.
4. Same with `feedTelnet("loopme\n")` in the trigger and prompt (on a
disconnected profile): stops with a single error instead of crashing.
---------
Signed-off-by: Vadim Peretokin <vperetokin@hey.com>
2026-07-13 20:14:21 +02:00
mudlet : : self ( ) - > startAutoLogin ( { } ) ;
infrastructure: use std::chrono literals for time durations (#9493)
#### Brief overview of PR changes/additions
Convert raw millisecond integer literals at time-duration call sites to
`std::chrono` literals, and add `#include <chrono>` to each touched
translation unit. Examples:
- `QTimer::singleShot(0, ...)` → `QTimer::singleShot(0ms, ...)`
- `mpTimerReplay->setInterval(1000)` → `setInterval(1s)`
- `mPendingTimer.start(60000)` → `start(1min)`
- `QObject::startTimer(50)` → `startTimer(50ms)`
- `QTest::qWait(100)` → `QTest::qWait(100ms)`
- `QThread::msleep(10)` → `QThread::sleep(10ms)`
This is a semantics-preserving refactor - every duration is kept exactly
equal to before (e.g. `1000` ms becomes `1s`, `60000` ms becomes
`1min`). No behavioural change.
#### Motivation for adding to Mudlet
Chrono literals make time durations self-documenting and type-safe. `1s`
/ `100ms` read unambiguously where a bare `1000` / `100` forces the
reader to remember each API's unit, and the compiler now rejects unit
mismatches. Only genuine duration arguments were converted - loop
counts, scroll-line counts, sizes, ports and the like were deliberately
left as plain integers.
All targeted APIs provide `std::chrono` overloads in the minimum
supported Qt (6.8.2): `QTimer::singleShot`/`start`/`setInterval` (5.8),
`QObject::startTimer` (5.9), `QThread::sleep(std::chrono::nanoseconds)`
(6.6) and `QTest::qWait(std::chrono::milliseconds)` (6.7).
#### Other info (issues closed, discussion etc)
Test case: the full application builds cleanly and the entire functional
`ctest` suite passes. The only failing test is the known, pre-existing
`PasswordMigrationTest` LSan exit-leak (GTK3/fontconfig noise), which is
unrelated to this change.
Assisted-by: Claude:claude-opus-4-8
2026-07-25 20:24:31 +02:00
QTest : : qWait ( 100 ms ) ;
fix: prevent crash from a trigger that endlessly feeds itself (#9368)
#### Brief overview of PR changes/additions
Abort with a Lua error (naming the trigger) when trigger processing
recurses past a depth limit, instead of overflowing the stack. Covers
both `feedTriggers()` and `feedTelnet()` loops, each with a limit sized
to its stack usage.
#### Motivation for adding to Mudlet
A trigger whose action calls `feedTriggers()` with text that re-matches
it recurses the C++ stack until Mudlet hard-crashes (Sentry `fbda193d`,
`EXCEPTION_STACK_OVERFLOW`); this turns that into a clear, recoverable
script error. Review found `feedTelnet()` crashes the same way, so it
gets the same treatment.
#### Other info (issues closed, discussion etc)
Crash seen on Windows in Sentry. The same loop on Linux/macOS only
reaches Lua's own `C stack overflow` guard (~200 nested C calls) -
cryptic, but no crash; Windows' smaller ~1 MB stack dies first. The
fix's depth limit (50) trips before either, so every platform gets the
same clear, named error.
`feedTelnet()` needs a much lower limit (5): each nested telnet
processing level holds ~100KB of stack buffers, so a 1MB stack dies
after ~8 levels. Fixing this also surfaced a reentrancy bug where nested
`feedTelnet()` re-posted the ancestors' pending `mMudData`, duplicating
output lines and abort errors; `postData()` now detaches the data before
posting.
**Test case:**
1. New regex trigger: pattern `^loopme$`, script
`feedTriggers("loopme\n")`.
2. Run `feedTriggers("loopme\n")`.
3. Before: crash (Windows) / `<C stack overflow>` (Linux). After: it
stops with an error naming the trigger and stays running.
4. Same with `feedTelnet("loopme\n")` in the trigger and prompt (on a
disconnected profile): stops with a single error instead of crashing.
---------
Signed-off-by: Vadim Peretokin <vperetokin@hey.com>
2026-07-13 20:14:21 +02:00
QTest : : mouseClick ( mudlet : : self ( ) - > mpConnectionDialog - > new_profile_button , Qt : : LeftButton ) ;
infrastructure: use std::chrono literals for time durations (#9493)
#### Brief overview of PR changes/additions
Convert raw millisecond integer literals at time-duration call sites to
`std::chrono` literals, and add `#include <chrono>` to each touched
translation unit. Examples:
- `QTimer::singleShot(0, ...)` → `QTimer::singleShot(0ms, ...)`
- `mpTimerReplay->setInterval(1000)` → `setInterval(1s)`
- `mPendingTimer.start(60000)` → `start(1min)`
- `QObject::startTimer(50)` → `startTimer(50ms)`
- `QTest::qWait(100)` → `QTest::qWait(100ms)`
- `QThread::msleep(10)` → `QThread::sleep(10ms)`
This is a semantics-preserving refactor - every duration is kept exactly
equal to before (e.g. `1000` ms becomes `1s`, `60000` ms becomes
`1min`). No behavioural change.
#### Motivation for adding to Mudlet
Chrono literals make time durations self-documenting and type-safe. `1s`
/ `100ms` read unambiguously where a bare `1000` / `100` forces the
reader to remember each API's unit, and the compiler now rejects unit
mismatches. Only genuine duration arguments were converted - loop
counts, scroll-line counts, sizes, ports and the like were deliberately
left as plain integers.
All targeted APIs provide `std::chrono` overloads in the minimum
supported Qt (6.8.2): `QTimer::singleShot`/`start`/`setInterval` (5.8),
`QObject::startTimer` (5.9), `QThread::sleep(std::chrono::nanoseconds)`
(6.6) and `QTest::qWait(std::chrono::milliseconds)` (6.7).
#### Other info (issues closed, discussion etc)
Test case: the full application builds cleanly and the entire functional
`ctest` suite passes. The only failing test is the known, pre-existing
`PasswordMigrationTest` LSan exit-leak (GTK3/fontconfig noise), which is
unrelated to this change.
Assisted-by: Claude:claude-opus-4-8
2026-07-25 20:24:31 +02:00
QTest : : qWait ( 100 ms ) ;
fix: prevent crash from a trigger that endlessly feeds itself (#9368)
#### Brief overview of PR changes/additions
Abort with a Lua error (naming the trigger) when trigger processing
recurses past a depth limit, instead of overflowing the stack. Covers
both `feedTriggers()` and `feedTelnet()` loops, each with a limit sized
to its stack usage.
#### Motivation for adding to Mudlet
A trigger whose action calls `feedTriggers()` with text that re-matches
it recurses the C++ stack until Mudlet hard-crashes (Sentry `fbda193d`,
`EXCEPTION_STACK_OVERFLOW`); this turns that into a clear, recoverable
script error. Review found `feedTelnet()` crashes the same way, so it
gets the same treatment.
#### Other info (issues closed, discussion etc)
Crash seen on Windows in Sentry. The same loop on Linux/macOS only
reaches Lua's own `C stack overflow` guard (~200 nested C calls) -
cryptic, but no crash; Windows' smaller ~1 MB stack dies first. The
fix's depth limit (50) trips before either, so every platform gets the
same clear, named error.
`feedTelnet()` needs a much lower limit (5): each nested telnet
processing level holds ~100KB of stack buffers, so a 1MB stack dies
after ~8 levels. Fixing this also surfaced a reentrancy bug where nested
`feedTelnet()` re-posted the ancestors' pending `mMudData`, duplicating
output lines and abort errors; `postData()` now detaches the data before
posting.
**Test case:**
1. New regex trigger: pattern `^loopme$`, script
`feedTriggers("loopme\n")`.
2. Run `feedTriggers("loopme\n")`.
3. Before: crash (Windows) / `<C stack overflow>` (Linux). After: it
stops with an error naming the trigger and stays running.
4. Same with `feedTelnet("loopme\n")` in the trigger and prompt (on a
disconnected profile): stops with a single error instead of crashing.
---------
Signed-off-by: Vadim Peretokin <vperetokin@hey.com>
2026-07-13 20:14:21 +02:00
QTest : : keyClicks ( QApplication : : focusWidget ( ) , hostname ) ;
infrastructure: use std::chrono literals for time durations (#9493)
#### Brief overview of PR changes/additions
Convert raw millisecond integer literals at time-duration call sites to
`std::chrono` literals, and add `#include <chrono>` to each touched
translation unit. Examples:
- `QTimer::singleShot(0, ...)` → `QTimer::singleShot(0ms, ...)`
- `mpTimerReplay->setInterval(1000)` → `setInterval(1s)`
- `mPendingTimer.start(60000)` → `start(1min)`
- `QObject::startTimer(50)` → `startTimer(50ms)`
- `QTest::qWait(100)` → `QTest::qWait(100ms)`
- `QThread::msleep(10)` → `QThread::sleep(10ms)`
This is a semantics-preserving refactor - every duration is kept exactly
equal to before (e.g. `1000` ms becomes `1s`, `60000` ms becomes
`1min`). No behavioural change.
#### Motivation for adding to Mudlet
Chrono literals make time durations self-documenting and type-safe. `1s`
/ `100ms` read unambiguously where a bare `1000` / `100` forces the
reader to remember each API's unit, and the compiler now rejects unit
mismatches. Only genuine duration arguments were converted - loop
counts, scroll-line counts, sizes, ports and the like were deliberately
left as plain integers.
All targeted APIs provide `std::chrono` overloads in the minimum
supported Qt (6.8.2): `QTimer::singleShot`/`start`/`setInterval` (5.8),
`QObject::startTimer` (5.9), `QThread::sleep(std::chrono::nanoseconds)`
(6.6) and `QTest::qWait(std::chrono::milliseconds)` (6.7).
#### Other info (issues closed, discussion etc)
Test case: the full application builds cleanly and the entire functional
`ctest` suite passes. The only failing test is the known, pre-existing
`PasswordMigrationTest` LSan exit-leak (GTK3/fontconfig noise), which is
unrelated to this change.
Assisted-by: Claude:claude-opus-4-8
2026-07-25 20:24:31 +02:00
QTest : : qWait ( 100 ms ) ;
fix: prevent crash from a trigger that endlessly feeds itself (#9368)
#### Brief overview of PR changes/additions
Abort with a Lua error (naming the trigger) when trigger processing
recurses past a depth limit, instead of overflowing the stack. Covers
both `feedTriggers()` and `feedTelnet()` loops, each with a limit sized
to its stack usage.
#### Motivation for adding to Mudlet
A trigger whose action calls `feedTriggers()` with text that re-matches
it recurses the C++ stack until Mudlet hard-crashes (Sentry `fbda193d`,
`EXCEPTION_STACK_OVERFLOW`); this turns that into a clear, recoverable
script error. Review found `feedTelnet()` crashes the same way, so it
gets the same treatment.
#### Other info (issues closed, discussion etc)
Crash seen on Windows in Sentry. The same loop on Linux/macOS only
reaches Lua's own `C stack overflow` guard (~200 nested C calls) -
cryptic, but no crash; Windows' smaller ~1 MB stack dies first. The
fix's depth limit (50) trips before either, so every platform gets the
same clear, named error.
`feedTelnet()` needs a much lower limit (5): each nested telnet
processing level holds ~100KB of stack buffers, so a 1MB stack dies
after ~8 levels. Fixing this also surfaced a reentrancy bug where nested
`feedTelnet()` re-posted the ancestors' pending `mMudData`, duplicating
output lines and abort errors; `postData()` now detaches the data before
posting.
**Test case:**
1. New regex trigger: pattern `^loopme$`, script
`feedTriggers("loopme\n")`.
2. Run `feedTriggers("loopme\n")`.
3. Before: crash (Windows) / `<C stack overflow>` (Linux). After: it
stops with an error naming the trigger and stays running.
4. Same with `feedTelnet("loopme\n")` in the trigger and prompt (on a
disconnected profile): stops with a single error instead of crashing.
---------
Signed-off-by: Vadim Peretokin <vperetokin@hey.com>
2026-07-13 20:14:21 +02:00
QTest : : keyClick ( QApplication : : focusWidget ( ) , Qt : : Key_Tab ) ;
infrastructure: use std::chrono literals for time durations (#9493)
#### Brief overview of PR changes/additions
Convert raw millisecond integer literals at time-duration call sites to
`std::chrono` literals, and add `#include <chrono>` to each touched
translation unit. Examples:
- `QTimer::singleShot(0, ...)` → `QTimer::singleShot(0ms, ...)`
- `mpTimerReplay->setInterval(1000)` → `setInterval(1s)`
- `mPendingTimer.start(60000)` → `start(1min)`
- `QObject::startTimer(50)` → `startTimer(50ms)`
- `QTest::qWait(100)` → `QTest::qWait(100ms)`
- `QThread::msleep(10)` → `QThread::sleep(10ms)`
This is a semantics-preserving refactor - every duration is kept exactly
equal to before (e.g. `1000` ms becomes `1s`, `60000` ms becomes
`1min`). No behavioural change.
#### Motivation for adding to Mudlet
Chrono literals make time durations self-documenting and type-safe. `1s`
/ `100ms` read unambiguously where a bare `1000` / `100` forces the
reader to remember each API's unit, and the compiler now rejects unit
mismatches. Only genuine duration arguments were converted - loop
counts, scroll-line counts, sizes, ports and the like were deliberately
left as plain integers.
All targeted APIs provide `std::chrono` overloads in the minimum
supported Qt (6.8.2): `QTimer::singleShot`/`start`/`setInterval` (5.8),
`QObject::startTimer` (5.9), `QThread::sleep(std::chrono::nanoseconds)`
(6.6) and `QTest::qWait(std::chrono::milliseconds)` (6.7).
#### Other info (issues closed, discussion etc)
Test case: the full application builds cleanly and the entire functional
`ctest` suite passes. The only failing test is the known, pre-existing
`PasswordMigrationTest` LSan exit-leak (GTK3/fontconfig noise), which is
unrelated to this change.
Assisted-by: Claude:claude-opus-4-8
2026-07-25 20:24:31 +02:00
QTest : : qWait ( 100 ms ) ;
fix: prevent crash from a trigger that endlessly feeds itself (#9368)
#### Brief overview of PR changes/additions
Abort with a Lua error (naming the trigger) when trigger processing
recurses past a depth limit, instead of overflowing the stack. Covers
both `feedTriggers()` and `feedTelnet()` loops, each with a limit sized
to its stack usage.
#### Motivation for adding to Mudlet
A trigger whose action calls `feedTriggers()` with text that re-matches
it recurses the C++ stack until Mudlet hard-crashes (Sentry `fbda193d`,
`EXCEPTION_STACK_OVERFLOW`); this turns that into a clear, recoverable
script error. Review found `feedTelnet()` crashes the same way, so it
gets the same treatment.
#### Other info (issues closed, discussion etc)
Crash seen on Windows in Sentry. The same loop on Linux/macOS only
reaches Lua's own `C stack overflow` guard (~200 nested C calls) -
cryptic, but no crash; Windows' smaller ~1 MB stack dies first. The
fix's depth limit (50) trips before either, so every platform gets the
same clear, named error.
`feedTelnet()` needs a much lower limit (5): each nested telnet
processing level holds ~100KB of stack buffers, so a 1MB stack dies
after ~8 levels. Fixing this also surfaced a reentrancy bug where nested
`feedTelnet()` re-posted the ancestors' pending `mMudData`, duplicating
output lines and abort errors; `postData()` now detaches the data before
posting.
**Test case:**
1. New regex trigger: pattern `^loopme$`, script
`feedTriggers("loopme\n")`.
2. Run `feedTriggers("loopme\n")`.
3. Before: crash (Windows) / `<C stack overflow>` (Linux). After: it
stops with an error naming the trigger and stays running.
4. Same with `feedTelnet("loopme\n")` in the trigger and prompt (on a
disconnected profile): stops with a single error instead of crashing.
---------
Signed-off-by: Vadim Peretokin <vperetokin@hey.com>
2026-07-13 20:14:21 +02:00
QTest : : keyClicks ( QApplication : : focusWidget ( ) , address ) ;
infrastructure: use std::chrono literals for time durations (#9493)
#### Brief overview of PR changes/additions
Convert raw millisecond integer literals at time-duration call sites to
`std::chrono` literals, and add `#include <chrono>` to each touched
translation unit. Examples:
- `QTimer::singleShot(0, ...)` → `QTimer::singleShot(0ms, ...)`
- `mpTimerReplay->setInterval(1000)` → `setInterval(1s)`
- `mPendingTimer.start(60000)` → `start(1min)`
- `QObject::startTimer(50)` → `startTimer(50ms)`
- `QTest::qWait(100)` → `QTest::qWait(100ms)`
- `QThread::msleep(10)` → `QThread::sleep(10ms)`
This is a semantics-preserving refactor - every duration is kept exactly
equal to before (e.g. `1000` ms becomes `1s`, `60000` ms becomes
`1min`). No behavioural change.
#### Motivation for adding to Mudlet
Chrono literals make time durations self-documenting and type-safe. `1s`
/ `100ms` read unambiguously where a bare `1000` / `100` forces the
reader to remember each API's unit, and the compiler now rejects unit
mismatches. Only genuine duration arguments were converted - loop
counts, scroll-line counts, sizes, ports and the like were deliberately
left as plain integers.
All targeted APIs provide `std::chrono` overloads in the minimum
supported Qt (6.8.2): `QTimer::singleShot`/`start`/`setInterval` (5.8),
`QObject::startTimer` (5.9), `QThread::sleep(std::chrono::nanoseconds)`
(6.6) and `QTest::qWait(std::chrono::milliseconds)` (6.7).
#### Other info (issues closed, discussion etc)
Test case: the full application builds cleanly and the entire functional
`ctest` suite passes. The only failing test is the known, pre-existing
`PasswordMigrationTest` LSan exit-leak (GTK3/fontconfig noise), which is
unrelated to this change.
Assisted-by: Claude:claude-opus-4-8
2026-07-25 20:24:31 +02:00
QTest : : qWait ( 100 ms ) ;
fix: prevent crash from a trigger that endlessly feeds itself (#9368)
#### Brief overview of PR changes/additions
Abort with a Lua error (naming the trigger) when trigger processing
recurses past a depth limit, instead of overflowing the stack. Covers
both `feedTriggers()` and `feedTelnet()` loops, each with a limit sized
to its stack usage.
#### Motivation for adding to Mudlet
A trigger whose action calls `feedTriggers()` with text that re-matches
it recurses the C++ stack until Mudlet hard-crashes (Sentry `fbda193d`,
`EXCEPTION_STACK_OVERFLOW`); this turns that into a clear, recoverable
script error. Review found `feedTelnet()` crashes the same way, so it
gets the same treatment.
#### Other info (issues closed, discussion etc)
Crash seen on Windows in Sentry. The same loop on Linux/macOS only
reaches Lua's own `C stack overflow` guard (~200 nested C calls) -
cryptic, but no crash; Windows' smaller ~1 MB stack dies first. The
fix's depth limit (50) trips before either, so every platform gets the
same clear, named error.
`feedTelnet()` needs a much lower limit (5): each nested telnet
processing level holds ~100KB of stack buffers, so a 1MB stack dies
after ~8 levels. Fixing this also surfaced a reentrancy bug where nested
`feedTelnet()` re-posted the ancestors' pending `mMudData`, duplicating
output lines and abort errors; `postData()` now detaches the data before
posting.
**Test case:**
1. New regex trigger: pattern `^loopme$`, script
`feedTriggers("loopme\n")`.
2. Run `feedTriggers("loopme\n")`.
3. Before: crash (Windows) / `<C stack overflow>` (Linux). After: it
stops with an error naming the trigger and stays running.
4. Same with `feedTelnet("loopme\n")` in the trigger and prompt (on a
disconnected profile): stops with a single error instead of crashing.
---------
Signed-off-by: Vadim Peretokin <vperetokin@hey.com>
2026-07-13 20:14:21 +02:00
QTest : : keyClick ( QApplication : : focusWidget ( ) , Qt : : Key_Tab ) ;
infrastructure: use std::chrono literals for time durations (#9493)
#### Brief overview of PR changes/additions
Convert raw millisecond integer literals at time-duration call sites to
`std::chrono` literals, and add `#include <chrono>` to each touched
translation unit. Examples:
- `QTimer::singleShot(0, ...)` → `QTimer::singleShot(0ms, ...)`
- `mpTimerReplay->setInterval(1000)` → `setInterval(1s)`
- `mPendingTimer.start(60000)` → `start(1min)`
- `QObject::startTimer(50)` → `startTimer(50ms)`
- `QTest::qWait(100)` → `QTest::qWait(100ms)`
- `QThread::msleep(10)` → `QThread::sleep(10ms)`
This is a semantics-preserving refactor - every duration is kept exactly
equal to before (e.g. `1000` ms becomes `1s`, `60000` ms becomes
`1min`). No behavioural change.
#### Motivation for adding to Mudlet
Chrono literals make time durations self-documenting and type-safe. `1s`
/ `100ms` read unambiguously where a bare `1000` / `100` forces the
reader to remember each API's unit, and the compiler now rejects unit
mismatches. Only genuine duration arguments were converted - loop
counts, scroll-line counts, sizes, ports and the like were deliberately
left as plain integers.
All targeted APIs provide `std::chrono` overloads in the minimum
supported Qt (6.8.2): `QTimer::singleShot`/`start`/`setInterval` (5.8),
`QObject::startTimer` (5.9), `QThread::sleep(std::chrono::nanoseconds)`
(6.6) and `QTest::qWait(std::chrono::milliseconds)` (6.7).
#### Other info (issues closed, discussion etc)
Test case: the full application builds cleanly and the entire functional
`ctest` suite passes. The only failing test is the known, pre-existing
`PasswordMigrationTest` LSan exit-leak (GTK3/fontconfig noise), which is
unrelated to this change.
Assisted-by: Claude:claude-opus-4-8
2026-07-25 20:24:31 +02:00
QTest : : qWait ( 100 ms ) ;
fix: prevent crash from a trigger that endlessly feeds itself (#9368)
#### Brief overview of PR changes/additions
Abort with a Lua error (naming the trigger) when trigger processing
recurses past a depth limit, instead of overflowing the stack. Covers
both `feedTriggers()` and `feedTelnet()` loops, each with a limit sized
to its stack usage.
#### Motivation for adding to Mudlet
A trigger whose action calls `feedTriggers()` with text that re-matches
it recurses the C++ stack until Mudlet hard-crashes (Sentry `fbda193d`,
`EXCEPTION_STACK_OVERFLOW`); this turns that into a clear, recoverable
script error. Review found `feedTelnet()` crashes the same way, so it
gets the same treatment.
#### Other info (issues closed, discussion etc)
Crash seen on Windows in Sentry. The same loop on Linux/macOS only
reaches Lua's own `C stack overflow` guard (~200 nested C calls) -
cryptic, but no crash; Windows' smaller ~1 MB stack dies first. The
fix's depth limit (50) trips before either, so every platform gets the
same clear, named error.
`feedTelnet()` needs a much lower limit (5): each nested telnet
processing level holds ~100KB of stack buffers, so a 1MB stack dies
after ~8 levels. Fixing this also surfaced a reentrancy bug where nested
`feedTelnet()` re-posted the ancestors' pending `mMudData`, duplicating
output lines and abort errors; `postData()` now detaches the data before
posting.
**Test case:**
1. New regex trigger: pattern `^loopme$`, script
`feedTriggers("loopme\n")`.
2. Run `feedTriggers("loopme\n")`.
3. Before: crash (Windows) / `<C stack overflow>` (Linux). After: it
stops with an error naming the trigger and stays running.
4. Same with `feedTelnet("loopme\n")` in the trigger and prompt (on a
disconnected profile): stops with a single error instead of crashing.
---------
Signed-off-by: Vadim Peretokin <vperetokin@hey.com>
2026-07-13 20:14:21 +02:00
QTest : : keyClicks ( QApplication : : focusWidget ( ) , port ) ;
infrastructure: use std::chrono literals for time durations (#9493)
#### Brief overview of PR changes/additions
Convert raw millisecond integer literals at time-duration call sites to
`std::chrono` literals, and add `#include <chrono>` to each touched
translation unit. Examples:
- `QTimer::singleShot(0, ...)` → `QTimer::singleShot(0ms, ...)`
- `mpTimerReplay->setInterval(1000)` → `setInterval(1s)`
- `mPendingTimer.start(60000)` → `start(1min)`
- `QObject::startTimer(50)` → `startTimer(50ms)`
- `QTest::qWait(100)` → `QTest::qWait(100ms)`
- `QThread::msleep(10)` → `QThread::sleep(10ms)`
This is a semantics-preserving refactor - every duration is kept exactly
equal to before (e.g. `1000` ms becomes `1s`, `60000` ms becomes
`1min`). No behavioural change.
#### Motivation for adding to Mudlet
Chrono literals make time durations self-documenting and type-safe. `1s`
/ `100ms` read unambiguously where a bare `1000` / `100` forces the
reader to remember each API's unit, and the compiler now rejects unit
mismatches. Only genuine duration arguments were converted - loop
counts, scroll-line counts, sizes, ports and the like were deliberately
left as plain integers.
All targeted APIs provide `std::chrono` overloads in the minimum
supported Qt (6.8.2): `QTimer::singleShot`/`start`/`setInterval` (5.8),
`QObject::startTimer` (5.9), `QThread::sleep(std::chrono::nanoseconds)`
(6.6) and `QTest::qWait(std::chrono::milliseconds)` (6.7).
#### Other info (issues closed, discussion etc)
Test case: the full application builds cleanly and the entire functional
`ctest` suite passes. The only failing test is the known, pre-existing
`PasswordMigrationTest` LSan exit-leak (GTK3/fontconfig noise), which is
unrelated to this change.
Assisted-by: Claude:claude-opus-4-8
2026-07-25 20:24:31 +02:00
QTest : : qWait ( 100 ms ) ;
fix: prevent crash from a trigger that endlessly feeds itself (#9368)
#### Brief overview of PR changes/additions
Abort with a Lua error (naming the trigger) when trigger processing
recurses past a depth limit, instead of overflowing the stack. Covers
both `feedTriggers()` and `feedTelnet()` loops, each with a limit sized
to its stack usage.
#### Motivation for adding to Mudlet
A trigger whose action calls `feedTriggers()` with text that re-matches
it recurses the C++ stack until Mudlet hard-crashes (Sentry `fbda193d`,
`EXCEPTION_STACK_OVERFLOW`); this turns that into a clear, recoverable
script error. Review found `feedTelnet()` crashes the same way, so it
gets the same treatment.
#### Other info (issues closed, discussion etc)
Crash seen on Windows in Sentry. The same loop on Linux/macOS only
reaches Lua's own `C stack overflow` guard (~200 nested C calls) -
cryptic, but no crash; Windows' smaller ~1 MB stack dies first. The
fix's depth limit (50) trips before either, so every platform gets the
same clear, named error.
`feedTelnet()` needs a much lower limit (5): each nested telnet
processing level holds ~100KB of stack buffers, so a 1MB stack dies
after ~8 levels. Fixing this also surfaced a reentrancy bug where nested
`feedTelnet()` re-posted the ancestors' pending `mMudData`, duplicating
output lines and abort errors; `postData()` now detaches the data before
posting.
**Test case:**
1. New regex trigger: pattern `^loopme$`, script
`feedTriggers("loopme\n")`.
2. Run `feedTriggers("loopme\n")`.
3. Before: crash (Windows) / `<C stack overflow>` (Linux). After: it
stops with an error naming the trigger and stays running.
4. Same with `feedTelnet("loopme\n")` in the trigger and prompt (on a
disconnected profile): stops with a single error instead of crashing.
---------
Signed-off-by: Vadim Peretokin <vperetokin@hey.com>
2026-07-13 20:14:21 +02:00
QTest : : keyClick ( QApplication : : focusWidget ( ) , Qt : : Key_Return ) ;
} ) ;
QSignalSpy spy ( mudlet : : self ( ) , & mudlet : : signal_profileLoaded ) ;
if ( ! spy . wait ( 5000 ) ) {
QFAIL ( " Profile took too long to load. " ) ;
}
auto host = mudlet : : self ( ) - > getActiveHost ( ) ;
if ( ! host ) {
QFAIL ( " No active host available for the test. " ) ;
}
QSignalSpy spy2 ( & ( host - > mTelnet ) , & cTelnet : : signal_connected ) ;
if ( ! spy2 . wait ( 2000 ) ) {
QFAIL ( " Could not connect with the host. " ) ;
}
}
// Joins every physical buffer line and normalises whitespace before matching,
// so a long needle that the console word-wraps (with added indents) across
// lines is still found - a per-line scan would miss it depending on where the
// wrap lands, which shifts with the trigger id width and console geometry.
QString joinedBuffer ( )
{
auto console = mudlet : : self ( ) - > getActiveHost ( ) - > mpConsole ;
QString allText ;
for ( int i = 0 ; i < = console - > buffer . getLastLineNumber ( ) ; + + i ) {
allText . append ( console - > buffer . line ( i ) ) . append ( QChar : : Space ) ;
}
return allText . simplified ( ) ;
}
bool bufferContains ( const QString & needle ) { return joinedBuffer ( ) . contains ( needle ) ; }
void deleteProfileDirectory ( const QString & profileName )
{
const QString path = mudlet : : getMudletPath ( enums : : profileHomePath , profileName ) ;
QDir dir ( path ) ;
if ( ! dir . exists ( ) ) {
return ;
}
dir . removeRecursively ( ) ;
}
} ;
void initializeQRCResources ( )
{
# ifdef INCLUDE_VARIABLE_SPLASH_SCREEN
qInitResources_additional_splash_screens ( ) ;
# endif
# ifdef INCLUDE_FONTS
qInitResources_mudlet_fonts_common ( ) ;
# if defined(Q_OS_LINUX) || defined(Q_OS_FREEBSD)
qInitResources_mudlet_fonts_posix ( ) ;
# endif
# endif
qInitResources_mudlet ( ) ;
qInitResources_qm ( ) ;
}
# include "TFeedTriggersRecursionTest.moc"
QTEST_MAIN ( TFeedTriggersRecursionTest )