Skip to content

Commit f1b0214

Browse files
committed
console: fix vfs tail follow-mode use-after-free on session disconnect
A command running in follow mode (OCS_RunLoop, e.g. "vfs tail <file>") runs its Service() loop on its own task, writing to the launching session's OvmsWriter via a raw pointer. The only stop path was the Ctrl-C Terminator insert callback. When an SSH/Telnet session was closed or dropped without Ctrl-C, the console (an OvmsWriter) was destroyed while the task kept running, so the next writer->write() dereferenced a freed object (zeroed vtable) -> LoadProhibited crash. OvmsWriter now tracks its single active follow-mode task (at most one can run, since input is captured by the Terminator while it runs). TerminateCommandTask() requests the task to stop (OCS_StopRequested) and waits until it has fully exited and released the writer; the task clears its registration as its last writer access. ConsoleSSH/ConsoleTelnet call it before freeing their transport, with a backstop in ~OvmsWriter(). Cooperative stop+join avoids force-killing a task mid-write (no vTaskDelete of a task that may hold a lock). Generic to any OvmsCommandTask, not just vfs tail.
1 parent d4d8b57 commit f1b0214

5 files changed

Lines changed: 62 additions & 0 deletions

File tree

vehicle/OVMS.V3/changes.txt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
Open Vehicle Monitor System v3 - Change log
22

33
????-??-?? ??? ??????? OTA release
4+
- Console: Fix crash (LoadProhibited use-after-free) when an SSH or Telnet session running a
5+
command in follow mode (e.g. "vfs tail <file>") was closed or dropped without Ctrl-C. The
6+
follow-mode command task kept running and wrote to its now-freed session writer. The writer
7+
now tracks its active follow-mode task, and console teardown stops and joins it before
8+
freeing the connection. Affects any follow-mode command (OvmsCommandTask), not just vfs tail.
49

510

611
2026-05-17 MB 3.3.006 OTA release

vehicle/OVMS.V3/components/console_ssh/src/console_ssh.cpp

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -340,6 +340,9 @@ ConsoleSSH::ConsoleSSH(OvmsSSH* server, struct mg_connection* nc)
340340

