fix(ruby-lsp): survive allocation failure in the stdlib seed

The REG_* macros wrote through the return-type vector without checking
the allocation, and registered entries whose formatted QN came back NULL.
A shared ruby_seed_signature helper now leaves a failed vector out, so the
signature types as unknown (the fallback ruby_register_method already
uses), and REG_METHOD / REG_SMETHOD skip an entry whose formatted name is
NULL, since a NULL QN would reach strcmp in finalize and in every lookup.

rubylsp_stdlib_seed_survives_alloc_failure seeds a registry from an arena
that cannot allocate. Before: UBSan reported a store to a null pointer and
the run crashed with SEGV at ruby_stdlib_data.c:163. After: every
registered entry carries a name, the registry finalizes, and the Kernel
built-ins with literal names still resolve.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
This commit is contained in:
Martin Vogel committed 2026-11-01 21:17:28 +01:00
1 parent 214a9ac843
commit 435baf6bd8
2 files changed
+74 -43

No files matched your search

+47 -43
View File
@@ -40,55 +40,59 @@
cbm_registry_add_type(reg, rt); \
} while (0)
/* Register an instance method on receiver type `recv_` returning `ret_`. */
#define REG_METHOD(recv_, name_, ret_) \
do { \
memset(&rf, 0, sizeof(rf)); \
rf.min_params = -1; \
rf.qualified_name = cbm_arena_sprintf(arena, "%s.%s", (recv_), (name_)); \
rf.short_name = (name_); \
rf.receiver_type = (recv_); \
{ \
const CBMType **rets = (const CBMType **)cbm_arena_alloc(arena, 2 * sizeof(*rets)); \
rets[0] = (ret_); \
rets[1] = NULL; \
rf.signature = cbm_type_func(arena, NULL, NULL, rets); \
} \
cbm_registry_add_func(reg, rf); \
/* Signature `() -> ret`. Every seed allocation can fail (arena exhausted):
* a NULL return vector is left out, so the signature types as unknown (the
* same fallback ruby_register_method uses) instead of being written through. */
static const CBMType *ruby_seed_signature(CBMArena *arena, const CBMType *ret) {
const CBMType **rets = (const CBMType **)cbm_arena_alloc(arena, 2 * sizeof(*rets));
if (rets) {
rets[0] = ret;
rets[1] = NULL;
}
return cbm_type_func(arena, NULL, NULL, rets);
}
/* Register an instance method on receiver type `recv_` returning `ret_`.
* An entry whose formatted QN came back NULL is not registered: a NULL QN
* would reach strcmp in finalize and in every later lookup. */
#define REG_METHOD(recv_, name_, ret_) \
do { \
memset(&rf, 0, sizeof(rf)); \
rf.min_params = -1; \
rf.qualified_name = cbm_arena_sprintf(arena, "%s.%s", (recv_), (name_)); \
if (!rf.qualified_name) \
break; \
rf.short_name = (name_); \
rf.receiver_type = (recv_); \
rf.signature = ruby_seed_signature(arena, (ret_)); \
cbm_registry_add_func(reg, rf); \
} while (0)
/* Register a singleton (class-side) method: receiver key "<qn>.self"
* matches ruby_lookup_singleton_method's keying convention. */
#define REG_SMETHOD(recv_, name_, ret_) \
do { \
memset(&rf, 0, sizeof(rf)); \
rf.min_params = -1; \
rf.qualified_name = cbm_arena_sprintf(arena, "%s.%s", (recv_), (name_)); \
rf.short_name = (name_); \
rf.receiver_type = cbm_arena_sprintf(arena, "%s.self", (recv_)); \
{ \
const CBMType **rets = (const CBMType **)cbm_arena_alloc(arena, 2 * sizeof(*rets)); \
rets[0] = (ret_); \
rets[1] = NULL; \
rf.signature = cbm_type_func(arena, NULL, NULL, rets); \
} \
cbm_registry_add_func(reg, rf); \
* matches ruby_lookup_singleton_method's keying convention. Skipped when
* either formatted name came back NULL (see REG_METHOD). */
#define REG_SMETHOD(recv_, name_, ret_) \
do { \
memset(&rf, 0, sizeof(rf)); \
rf.min_params = -1; \
rf.qualified_name = cbm_arena_sprintf(arena, "%s.%s", (recv_), (name_)); \
rf.receiver_type = cbm_arena_sprintf(arena, "%s.self", (recv_)); \
if (!rf.qualified_name || !rf.receiver_type) \
break; \
rf.short_name = (name_); \
rf.signature = ruby_seed_signature(arena, (ret_)); \
cbm_registry_add_func(reg, rf); \
} while (0)
/* Register a global (Kernel) built-in function returning `ret_`. */
#define REG_BUILTIN(name_, ret_) \
do { \
memset(&rf, 0, sizeof(rf)); \
rf.min_params = -1; \
rf.qualified_name = (name_); \
rf.short_name = (name_); \
{ \
const CBMType **rets = (const CBMType **)cbm_arena_alloc(arena, 2 * sizeof(*rets)); \
rets[0] = (ret_); \
rets[1] = NULL; \
rf.signature = cbm_type_func(arena, NULL, NULL, rets); \
} \
cbm_registry_add_func(reg, rf); \
#define REG_BUILTIN(name_, ret_) \
do { \
memset(&rf, 0, sizeof(rf)); \
rf.min_params = -1; \
rf.qualified_name = (name_); \
rf.short_name = (name_); \
rf.signature = ruby_seed_signature(arena, (ret_)); \
cbm_registry_add_func(reg, rf); \
} while (0)
void cbm_ruby_stdlib_register(CBMTypeRegistry *reg, CBMArena *arena) {
+27
View File
@@ -1498,6 +1498,32 @@ TEST(rubylsp_const_path_is_linear) {
PASS();
}
/* Review ask (c): the stdlib seed must survive allocation failure. A
* zero-initialised arena has no blocks and no grow size, so every
* allocation from it returns NULL. The seed must not write through a NULL
* return-type vector, and must not register an entry whose formatted name
* came back NULL (a NULL QN would reach strcmp in finalize and in every later
* lookup). */
TEST(rubylsp_stdlib_seed_survives_alloc_failure) {
CBMArena reg_arena;
cbm_arena_init(&reg_arena);
CBMTypeRegistry reg;
cbm_registry_init(&reg, &reg_arena);
CBMArena dead;
memset(&dead, 0, sizeof(dead));
cbm_ruby_stdlib_register(&reg, &dead);
ASSERT(reg.type_count > 0);
ASSERT(reg.func_count > 0); /* Kernel built-ins carry literal names */
for (int i = 0; i < reg.func_count; i++) {
ASSERT(reg.funcs[i].qualified_name != NULL);
ASSERT(reg.funcs[i].short_name != NULL);
}
cbm_registry_finalize(&reg);
ASSERT(cbm_registry_lookup_func(&reg, "puts") != NULL);
cbm_arena_destroy(&reg_arena);
PASS();
}
/* ── suite ──────────────────────────────────────────────────────── */
void suite_ruby_lsp(void) {
@@ -1536,4 +1562,5 @@ void suite_ruby_lsp(void) {
RUN_TEST(rubylsp_self_rebinding_blocks_no_edge);
RUN_TEST(rubylsp_mixin_walk_visits_each_module_once);
RUN_TEST(rubylsp_const_path_is_linear);
RUN_TEST(rubylsp_stdlib_seed_survives_alloc_failure);
}