modpython: invalid capability owner downcast after a Python callback error #1104

Open
opened 2026-10-27 15:23:37 +01:00 by adam · 2 comments
Owner

Summary

modpython lets Python register capability callbacks on the native corecaps module through CModule. If a callback raises, the error handler casts that CCoreCaps object to CPyModule. In a local ZNC 1.11.x test, UBSan reported invalid downcasts on both server and authenticated-client CAP paths; a separate uninstrumented server-path run ended with SIGSEGV.

Affected code

In modpython.i:234–247 exposes the method, and Modules.cpp:1233–1236 stores the receiving module via pCap->SetModule(this). On a callback exception, modpython.cpp:499 (server) or :515 (client) calls ((CPyModule*)GetModule())->GetPyExceptionStr() even when the stored owner is CCoreCaps.

Reproduction

Use a Python-enabled ZNC test instance with corecaps, modpython, and this operator-written global module loaded in that order. A local fake IRC server listens on one Unix socket, which ZNC connects to; an authenticated test client connects to ZNC's separate Unix listener. Use a disposable datadir and credentials generated with the matching ZNC binary's --makepass.

ownerprobe.py (the code used for the server and client runs):

import znc

class ownerprobe(znc.Module):
    description = "Local capability owner test"
    module_types = [znc.CModInfo.GlobalModule]

    def OnLoad(self, args, ret):
        def server_change(network, state):
            raise RuntimeError("server callback")

        def client_change(client, state):
            raise RuntimeError("client callback")

        modules = znc.CZNC.Get().GetModules()
        try:
            count = len(modules)
        except Exception:
            count = modules.size()
        for i in range(count):
            owner = modules[i]
            if str(owner.GetModName()) == "corecaps":
                owner.AddServerDependentCapability(
                    "testcapx", server_change, client_change)
                return True
        return True  # no target: skip registration
  • Server path: After ZNC sends CAP LS 302, the fake server sends 001/376, then :ircd.test CAP <nick> NEW testcapx=value. ZNC requests CAP REQ :testcapx; the server answers :ircd.test CAP <nick> ACK :testcapx. The raising callback reaches OnServerChangedSupport.
  • Client path: An authenticated local client sends CAP LS 302, NICK, USER, CAP END, then CAP REQ :-testcapx. The raising callback reaches OnClientChangedSupport.

Observed results

Both sanitized ZNC children exited 1. Distinct report exceptions:

modules/modpython.cpp:499:27: runtime error: downcast ... does not point to an object of type 'CPyModule'
note: object is of type 'CCoreCaps'
#0 CPyCapability::OnServerChangedSupport(CIRCNetwork*, bool)
#3 CIRCSock::OnCapabilityMessage(CMessage&)

modules/modpython.cpp:515:27: runtime error: downcast ... does not point to an object of type 'CPyModule'
note: object is of type 'CCoreCaps'
#0 CPyCapability::OnClientChangedSupport(CClient*, bool)
#3 CClient::HandleCap(CMessage const&)

An earlier direct uninstrumented Release server-path run recorded ZNC child exit -11 (SIGSEGV). A separate GDB run stopped at:

#0 CModPython::GetPyExceptionStr()
#1 CPyCapability::OnServerChangedSupport(CIRCNetwork*, bool)
#2 CModule::InternalServerDependentCapsOnServerCapResult(CString const&, bool)
#3 CModules::OnServerCapResult(CString const&, bool)
#4 CIRCSock::OnCapabilityMessage(CMessage&)
#5 CIRCSock::ReadLine(CString const&)

Environment and scope

The runs used a local 1.11.x-git-unknown ZNC build; its complete upstream revision is unknown. The sanitized build used x86_64 Clang 18.1.3, Python 3.12, SWIG 4.2.0, and -O1 -g -fno-omit-frame-pointer -fno-optimize-sibling-calls -fsanitize=address,undefined -fno-sanitize-recover=bounds,object-size,vptr -fPIE -Wno-everything.