341341
ConsoleSSH::~ConsoleSSH()
342342
{
343+
// Stop any follow-mode command task (e.g. "vfs tail") before freeing the
344+
// transport it writes to, otherwise it would dereference a freed writer.
345+
TerminateCommandTask();
343346
WOLFSSH* ssh = m_ssh;
344347
m_ssh = NULL;
345348
wolfSSH_free(ssh);

vehicle/OVMS.V3/components/console_telnet/src/console_telnet.cpp

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -182,6 +182,9 @@ ConsoleTelnet::ConsoleTelnet(struct mg_connection* nc)
182182

183183
ConsoleTelnet::~ConsoleTelnet()
184184
{
185+
// Stop any follow-mode command task (e.g. "vfs tail") before freeing the
186+
// transport it writes to, otherwise it would dereference a freed writer.
187+
TerminateCommandTask();
185188
telnet_t *telnet = m_telnet;
186189
m_telnet = NULL;
187190
telnet_free(telnet);

vehicle/OVMS.V3/main/ovms_command.cpp

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,10 +80,41 @@ OvmsWriter::OvmsWriter()
8080
m_insert = NULL;
8181
m_userData = NULL;
8282
m_monitoring = false;
83+
m_curtask = NULL;
8384
}
8485

8586
OvmsWriter::~OvmsWriter()
8687
{
88+
// Backstop: stop any follow-mode command task still bound to this writer.
89+
// Interactive consoles that own a transport should call this earlier (before
90+
// freeing that transport); here it only guards writers that did not.
91+
TerminateCommandTask();
92+
}
93+
94+
void OvmsWriter::RegisterCommandTask(OvmsCommandTask* task)
95+
{
96+
m_curtask = task;
97+
}
98+
99+
void OvmsWriter::DeregisterCommandTask(OvmsCommandTask* task)
100+
{
101+
if (m_curtask == task)
102+
m_curtask = NULL;
103+
}
104+
105+
void OvmsWriter::TerminateCommandTask()
106+
{
107+
// Ask the active follow-mode command task (if any) to stop, then wait until
108+
// it has fully exited. The task clears its registration (m_curtask) as its
109+
// last action that touches this writer, so once we observe it cleared the
110+
// task can no longer dereference us. This relies on no concurrent Ctrl-C
111+
// termination, which holds during connection-close teardown.
112+
OvmsCommandTask* task = m_curtask;
113+
if (task == NULL)
114+
return;
115+
task->RequestStop();
116+
while (m_curtask == task)
117+
vTaskDelay(pdMS_TO_TICKS(20));
87118
}
88119

89120
void OvmsWriter::Exit()
@@ -1744,8 +1775,12 @@ bool OvmsCommandTask::Run()
17441775
case OCS_RunLoop:
17451776
// start task:
17461777
writer->RegisterInsertCallback(Terminator, (void*) this);
1778+
// Register before Instantiate() so the task is never started while
1779+
// unregistered (it could otherwise finish and deregister before we set it).
1780+
writer->RegisterCommandTask(this);
17471781
if (!Instantiate())
17481782
{
1783+
writer->DeregisterCommandTask(this);
17491784
delete this;
17501785
return false;
17511786
}
@@ -1772,6 +1807,9 @@ OvmsCommandTask::~OvmsCommandTask()
17721807
if (m_state == OCS_StopRequested)
17731808
writer->puts("^C");
17741809
writer->DeregisterInsertCallback(Terminator);
1810+
// Must be the last access to writer: it releases a console blocked in
1811+
// TerminateCommandTask(), which may then free the writer.
1812+
writer->DeregisterCommandTask(this);
17751813
if (argv)
17761814
{
17771815
for (int i=0; i < argc; i++)

vehicle/OVMS.V3/main/ovms_command.h

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@
5454
class OvmsWriter;
5555
class OvmsCommand;
5656
class OvmsCommandMap;
57+
class OvmsCommandTask;
5758
class LogBuffers;
5859
typedef bool (*InsertCallback)(OvmsWriter* writer, void* userData, char);
5960

@@ -80,6 +81,16 @@ class OvmsWriter
8081
virtual void finalise() {}
8182
virtual void ProcessChar(char c) {}
8283

84+
public:
85+
// Lifecycle of a follow-mode (OCS_RunLoop) command task bound to this writer.
86+
// At most one such task can be active at a time (console input is captured by
87+
// the command's Ctrl-C Terminator while it runs). TerminateCommandTask() must
88+
// be called by a console before it frees any transport it owns, so the task
89+
// can no longer dereference this writer after it is destroyed.
90+
void RegisterCommandTask(OvmsCommandTask* task);
91+
void DeregisterCommandTask(OvmsCommandTask* task);
92+
void TerminateCommandTask();
93+
8394
public:
8495
// Used to notify the writer of a migration of a file within the VFS
8596
virtual const std::string GetPath() { return std::string(""); }
@@ -98,6 +109,7 @@ class OvmsWriter
98109
InsertCallback m_insert;
99110
void* m_userData;
100111
bool m_monitoring;
112+
OvmsCommandTask* m_curtask; // active follow-mode command task, or NULL
101113
};
102114

103115
template <typename T>
@@ -328,6 +340,7 @@ class OvmsCommandTask : public TaskBase
328340
static bool Terminator(OvmsWriter* writer, void* userdata, char ch);
329341
bool IsRunning() { return m_state == OCS_RunLoop; }
330342
bool IsTerminated() { return m_state == OCS_StopRequested; }
343+
void RequestStop() { m_state = OCS_StopRequested; }
331344

332345
protected:
333346
int verbosity;

0 commit comments

Comments
 (0)