Fix crash while unloading modules

The bug allowed unprivileged users to trigger use-after-free while
unloading modules

Note that the fix requires a slight API change for OnEmbeddedWebRequest:
now the callers (webadmin is the only known caller of it) call it via
DoEmbeddedWebRequest() instead. The modules which implement this hook
need no changes.

CVE-2026-82373

Found by Claude from Anthropic, reported by Ada Logics.
This commit is contained in:
Alexey Sokolov committed 2026-09-12 19:08:21 +01:00
1 parent d9b392afd9
commit 4b88c64c6e
7 files changed
+114 -82

No files matched your search

+70 -1
View File
@@ -465,6 +465,9 @@ class CModule {
void SetNetwork(CIRCNetwork* pNetwork);
void SetClient(CClient* pClient);
/** True if the module can be safely unloaded. */
bool IsCallStackEmpty() const;
/** This function throws CModule::UNLOAD which causes this module to be unloaded.
*/
void Unload() { throw UNLOAD; }
@@ -535,9 +538,11 @@ class CModule {
* @return The List.
*/
virtual VWebSubPages& GetSubPages() { return m_vSubPages; }
private:
/** Using this hook, module can embed web stuff directly to different places.
* This method is called whenever embededded modules I/O happens.
* This method is called whenever embedded modules I/O happens.
* Name of used .tmpl file (if any) is up to caller.
* Callers call it via `DoEmbeddedWebRequest()`.
* @param WebSock Socket for web connection, don't do bad things with it.
* @param sPageName Describes the place where web stuff is embedded to.
* @param Tmpl Template. Depending on context, you can do various stuff with it.
@@ -547,6 +552,9 @@ class CModule {
virtual bool OnEmbeddedWebRequest(CWebSock& WebSock,
const CString& sPageName,
CTemplate& Tmpl);
public:
bool DoEmbeddedWebRequest(CWebSock& WebSock, const CString& sPageName,
CTemplate& Tmpl);
/** Called just before znc.conf is rehashed */
virtual void OnPreRehash();
@@ -1528,6 +1536,9 @@ class CModule {
m_mssRegistry; //!< way to save name/value pairs. Note there is no encryption involved in this
VWebSubPages m_vSubPages;
std::map<CString, CModCommand> m_mCommands;
int m_iCallStackDepth = 0;
friend struct CModCallProtector;
};
class CModules : public std::vector<CModule*>, private CCoreTranslationMixin {
@@ -1766,4 +1777,62 @@ class CModules : public std::vector<CModule*>, private CCoreTranslationMixin {
CClient* m_pClient;
};
struct CModCallProtector {
explicit CModCallProtector(CModule& pMod) : m_pMod(&pMod) {
pMod.m_iCallStackDepth++;
}
~CModCallProtector() { m_pMod->m_iCallStackDepth--; }
CModule* m_pMod;
};
#ifndef SWIG
template <typename T, T* (CModule::*Getter)() const, void (CModule::*Setter)(T*)>
struct CTemporaryModField {
CTemporaryModField(CModule& pMod, T* pValue) {
m_pMod = &pMod;
m_pOldT = (pMod.*Getter)();
if (pValue) {
(pMod.*Setter)(pValue);
}
}
~CTemporaryModField() { (m_pMod->*Setter)(m_pOldT); }
CModule* m_pMod;
T* m_pOldT;
};
using CTemporaryModClient =
CTemporaryModField<CClient, &CModule::GetClient, &CModule::SetClient>;
using CTemporaryModNetwork =
CTemporaryModField<CIRCNetwork, &CModule::GetNetwork, &CModule::SetNetwork>;
using CTemporaryModUser =
CTemporaryModField<CUser, &CModule::GetUser, &CModule::SetUser>;
// Same as above, but for plural CModules
template <typename T, T* (CModules::*Getter)() const, void (CModules::*Setter)(T*)>
struct CTemporaryModsField {
CTemporaryModsField(CModules& pMods, T* pValue) {
m_pMods = &pMods;
m_pOldT = (pMods.*Getter)();
if (pValue) {
(pMods.*Setter)(pValue);
}
}
~CTemporaryModsField() { (m_pMods->*Setter)(m_pOldT); }
CModules* m_pMods;
T* m_pOldT;
};
using CTemporaryModsClient =
CTemporaryModsField<CClient, &CModules::GetClient, &CModules::SetClient>;
using CTemporaryModsNetwork =
CTemporaryModsField<CIRCNetwork, &CModules::GetNetwork, &CModules::SetNetwork>;
using CTemporaryModsUser =
CTemporaryModsField<CUser, &CModules::GetUser, &CModules::SetUser>;
#endif
#endif // !ZNC_MODULES_H
+6 -24
View File
@@ -56,21 +56,12 @@ extern bool ZNC_NO_NEED_TO_DO_ANYTHING_ON_MODULE_CALL_EXITER;
#define _GLOBALMODULECALL(macFUNC, macUSER, macNETWORK, macCLIENT, macEXITER) \
do { \
CModules& GMods = CZNC::Get().GetModules(); \
CUser* pOldGUser = GMods.GetUser(); \
CIRCNetwork* pOldGNetwork = GMods.GetNetwork(); \
CClient* pOldGClient = GMods.GetClient(); \
GMods.SetUser(macUSER); \
GMods.SetNetwork(macNETWORK); \
GMods.SetClient(macCLIENT); \
CTemporaryModsUser TempUser(GMods, macUSER); \
CTemporaryModsNetwork TempNetwork(GMods, macNETWORK); \
CTemporaryModsClient TempClient(GMods, macCLIENT); \
if (GMods.macFUNC) { \
GMods.SetUser(pOldGUser); \
GMods.SetNetwork(pOldGNetwork); \
GMods.SetClient(pOldGClient); \
*macEXITER = true; \
} \
GMods.SetUser(pOldGUser); \
GMods.SetNetwork(pOldGNetwork); \
GMods.SetClient(pOldGClient); \
} while (false)
#define _USERMODULECALL(macFUNC, macUSER, macNETWORK, macCLIENT, macEXITER) \
@@ -84,17 +75,11 @@ extern bool ZNC_NO_NEED_TO_DO_ANYTHING_ON_MODULE_CALL_EXITER;
} \
if (macUSER != nullptr) { \
CModules& UMods = macUSER->GetModules(); \
CIRCNetwork* pOldUNetwork = UMods.GetNetwork(); \
CClient* pOldUClient = UMods.GetClient(); \
UMods.SetNetwork(macNETWORK); \
UMods.SetClient(macCLIENT); \
CTemporaryModsNetwork TempNetwork(UMods, macNETWORK); \
CTemporaryModsClient TempClient(UMods, macCLIENT); \
if (UMods.macFUNC) { \
UMods.SetNetwork(pOldUNetwork); \
UMods.SetClient(pOldUClient); \
*macEXITER = true; \
} \
UMods.SetNetwork(pOldUNetwork); \
UMods.SetClient(pOldUClient); \
} \
} while (false)
@@ -109,13 +94,10 @@ extern bool ZNC_NO_NEED_TO_DO_ANYTHING_ON_MODULE_CALL_EXITER;
} \
if (macNETWORK != nullptr) { \
CModules& NMods = macNETWORK->GetModules(); \
CClient* pOldNClient = NMods.GetClient(); \
NMods.SetClient(macCLIENT); \
CTemporaryModsClient TempClient(NMods, macCLIENT); \
if (NMods.macFUNC) { \
NMods.SetClient(pOldNClient); \
*macEXITER = true; \
} \
NMods.SetClient(pOldNClient); \
} \
} while (false)
+1 -1
View File
@@ -66,7 +66,7 @@ void OnServerCapResult(const CString& sCap, bool bSuccess)
void OnClientAttached()
void OnClientDetached()
EModRet OnTimerAutoJoin(CChan& Channel)
bool OnEmbeddedWebRequest(CWebSock& WebSock, const CString& sPageName, CTemplate& Tmpl)
bool OnEmbeddedWebRequest(CWebSock& WebSock, const CString& sPageName, CTemplate& Tmpl)=false
EModRet OnAddNetwork(CIRCNetwork& Network, CString& sErrorRet)
EModRet OnDeleteNetwork(CIRCNetwork& Network)
EModRet OnSendToClient(CString& sLine, CClient& Client)
+1 -1
View File
@@ -66,7 +66,7 @@ void OnServerCapResult(const CString& sCap, bool bSuccess)
void OnClientAttached()
void OnClientDetached()
EModRet OnTimerAutoJoin(CChan& Channel)
bool OnEmbeddedWebRequest(CWebSock& WebSock, const CString& sPageName, CTemplate& Tmpl)
bool OnEmbeddedWebRequest(CWebSock& WebSock, const CString& sPageName, CTemplate& Tmpl)=false
EModRet OnAddNetwork(CIRCNetwork& Network, CString& sErrorRet)
EModRet OnDeleteNetwork(CIRCNetwork& Network)
EModRet OnSendToClient(CString& sLine, CClient& Client)
+6 -6
View File
@@ -742,7 +742,7 @@ class CWebAdminMod : public CModule {
CTemplate& mod = Tmpl.AddRow("EmbeddedModuleLoop");
mod.insert(Tmpl.begin(), Tmpl.end());
mod["WebadminAction"] = "display";
if ((*i)->OnEmbeddedWebRequest(WebSock, "webadmin/channel",
if ((*i)->DoEmbeddedWebRequest(WebSock, "webadmin/channel",
mod)) {
mod["Embed"] = WebSock.FindTmpl(*i, "WebadminChan.tmpl");
mod["ModName"] = (*i)->GetModName();
@@ -816,7 +816,7 @@ class CWebAdminMod : public CModule {
TmplMod["ChanName"] = pChan->GetName();
TmplMod["WebadminAction"] = "change";
FOR_EACH_MODULE(it, pNetwork) {
(*it)->OnEmbeddedWebRequest(WebSock, "webadmin/channel", TmplMod);
(*it)->DoEmbeddedWebRequest(WebSock, "webadmin/channel", TmplMod);
}
if (!CZNC::Get().WriteConfig()) {
@@ -1027,7 +1027,7 @@ class CWebAdminMod : public CModule {
CTemplate& mod = Tmpl.AddRow("EmbeddedModuleLoop");
mod.insert(Tmpl.begin(), Tmpl.end());
mod["WebadminAction"] = "display";
if ((*i)->OnEmbeddedWebRequest(WebSock, "webadmin/network",
if ((*i)->DoEmbeddedWebRequest(WebSock, "webadmin/network",
mod)) {
mod["Embed"] = WebSock.FindTmpl(*i, "WebadminNetwork.tmpl");
mod["ModName"] = (*i)->GetModName();
@@ -1246,7 +1246,7 @@ class CWebAdminMod : public CModule {
TmplMod["Name"] = pNetwork->GetName();
TmplMod["WebadminAction"] = "change";
FOR_EACH_MODULE(it, make_pair(pUser, pNetwork)) {
(*it)->OnEmbeddedWebRequest(WebSock, "webadmin/network", TmplMod);
(*it)->DoEmbeddedWebRequest(WebSock, "webadmin/network", TmplMod);
}
if (!CZNC::Get().WriteConfig()) {
@@ -1670,7 +1670,7 @@ class CWebAdminMod : public CModule {
CTemplate& mod = Tmpl.AddRow("EmbeddedModuleLoop");
mod.insert(Tmpl.begin(), Tmpl.end());
mod["WebadminAction"] = "display";
if ((*i)->OnEmbeddedWebRequest(WebSock, "webadmin/user", mod)) {
if ((*i)->DoEmbeddedWebRequest(WebSock, "webadmin/user", mod)) {
mod["Embed"] = WebSock.FindTmpl(*i, "WebadminUser.tmpl");
mod["ModName"] = (*i)->GetModName();
}
@@ -1731,7 +1731,7 @@ class CWebAdminMod : public CModule {
TmplMod["Username"] = sUsername;
TmplMod["WebadminAction"] = "change";
FOR_EACH_MODULE(it, pUser) {
(*it)->OnEmbeddedWebRequest(WebSock, "webadmin/user", TmplMod);
(*it)->DoEmbeddedWebRequest(WebSock, "webadmin/user", TmplMod);
}
if (!CZNC::Get().WriteConfig()) {
+10 -19
View File
@@ -21,8 +21,8 @@
#include <znc/IRCNetwork.h>
#include <znc/Query.h>
using std::set;
using std::map;
using std::set;
using std::vector;
#define CALLMOD(MOD, CLIENT, USER, NETWORK, FUNC) \
@@ -30,10 +30,9 @@ using std::vector;
CModule* pModule = nullptr; \
if (NETWORK && (pModule = (NETWORK)->GetModules().FindModule(MOD))) { \
try { \
CClient* pOldClient = pModule->GetClient(); \
pModule->SetClient(CLIENT); \
CModCallProtector Inside(*pModule); \
CTemporaryModClient TempClient(*pModule, CLIENT); \
pModule->FUNC; \
pModule->SetClient(pOldClient); \
} catch (const CModule::EModException& e) { \
if (e == CModule::UNLOAD) { \
(NETWORK)->GetModules().UnloadModule(MOD); \
@@ -41,13 +40,10 @@ using std::vector;
} \
} else if ((pModule = (USER)->GetModules().FindModule(MOD))) { \
try { \
CClient* pOldClient = pModule->GetClient(); \
CIRCNetwork* pOldNetwork = pModule->GetNetwork(); \
pModule->SetClient(CLIENT); \
pModule->SetNetwork(NETWORK); \
CModCallProtector Inside(*pModule); \
CTemporaryModClient TempClient(*pModule, CLIENT); \
CTemporaryModNetwork TempNetwork(*pModule, NETWORK); \
pModule->FUNC; \
pModule->SetClient(pOldClient); \
pModule->SetNetwork(pOldNetwork); \
} catch (const CModule::EModException& e) { \
if (e == CModule::UNLOAD) { \
(USER)->GetModules().UnloadModule(MOD); \
@@ -55,16 +51,11 @@ using std::vector;
} \
} else if ((pModule = CZNC::Get().GetModules().FindModule(MOD))) { \
try { \
CClient* pOldClient = pModule->GetClient(); \
CIRCNetwork* pOldNetwork = pModule->GetNetwork(); \
CUser* pOldUser = pModule->GetUser(); \
pModule->SetClient(CLIENT); \
pModule->SetNetwork(NETWORK); \
pModule->SetUser(USER); \
CModCallProtector Inside(*pModule); \
CTemporaryModClient TempClient(*pModule, CLIENT); \
CTemporaryModNetwork TempNetwork(*pModule, NETWORK); \
CTemporaryModUser TempUser(*pModule, USER); \
pModule->FUNC; \
pModule->SetClient(pOldClient); \
pModule->SetNetwork(pOldNetwork); \
pModule->SetUser(pOldUser); \
} catch (const CModule::EModException& e) { \
if (e == CModule::UNLOAD) { \
CZNC::Get().GetModules().UnloadModule(MOD); \
+20 -30
View File
@@ -39,22 +39,11 @@ bool ZNC_NO_NEED_TO_DO_ANYTHING_ON_MODULE_CALL_EXITER;
#define MODUNLOADCHK(func) \
for (CModule* pMod : *this) { \
try { \
CClient* pOldClient = pMod->GetClient(); \
pMod->SetClient(m_pClient); \
CUser* pOldUser = nullptr; \
if (m_pUser) { \
pOldUser = pMod->GetUser(); \
pMod->SetUser(m_pUser); \
} \
CIRCNetwork* pNetwork = nullptr; \
if (m_pNetwork) { \
pNetwork = pMod->GetNetwork(); \
pMod->SetNetwork(m_pNetwork); \
} \
CModCallProtector inside(*pMod); \
CTemporaryModClient TempClient(*pMod, m_pClient); \
CTemporaryModUser TempUser(*pMod, m_pUser); \
CTemporaryModNetwork TempNetwork(*pMod, m_pNetwork); \
pMod->func; \
if (m_pUser) pMod->SetUser(pOldUser); \
if (m_pNetwork) pMod->SetNetwork(pNetwork); \
pMod->SetClient(pOldClient); \
} catch (const CModule::EModException& e) { \
if (e == CModule::UNLOAD) { \
UnloadModule(pMod->GetModName()); \
@@ -66,23 +55,12 @@ bool ZNC_NO_NEED_TO_DO_ANYTHING_ON_MODULE_CALL_EXITER;
bool bHaltCore = false; \
for (CModule* pMod : *this) { \
try { \
CModCallProtector inside(*pMod); \
CModule::EModRet e = CModule::CONTINUE; \
CClient* pOldClient = pMod->GetClient(); \
pMod->SetClient(m_pClient); \
CUser* pOldUser = nullptr; \
if (m_pUser) { \
pOldUser = pMod->GetUser(); \
pMod->SetUser(m_pUser); \
} \
CIRCNetwork* pNetwork = nullptr; \
if (m_pNetwork) { \
pNetwork = pMod->GetNetwork(); \
pMod->SetNetwork(m_pNetwork); \
} \
CTemporaryModClient TempClient(*pMod, m_pClient); \
CTemporaryModUser TempUser(*pMod, m_pUser); \
CTemporaryModNetwork TempNetwork(*pMod, m_pNetwork); \
e = pMod->func; \
if (m_pUser) pMod->SetUser(pOldUser); \
if (m_pNetwork) pMod->SetNetwork(pNetwork); \
pMod->SetClient(pOldClient); \
if (e == CModule::HALTMODS) { \
break; \
} else if (e == CModule::HALTCORE) { \
@@ -205,6 +183,8 @@ void CModule::SetUser(CUser* pUser) { m_pUser = pUser; }
void CModule::SetNetwork(CIRCNetwork* pNetwork) { m_pNetwork = pNetwork; }
void CModule::SetClient(CClient* pClient) { m_pClient = pClient; }
bool CModule::IsCallStackEmpty() const { return m_iCallStackDepth == 0; }
CString CModule::ExpandString(const CString& sStr) const {
CString sRet;
return ExpandString(sStr, sRet);
@@ -622,6 +602,11 @@ bool CModule::OnEmbeddedWebRequest(CWebSock& WebSock, const CString& sPageName,
CTemplate& Tmpl) {
return false;
}
bool CModule::DoEmbeddedWebRequest(CWebSock& WebSock, const CString& sPageName,
CTemplate& Tmpl) {
CModCallProtector inside(*this);
return OnEmbeddedWebRequest(WebSock, sPageName, Tmpl);
}
// !Webmods
bool CModule::OnLoad(const CString& sArgs, CString& sMessage) {
@@ -1953,6 +1938,11 @@ bool CModules::UnloadModule(const CString& sModule, CString& sRetMsg) {
return false;
}
if (!pModule->IsCallStackEmpty()) {
sRetMsg = t_f("Module [{1}] is being called, cannot unload.")(sMod);
return false;
}
bool bSuccess;
bool bHandled = false;
_GLOBALMODULECALL(OnModuleUnloading(pModule, bSuccess, sRetMsg),