Summary
ibmmq 2.0.5 crashes in mqotel.otel_get_trace_after() when OpenTelemetry is enabled and a Queue.get() path uses a usable gmo.MsgHandle.
The failure is:
UnboundLocalError: cannot access local variable 'hc' where it is not associated with a value
The immediate problem is that hc is referenced before it is assigned:
removed = 0
mh = gmo.MsgHandle
if _is_usable_handle(mh):
temp_msg_handle = MessageHandle(qmgr=hc, dup_handle=mh)
...
tmp_ho = ho
hc = ho.get_queue_manager()
In code/ibmmq/mqotel.py, hc is used in MessageHandle(qmgr=hc, dup_handle=mh) before hc = ho.get_queue_manager() is executed later in the same block.
When this is hit
This becomes reachable when all of the following are true:
- OpenTelemetry is importable, so
mqotel is auto-enabled.
- A message is received through the OTel post-processing path.
gmo.MsgHandle contains a usable handle, for example when MQGMO_PROPERTIES_IN_HANDLE is used.
A real-world example is a consumer that creates a message handle and assigns it to gmo.MsgHandle before Queue.get().
Minimal reproduction test
I added this focused unit test locally to reproduce the bug without needing a live queue manager or compiled extension. It loads mqotel.py directly with stubbed dependencies and exercises the exact failing branch.
"""Regression coverage for mqotel message-handle processing."""
import importlib.util
import sys
import types
import unittest
from pathlib import Path
from unittest import mock
MQOTEL_PATH = Path(__file__).resolve().parents[1] / "ibmmq" / "mqotel.py"
def _noop(*args, **kwargs):
return None
class _FakeCMQC:
MQGMO_PROPERTIES_FORCE_MQRFH2 = 0x0001
MQGMO_PROPERTIES_IN_HANDLE = 0x0002
MQGMO_NO_PROPERTIES = 0x0004
MQGMO_PROPERTIES_COMPATIBILITY = 0x0008
MQOO_INPUT_AS_Q_DEF = 0x0010
MQOO_INPUT_SHARED = 0x0020
MQOO_INPUT_EXCLUSIVE = 0x0040
MQHM_NONE = 0
MQHM_UNUSABLE_HMSG = -1
MQHO_NONE = 0
MQHO_UNUSABLE_HOBJ = -1
MQIMPO_CONVERT_VALUE = 0x0080
MQIMPO_INQ_FIRST = 0x0100
MQRC_PROPERTY_NOT_AVAILABLE = 2492
MQFMT_RF_HEADER_2 = b"MQHRF2 "
class _FakeMQMIError(Exception):
def __init__(self, reason):
super().__init__(reason)
self.reason = reason
class _FakeMessageHandle:
def __init__(self, qmgr=None, dup_handle=None):
self.qmgr = qmgr
self.dup_handle = dup_handle
def inq(self, impo, pd, prop):
raise _FakeMQMIError(_FakeCMQC.MQRC_PROPERTY_NOT_AVAILABLE)
def get_handle(self):
return self.dup_handle or 1
def dlt(self):
return None
class _FakePD:
pass
class _FakeIMPO:
def __init__(self):
self.Options = 0
class _FakeOTelFunctions:
disc = None
open = None
close = None
put_trace_before = None
put_trace_after = None
get_trace_before = None
get_trace_after = None
def _load_mqotel_module():
invalid_span = object()
fake_trace = types.ModuleType("opentelemetry.trace")
fake_trace.INVALID_SPAN = invalid_span
fake_trace.get_current_span = lambda: invalid_span
fake_opentelemetry = types.ModuleType("opentelemetry")
fake_opentelemetry.trace = fake_trace
fake_mqlog = types.ModuleType("mqlog")
for func_name in ("trace_entry", "trace_exit", "debug", "trace", "error"):
setattr(fake_mqlog, func_name, _noop)
fake_mqcommon = types.ModuleType("mqcommon")
fake_mqcommon.OTelFunctions = _FakeOTelFunctions
fake_mqcommon.__all__ = ["OTelFunctions"]
fake_mqerrors = types.ModuleType("mqerrors")
fake_mqerrors.MQMIError = _FakeMQMIError
fake_mqerrors.__all__ = ["MQMIError"]
fake_ibmmq = types.ModuleType("ibmmq")
fake_ibmmq.CMQC = _FakeCMQC
fake_ibmmq.MessageHandle = _FakeMessageHandle
fake_ibmmq.Queue = type("Queue", (), {})
fake_ibmmq.OD = type("OD", (), {})
fake_ibmmq.PD = _FakePD
fake_ibmmq.IMPO = _FakeIMPO
fake_ibmmq.SMPO = type("SMPO", (), {})
fake_ibmmq.RFH2 = type("RFH2", (), {})
fake_modules = {
"opentelemetry": fake_opentelemetry,
"opentelemetry.trace": fake_trace,
"mqlog": fake_mqlog,
"mqcommon": fake_mqcommon,
"mqerrors": fake_mqerrors,
"ibmmq": fake_ibmmq,
}
spec = importlib.util.spec_from_file_location("mqotel_bug_repro", MQOTEL_PATH)
module = importlib.util.module_from_spec(spec)
with mock.patch.dict(sys.modules, fake_modules):
spec.loader.exec_module(module)
return module
class TestMQOTel(unittest.TestCase):
def test_get_trace_after_with_msg_handle_returns_without_crashing(self):
mqotel = _load_mqotel_module()
ho = types.SimpleNamespace(
get_handle=lambda: 37,
get_queue_manager=lambda: types.SimpleNamespace(get_handle=lambda: 91),
)
gmo = types.SimpleNamespace(
MsgHandle=1234,
Options=_FakeCMQC.MQGMO_PROPERTIES_IN_HANDLE,
)
md = types.SimpleNamespace(Format=b"", CodedCharSetId=0, Encoding=0)
removed = mqotel.otel_get_trace_after(ho, gmo, md, None, b"payload", False)
self.assertEqual(0, removed)
Running it with:
python -m unittest discover -s code/tests -p 'test_mqotel.py'
produces:
ERROR: test_get_trace_after_with_msg_handle_returns_without_crashing
Traceback (most recent call last):
File ".../code/tests/test_mqotel.py", line 139, in test_get_trace_after_with_msg_handle_returns_without_crashing
removed = mqotel.otel_get_trace_after(ho, gmo, md, None, b"payload", False)
File ".../code/ibmmq/mqotel.py", line 520, in otel_get_trace_after
temp_msg_handle = MessageHandle(qmgr=hc, dup_handle=mh)
UnboundLocalError: cannot access local variable 'hc' where it is not associated with a value
Expected behavior
otel_get_trace_after() should not crash when gmo.MsgHandle is usable. It should obtain the queue manager handle before constructing the duplicate MessageHandle and continue processing normally.
Possible fix
Move:
hc = ho.get_queue_manager()
so it executes before:
temp_msg_handle = MessageHandle(qmgr=hc, dup_handle=mh)
Workaround
Setting MQIPY_NOOTEL=true avoids the broken OTel integration path, but that is only a workaround and disables MQ-specific OTel propagation.
Summary
ibmmq2.0.5 crashes inmqotel.otel_get_trace_after()when OpenTelemetry is enabled and aQueue.get()path uses a usablegmo.MsgHandle.The failure is:
The immediate problem is that
hcis referenced before it is assigned:In
code/ibmmq/mqotel.py,hcis used inMessageHandle(qmgr=hc, dup_handle=mh)beforehc = ho.get_queue_manager()is executed later in the same block.When this is hit
This becomes reachable when all of the following are true:
mqotelis auto-enabled.gmo.MsgHandlecontains a usable handle, for example whenMQGMO_PROPERTIES_IN_HANDLEis used.A real-world example is a consumer that creates a message handle and assigns it to
gmo.MsgHandlebeforeQueue.get().Minimal reproduction test
I added this focused unit test locally to reproduce the bug without needing a live queue manager or compiled extension. It loads
mqotel.pydirectly with stubbed dependencies and exercises the exact failing branch.Running it with:
python -m unittest discover -s code/tests -p 'test_mqotel.py'produces:
Expected behavior
otel_get_trace_after()should not crash whengmo.MsgHandleis usable. It should obtain the queue manager handle before constructing the duplicateMessageHandleand continue processing normally.Possible fix
Move:
so it executes before:
Workaround
Setting
MQIPY_NOOTEL=trueavoids the broken OTel integration path, but that is only a workaround and disables MQ-specific OTel propagation.