Summary
ray_table_nrows() and ray_table_ncols() segfault when handed a value
that is not a table. They are part of the public embedding API in
rayforce.h, which takes an opaque ray_t* and documents no
precondition, so from an embedder's side these read as total functions.
Related but separate from #565 (which is a __VM NULL guard on a
different path).
Reproduction
Against dev at c27fdc34. The value is an ordinary scalar reply to
(+ 1 2) over IPC — type=-5:
ray_runtime_create(0, NULL);
ray_poll_t* p = ray_poll_create(); ray_runtime_set_poll(p);
int64_t h = ray_ipc_connect("127.0.0.1", port, NULL, NULL, 2000);
ray_t* r = ray_ipc_send(h, ray_str("(+ 1 2)", 7));
printf("type=%d\n", r->type); /* -5 */
ray_table_nrows(r); /* SIGSEGV */
Probing each accessor on that same reply, with a SIGSEGV handler:
scalar IPC reply, type=-5:
table_nrows -> SIGSEGV
table_ncols -> SIGSEGV
dict_len -> 0
dict_len returning 0 is not a guard — ray_dict_len is written the
same way (ray_dict_keys(d) then keys->len); this input just happens
to land on a falsy pointer. Which is the awkward part: the same call is
benign on one value and fatal on another. A locally built
ray_i64(42) passed to ray_table_nrows returns 0 here; only the
IPC-deserialized atom crashes. That makes the misuse very easy to ship —
it is how we found it, in a smoke test that had been passing a scalar
reply to ray_table_nrows for a while under a try ... with that of
course cannot catch a segfault.
Why this isn't a complaint about the design
I realise the accessors are written against block layout, not the type
tag, and that this is deliberate: your own call sites check first
(if (x->type == RAY_TABLE) in src/ops/agg.c:546, the ternary in
src/core/qlog.c:38), because the evaluator has already established the
type during builtin dispatch. Keeping ray_table_nrows branch-free
matters on paths like agg_engine.c's parallel-size test. And
ray_table_validate_rectangular() is there for callers who want an
explicit check.
That works well for the evaluator. It works less well for an embedder,
which has no evaluator in front of the API — ray_t* is one type, so
there is nothing to check against except the tag you already store.
Worth noting ray_table_ncols already returns 0 defensively:
int64_t ray_table_ncols(ray_t* tbl) {
if (!tbl || RAY_IS_ERR(tbl)) return 0;
It rejects NULL and errors but not a wrong tag, which is the case that
actually crashes.
Possible resolutions, in your preference order not mine
- Make them total. Extend the existing defensive return: add
tbl->type != RAY_TABLE to the guard ray_table_ncols already has,
and give ray_table_nrows the same. One predictable, well-branch-
predicted comparison against a field already in cache. Same for
ray_dict_len.
- Document the precondition. If the branch is unacceptable on hot
paths, saying so in rayforce.h would be enough — I grepped the
whole header for caller must / precondition / undefined /
assumes and there is currently nothing, so an embedder has no way
to learn this short of a crash.
- Checked variants.
ray_table_nrows_checked() or similar, leaving
the hot path untouched.
Any of these solves it for us; (2) alone would have been enough. We have
guarded it on our side already, so there is nothing urgent here — filing
it because the next embedder will hit the same thing, and because a
public API segfaulting on an in-range value of its own parameter type
seemed worth reporting rather than working around silently.
Happy to send a PR for whichever option you prefer.
Summary
ray_table_nrows()andray_table_ncols()segfault when handed a valuethat is not a table. They are part of the public embedding API in
rayforce.h, which takes an opaqueray_t*and documents noprecondition, so from an embedder's side these read as total functions.
Related but separate from #565 (which is a
__VMNULL guard on adifferent path).
Reproduction
Against
devatc27fdc34. The value is an ordinary scalar reply to(+ 1 2)over IPC —type=-5:Probing each accessor on that same reply, with a SIGSEGV handler:
dict_lenreturning 0 is not a guard —ray_dict_lenis written thesame way (
ray_dict_keys(d)thenkeys->len); this input just happensto land on a falsy pointer. Which is the awkward part: the same call is
benign on one value and fatal on another. A locally built
ray_i64(42)passed toray_table_nrowsreturns 0 here; only theIPC-deserialized atom crashes. That makes the misuse very easy to ship —
it is how we found it, in a smoke test that had been passing a scalar
reply to
ray_table_nrowsfor a while under atry ... withthat ofcourse cannot catch a segfault.
Why this isn't a complaint about the design
I realise the accessors are written against block layout, not the type
tag, and that this is deliberate: your own call sites check first
(
if (x->type == RAY_TABLE)insrc/ops/agg.c:546, the ternary insrc/core/qlog.c:38), because the evaluator has already established thetype during builtin dispatch. Keeping
ray_table_nrowsbranch-freematters on paths like
agg_engine.c's parallel-size test. Andray_table_validate_rectangular()is there for callers who want anexplicit check.
That works well for the evaluator. It works less well for an embedder,
which has no evaluator in front of the API —
ray_t*is one type, sothere is nothing to check against except the tag you already store.
Worth noting
ray_table_ncolsalready returns 0 defensively:It rejects NULL and errors but not a wrong tag, which is the case that
actually crashes.
Possible resolutions, in your preference order not mine
tbl->type != RAY_TABLEto the guardray_table_ncolsalready has,and give
ray_table_nrowsthe same. One predictable, well-branch-predicted comparison against a field already in cache. Same for
ray_dict_len.paths, saying so in
rayforce.hwould be enough — I grepped thewhole header for
caller must/precondition/undefined/assumesand there is currently nothing, so an embedder has no wayto learn this short of a crash.
ray_table_nrows_checked()or similar, leavingthe hot path untouched.
Any of these solves it for us; (2) alone would have been enough. We have
guarded it on our side already, so there is nothing urgent here — filing
it because the next embedder will hit the same thing, and because a
public API segfaulting on an in-range value of its own parameter type
seemed worth reporting rather than working around silently.
Happy to send a PR for whichever option you prefer.