Skip to content

Fix: GetMany queries that succeed with a nil result (issue #3070) - #3076

Open
dgarnier wants to merge 5 commits into
MDSplus:alphafrom
dgarnier:fix-getmany-null-result
Open

Fix: GetMany queries that succeed with a nil result (issue #3070)#3076
dgarnier wants to merge 5 commits into
MDSplus:alphafrom
dgarnier:fix-getmany-null-result

Conversation

@dgarnier

Copy link
Copy Markdown
Contributor

Fix: GetMany queries that succeed with a nil result (issue #3070)

Problem

A GetMany query whose expression evaluates to nil (e.g. GETNCI(<node>, "RECORD")
of a clock whose record is * : * : 5E-6 — a Range with a missing begin) crashed
the mdsip worker instead of returning { 'value': missing }, the way a plain
get() of the same expression does.

The crash had several layers; the root cause turned out to be broader than the
Dictionary accessors that first showed up in a debugger.

Root cause

The C++ object layer uses a NULL Data * to mean missing (*) — it is what
execute() returns for a nil expression, and what convertToApdDsc() emits as a
null descriptor pointer. The machinery was inconsistent about tolerating that NULL:

  1. Dictionary::getItem / setItem dereferenced the stored value without a
    NULL check, so storing/reading a missing value segfaulted. (fixed in the header)

  2. deleteData(Data *) dereferenced data->refCount with no NULL guard.
    This is the real culprit and the one the fix below targets.

deleteData(NULL) is reached on ordinary cleanup paths, because AutoData<T> — the
RAII wrapper used throughout the mdsip object code — calls deleteData(ptr)
unconditionally in its destructor, and it routinely wraps the (nullable) result of
getItem():

  • Server, getManyObj (mdsipobjects.cpp): a query may legitimately omit the
    args key — the server explicitly supports that (if (argsData.get() && argsData->len() > 0)).
    But AutoData<List> argsData((List *)currArg->getItem(&argsKey)) then wraps the
    NULL that getItem returns for the absent key, and destroying it at the end of
    each loop iteration calls deleteData(NULL).
  • Client, GetMany::get() (mdsipobjects.cpp): for a query that succeeds with a
    nil result, AutoData<Data> result(dictionary->getItem(&valueKey)) wraps NULL, so
    the same deleteData(NULL) fires on the way out — i.e. the crash also reproduces on
    a real client reading back a nil value, not just inside the worker.

Why it never showed up before: the production C++ GetMany client always attaches
an args key (an empty List when there are no args), so getItem(&argsKey) was never
NULL in normal use. The new test exercises the args-less path on purpose.

The fix, and why it lives in deleteData

void MDSplus::deleteData(Data *data)
{
  if (!data)      // NULL == missing ("*"): accept it as a no-op
    return;
  ...
}

We considered fixing only getManyObj, but that would patch one call site and leave
the same defect live on the client (GetMany::get()) and at every other
AutoData<X>(getItem(...)) in the codebase. The NULL reaching deleteData is not
specific to getManyObj — it is the general contract that getItem()/execute()
return NULL for "missing" and that AutoData is an RAII wrapper you can hand that NULL
to. getManyObj is correct as written; it was simply relying on a deleteData that
couldn't hold up its end.

So the fix belongs at the shared choke point. deleteData is the right one: it matches
C++'s own delete nullptr semantics, matches the already-NULL-tolerant helpers in the
same file (e.g. getMember), and covers both the AutoData destructor and the raw
deleteData(descs[i]) calls. (Apd::propagateDeletion already guards its own elements,
which is exactly the convention we are now making universal.)

Tests

mdsobjects/cpp/testing/MdsGetManyTest.cpp (new) covers three levels:

  1. A Dictionary accepts a missing (NULL) value — setItem/getItem/replace round trips.
  2. A missing value survives the serialize / deserialize round trip used to transport the
    result dictionary.
  3. End-to-end: GetManyExecute() on a batch containing a nil-result query returns
    { 'value': missing } (answered, not an error) instead of crashing.

macOS test-harness changes (required to even see this failure)

Two changes to the Check-based C test harness were needed so that macOS reports crashes
and timeouts the way Linux does — without them, this bug manifested as a silent hang and
the test appeared to "pass" or stall rather than fail:

  • testing/check_backend.c — On macOS a hardware fault (null deref, illegal
    instruction, …) is delivered to the task-level Mach exception port (the system crash
    reporter), which parks the faulting thread instead of terminating the process. The
    forked test child then never dies, the parent blocks forever in waitpid(), and the
    whole run stalls. In __setup_child() we now clear the child's exception ports
    (task_set_exception_ports(..., EXC_MASK_BAD_ACCESS | EXC_MASK_BAD_INSTRUCTION | EXC_MASK_ARITHMETIC, MACH_PORT_NULL, ...)), so a fault falls through to the default
    action and a crashing test dies promptly and is reported, just like on Linux.

  • testing/backends/check/lib/macos_timer.h — macOS lacks POSIX timer_create
    delivery, so the harness emulates it with a dispatch timer. The default expiration
    handler previously called signal(SIGALRM, NULL), which only reset the SIGALRM
    disposition and never actually raised the signal — so test timeouts silently never
    fired on macOS and a hung child stalled the runner indefinitely. It now does
    kill(getpid(), SIGALRM) to emulate real POSIX-timer delivery, so the harness'
    SIGALRM handler runs and kills the timed-out child.

Together these make a segfault/hang in a test surface as a reported failure on macOS
instead of a silent stall — which is how the remaining deleteData crash was found
after the header guards were added.

Fixes #3070.

dgarnier and others added 5 commits July 21, 2026 18:48
…3070)

A GetMany batch containing a query that evaluates successfully to a
nil/missing result crashes the mdsip worker: getManyObj() stores the
NULL Data* under the 'value' key, and Dictionary::setItem dereferences
it (data->incRefCount()).

Add MdsGetManyTest covering:
 - Dictionary holding a missing (NULL) value: setItem, getItem, len,
   and replacing missing<->data
 - the serialize/deserialize round trip of a dictionary with a missing
   value (the transport format of the GetManyExecute reply)
 - GetManyExecute() end to end with a query mirroring the field case,
   DATA(BEGIN_OF(BUILD_RANGE(*, *, 5E-6))), asserting the answer is
   { 'value': missing } exactly as a plain get() of the same expression
   would return

The test crashes without the corresponding fix to Dictionary::setItem /
getItem in include/mdsobjects.h.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
When a GetMany query evaluates successfully to nil, execute() returns a
NULL Data* (convertFromDsc() maps an empty XD to NULL), and getManyObj()
stores it under the 'value' key of the answer dictionary. That call,
Dictionary::setItem, dereferenced the value unconditionally
(data->incRefCount()), crashing the mdsip worker mid-reply. A plain
get() of the same expression harmlessly returns nil, and queries that
FAIL inside a GetMany are handled (an 'error' entry is returned) - only
the successful-nil case was fatal.

NULL-guard the value refcounting in Dictionary::setItem and getItem.
Everything below already treats a NULL Apd element as missing: the Apd
constructor and propagateDeletion() guard it, convertToApdDsc() emits a
null descriptor pointer for it, MdsSerializeDscOut() writes it as
offset 0, and both python clients deserialize it back as None/missing -
so the answer arrives as { 'value': * }, matching plain get().

Fixes MDSplus#3070

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
deleteData(NULL) dereferenced the pointer at data->refCount. Make it a
no-op on NULL, matching the "NULL == missing" convention the rest of the
Apd/Dictionary machinery already follows. This closes the remaining crash
on the args-less / nil-value GetMany paths (server getManyObj cleanup of
an absent "args" key, and client GetMany::get() of a missing value).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@WhoBrokeTheBuild
WhoBrokeTheBuild self-requested a review July 21, 2026 16:30
@WhoBrokeTheBuild WhoBrokeTheBuild self-assigned this Jul 21, 2026
@WhoBrokeTheBuild WhoBrokeTheBuild added the bug An unexpected problem or unintended behavior label Jul 21, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug An unexpected problem or unintended behavior

Projects

None yet

Development

Successfully merging this pull request may close these issues.

mdsip: GetMany batch containing a query that succeeds with a NULL result crashes the server worker (NULL deref in getManyObj)

2 participants