## Summary `modpython` lets Python register capability callbacks on the native `corecaps` module through `CModule`. If a callback raises, the error handler casts that `CCoreCaps` object to `CPyModule`. In a local ZNC 1.11.x test, UBSan reported invalid downcasts on both server and authenticated-client CAP paths; a separate uninstrumented server-path run ended with SIGSEGV. ## Affected code In [`modpython.i:234–247`](https://github.com/znc/znc/blob/bfba882ccc5a1672897941e3dcdf9bdcb451b425/modules/modpython/modpython.i#L234-L247) exposes the method, and [`Modules.cpp:1233–1236`](https://github.com/znc/znc/blob/bfba882ccc5a1672897941e3dcdf9bdcb451b425/src/Modules.cpp#L1233-L1236) stores the receiving module via `pCap->SetModule(this)`. On a callback exception, [`modpython.cpp:499`](https://github.com/znc/znc/blob/bfba882ccc5a1672897941e3dcdf9bdcb451b425/modules/modpython.cpp#L492-L502) (server) or [`:515`](https://github.com/znc/znc/blob/bfba882ccc5a1672897941e3dcdf9bdcb451b425/modules/modpython.cpp#L508-L518) (client) calls `((CPyModule*)GetModule())->GetPyExceptionStr()` even when the stored owner is `CCoreCaps`. ## Reproduction Use a Python-enabled ZNC test instance with `corecaps`, `modpython`, and this operator-written global module loaded in that order. A local fake IRC server listens on one Unix socket, which ZNC connects to; an authenticated test client connects to ZNC's separate Unix listener. Use a disposable datadir and credentials generated with the matching ZNC binary's `--makepass`. `ownerprobe.py` (the code used for the server and client runs): ```python import znc class ownerprobe(znc.Module): description = "Local capability owner test" module_types = [znc.CModInfo.GlobalModule] def OnLoad(self, args, ret): def server_change(network, state): raise RuntimeError("server callback") def client_change(client, state): raise RuntimeError("client callback") modules = znc.CZNC.Get().GetModules() try: count = len(modules) except Exception: count = modules.size() for i in range(count): owner = modules[i] if str(owner.GetModName()) == "corecaps": owner.AddServerDependentCapability( "testcapx", server_change, client_change) return True return True # no target: skip registration ``` - **Server path:** After ZNC sends `CAP LS 302`, the fake server sends `001`/`376`, then `:ircd.test CAP <nick> NEW testcapx=value`. ZNC requests `CAP REQ :testcapx`; the server answers `:ircd.test CAP <nick> ACK :testcapx`. The raising callback reaches `OnServerChangedSupport`. - **Client path:** An authenticated local client sends `CAP LS 302`, `NICK`, `USER`, `CAP END`, then `CAP REQ :-testcapx`. The raising callback reaches `OnClientChangedSupport`. ## Observed results Both sanitized ZNC children exited **1**. Distinct report exceptions: ```text modules/modpython.cpp:499:27: runtime error: downcast ... does not point to an object of type 'CPyModule' note: object is of type 'CCoreCaps' #0 CPyCapability::OnServerChangedSupport(CIRCNetwork*, bool) #3 CIRCSock::OnCapabilityMessage(CMessage&) modules/modpython.cpp:515:27: runtime error: downcast ... does not point to an object of type 'CPyModule' note: object is of type 'CCoreCaps' #0 CPyCapability::OnClientChangedSupport(CClient*, bool) #3 CClient::HandleCap(CMessage const&) ``` An earlier direct uninstrumented Release server-path run recorded ZNC child exit **-11** (SIGSEGV). A separate GDB run stopped at: ```text #0 CModPython::GetPyExceptionStr() #1 CPyCapability::OnServerChangedSupport(CIRCNetwork*, bool) #2 CModule::InternalServerDependentCapsOnServerCapResult(CString const&, bool) #3 CModules::OnServerCapResult(CString const&, bool) #4 CIRCSock::OnCapabilityMessage(CMessage&) #5 CIRCSock::ReadLine(CString const&) ``` ## Environment and scope The runs used a local `1.11.x-git-unknown` ZNC build; its complete upstream revision is unknown. The sanitized build used x86_64 Clang 18.1.3, Python 3.12, SWIG 4.2.0, and `-O1 -g -fno-omit-frame-pointer -fno-optimize-sibling-calls -fsanitize=address,undefined -fno-sanitize-recover=bounds,object-size,vptr -fPIE -Wno-everything`. <!-- github-sync-id:5975864981 -->
Author
Owner

Yes, modpython doesn't prevent the module author from shooting themselves in foot.

ZNC API gives you access to other modules. What's the point of calling AddServerDependentCapability not on "self", but on some other module?

Yes, modpython doesn't prevent the module author from shooting themselves in foot. ZNC API gives you access to other modules. What's the point of calling AddServerDependentCapability not on "self", but on some other module? <!-- github-comment-id:6336635203 -->
Author
Owner

Btw, if you do something like that from C++ module, and attach callback to some other module instead of "this", it can also cause crash in certain cases.

Btw, if you do something like that from C++ module, and attach callback to some other module instead of "this", it can also cause crash in certain cases. <!-- github-comment-id:6350898788 -->
Sign in to join this conversation.
No labels
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: adam/znc#1104