Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions include/rayforce.h
Original file line number Diff line number Diff line change
Expand Up @@ -644,6 +644,7 @@ ray_err_t ray_env_set(int64_t sym_id, ray_t* val);

ray_t* ray_table_new(int64_t ncols);
ray_t* ray_table_add_col(ray_t* tbl, int64_t name_id, ray_t* col_vec);
ray_t* ray_table_validate_rectangular(ray_t* tbl, const char* context);
ray_t* ray_table_get_col(ray_t* tbl, int64_t name_id);
ray_t* ray_table_get_col_idx(ray_t* tbl, int64_t idx);
int64_t ray_table_col_name(ray_t* tbl, int64_t idx);
Expand Down
5 changes: 5 additions & 0 deletions src/store/col.c
Original file line number Diff line number Diff line change
Expand Up @@ -590,6 +590,11 @@ static ray_t* col_read_recursive(const uint8_t** pp, size_t* remaining) {
ray_release(col); /* table_add_col retains */
if (!tbl || RAY_IS_ERR(tbl)) return tbl;
}
ray_t* shape_err = ray_table_validate_rectangular(tbl, "col load table");
if (shape_err) {
ray_release(tbl);
return shape_err;
}
return tbl;
}

Expand Down
23 changes: 21 additions & 2 deletions src/store/serde.c
Original file line number Diff line number Diff line change
Expand Up @@ -822,20 +822,27 @@ static ray_t* de_raw_inner(uint8_t* buf, int64_t* len) {

void* name_data = ray_data(schema);
ray_t** col_ptrs = (ray_t**)ray_data(cols);
for (int64_t i = 0; i < ncols && i < schema->len; i++) {
for (int64_t i = 0; i < ncols; i++) { /* ncols == schema->len (guarded above) */
int64_t name_id = (schema->type == RAY_I64)
? ((int64_t*)name_data)[i]
: ray_read_sym(name_data, i, RAY_SYM, schema->attrs);
ray_t* new_tbl = ray_table_add_col(tbl, name_id, col_ptrs[i]);
if (!new_tbl || RAY_IS_ERR(new_tbl)) {
ray_release(tbl);
ray_release(schema);
ray_release(cols);
return new_tbl;
}
tbl = new_tbl;
}

ray_t* shape_err = ray_table_validate_rectangular(tbl, "deserialize table");
if (shape_err) {
ray_release(tbl);
ray_release(schema);
ray_release(cols);
return shape_err;
}

ray_release(schema);
ray_release(cols);
return tbl;
Expand All @@ -857,6 +864,18 @@ static ray_t* de_raw_inner(uint8_t* buf, int64_t* len) {
return vals;
}

/* One value per key: a dict probe finds a key index in [0, keys->len)
* and reads the value at that index, so a crafted dict with more keys
* than values would index the value block out of bounds (OOB
* read/release). vals may be a LIST of column vectors or a flat value
* vector — either way its length must equal keys->len. */
if (keys->len != vals->len) {
ray_t* e = ray_error("domain", "deserialize dict: key/value count mismatch (%lld keys, %lld values)", (long long)keys->len, (long long)vals->len);
ray_release(keys);
ray_release(vals);
return e;
}

/* Build dict: alloc with 2 slots */
ray_t* dict = ray_alloc(2 * sizeof(ray_t*));
if (!dict || RAY_IS_ERR(dict)) {
Expand Down
24 changes: 24 additions & 0 deletions src/table/dict.c
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,10 @@

#define DICT_DATA_SIZE (2 * sizeof(ray_t*))

static bool dict_side_is_list_or_vec(ray_t* side) {
return side && !RAY_IS_ERR(side) && (side->type == RAY_LIST || ray_is_vec(side));
}

static ray_t* dict_alloc_block(ray_t* keys, ray_t* vals) {
ray_t* d = ray_alloc(DICT_DATA_SIZE);
if (!d || RAY_IS_ERR(d)) return d;
Expand Down Expand Up @@ -69,6 +73,26 @@ ray_t* ray_dict_new(ray_t* keys, ray_t* vals) {
ray_release(keys);
return vals ? vals : ray_error("type", "dict: vals vector is null");
}
if (!dict_side_is_list_or_vec(keys)) {
ray_release(keys);
ray_release(vals);
return ray_error("domain", "dict: keys must be list/vector-like, got %s",
ray_type_name(keys->type));
}
if (!dict_side_is_list_or_vec(vals)) {
ray_release(keys);
ray_release(vals);
return ray_error("domain", "dict: vals must be list/vector-like, got %s",
ray_type_name(vals->type));
}
if (keys->len != vals->len) {
int64_t klen = keys->len;
int64_t vlen = vals->len;
ray_release(keys);
ray_release(vals);
return ray_error("domain", "dict: key/value count mismatch (%lld keys, %lld values)",
(long long)klen, (long long)vlen);
}
ray_t* d = dict_alloc_block(keys, vals);
if (!d || RAY_IS_ERR(d)) {
ray_release(keys);
Expand Down
59 changes: 58 additions & 1 deletion src/table/table.c
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,21 @@ static inline ray_t* tbl_cols(ray_t* tbl) {
return tbl_slots(tbl)[1];
}

static bool table_col_is_valid(ray_t* col) {
if (!col || RAY_IS_ERR(col)) return false;
return col->type == RAY_LIST ||
ray_is_vec(col) ||
RAY_IS_PARTED(col->type) ||
col->type == RAY_MAPCOMMON;
}

static int64_t table_col_nrows(ray_t* col) {
if (!col) return 0;
if (RAY_IS_PARTED(col->type) || col->type == RAY_MAPCOMMON)
return ray_parted_nrows(col);
return col->len;
}

/* --------------------------------------------------------------------------
* ray_table_new — allocates an empty table with capacity for `ncols`.
*
Expand Down Expand Up @@ -103,7 +118,11 @@ ray_t* ray_table_new(int64_t ncols) {

ray_t* ray_table_add_col(ray_t* tbl, int64_t name_id, ray_t* col_vec) {
if (!tbl || RAY_IS_ERR(tbl)) return tbl;
if (!col_vec || RAY_IS_ERR(col_vec)) return ray_error("type", "table add_col: column must be a vector, got %s", col_vec ? ray_type_name(col_vec->type) : "null");
if (!table_col_is_valid(col_vec)) {
ray_release(tbl);
return ray_error("domain", "table add_col: column must be list/vector-like, got %s",
col_vec ? ray_type_name(col_vec->type) : "null");
}

tbl = ray_cow(tbl);
if (!tbl || RAY_IS_ERR(tbl)) return tbl;
Expand All @@ -127,6 +146,44 @@ ray_t* ray_table_add_col(ray_t* tbl, int64_t name_id, ray_t* col_vec) {
return tbl;
}

ray_t* ray_table_validate_rectangular(ray_t* tbl, const char* context) {
const char* where = context ? context : "table";
if (!tbl) return ray_error("type", "%s: expected table, got null", where);
if (RAY_IS_ERR(tbl))
return ray_error(ray_err_code(tbl) ? ray_err_code(tbl) : "error",
"%s: expected table, got error", where);
if (tbl->type != RAY_TABLE)
return ray_error("type", "%s: expected table, got %s", where, ray_type_name(tbl->type));

ray_t* schema = tbl_schema(tbl);
ray_t* cols = tbl_cols(tbl);
if (!schema || !cols || RAY_IS_ERR(schema) || RAY_IS_ERR(cols))
return ray_error("domain", "%s: malformed table storage", where);
if (schema->len != cols->len)
return ray_error("domain",
"%s: schema/column count mismatch (%lld names, %lld columns)",
where, (long long)schema->len, (long long)cols->len);

ray_t** col_ptrs = (ray_t**)ray_data(cols);
int64_t expected = 0;
for (int64_t i = 0; i < cols->len; i++) {
ray_t* col = col_ptrs[i];
if (!table_col_is_valid(col))
return ray_error("domain", "%s: column must be list/vector-like, got %s",
where, col ? ray_type_name(col->type) : "null");
int64_t got = table_col_nrows(col);
if (i == 0) {
expected = got;
} else if (got != expected) {
return ray_error("domain",
"%s: ragged columns (column %lld has %lld rows, expected %lld)",
where, (long long)i, (long long)got, (long long)expected);
}
}

return NULL;
}

/* --------------------------------------------------------------------------
* ray_table_get_col — lookup column by sym id; borrowed pointer or NULL.
* -------------------------------------------------------------------------- */
Expand Down
14 changes: 4 additions & 10 deletions test/rfl/system/serde.rfl
Original file line number Diff line number Diff line change
Expand Up @@ -29,13 +29,7 @@
(count (de (ser [1 0N 3]))) -- 3
(sum (de (ser [1 0N 3]))) -- 4

;; ────────────── table frame: schema/column count must match ──────────────
;; The table decoder loops over min(cols->len, schema->len), so a crafted frame
;; whose schema names and columns disagree would be silently accepted as a
;; truncated table. Splice a 2-name i64 schema with a 1-column list into one
;; table payload ([0x62 attrs] + ser(schema) + ser(cols), each self-consistent)
;; and confirm it is rejected rather than decoded as a 1-column table.
(set _s74p (concat (concat (as 'U8 [0x62 0x00]) (drop (ser (as 'I64 [7 7])) 16)) (drop (ser (list (as 'I64 [1]))) 16)))
(set _s74h (take (ser 0) 16))
(set _s74f (concat (concat (concat (take _s74h 8) (as 'U8 (enlist (count _s74p)))) (drop _s74h 9)) _s74p))
(de _s74f) !- domain
;; Container-frame count/length validation (table schema-vs-columns, ragged
;; columns, dict keys-vs-values) is covered in test/test_store.c
;; (store/serde_container_count_mismatch), which builds frames via
;; sizeof(ray_ipc_header_t) rather than hard-coding the wire layout.
28 changes: 6 additions & 22 deletions test/test_journal.c
Original file line number Diff line number Diff line change
Expand Up @@ -1702,14 +1702,12 @@ static test_result_t test_journal_snapshot_rename_fails(void) {
}

/* ═══════════════════════════════════════════════════════════════════════
* 20. Snapshot: .qdb dict with more keys than values (missing-val path).
* 20. Snapshot: .qdb dict cannot be built with more keys than values.
* ═══════════════════════════════════════════════════════════════════════ */

static test_result_t test_journal_open_qdb_missing_val(void) {
char base[256]; make_base(base, sizeof(base), "oc_qdbmissing");
char qpath[270]; qdb_path(qpath, sizeof(qpath), base);

/* Build a dict: 2 sym keys, 1 value — second key has no corresponding val. */
/* Build a dict: 2 sym keys, 1 value. Public construction rejects this
* before it can become a partially-loaded snapshot. */
int64_t s1 = ray_sym_intern("jrn_k1", 6);
int64_t s2 = ray_sym_intern("jrn_k2", 6);
ray_t* keys = ray_sym_vec_new(RAY_SYM_W64, 2);
Expand All @@ -1724,24 +1722,10 @@ static test_result_t test_journal_open_qdb_missing_val(void) {

ray_t* d = ray_dict_new(keys, vals);
TEST_ASSERT_NOT_NULL(d);
TEST_ASSERT_FALSE(RAY_IS_ERR(d));

ray_err_t se = ray_obj_save(d, qpath);
ray_release(d);
TEST_ASSERT_EQ_I(se, RAY_OK);

/* Open: should warn about missing val for sym jrn_k2, but succeed. */
ray_err_t e = ray_journal_open(base, RAY_JOURNAL_ASYNC);
/* Partial load — bind_errs == 0 (we skipped, not failed), so OK. */
if (e == RAY_OK) {
TEST_ASSERT_TRUE(ray_journal_is_open());
TEST_ASSERT_EQ_I(ray_journal_close(), RAY_OK);
} else {
/* If open returned domain, still OK for test purposes. */
TEST_ASSERT_FALSE(ray_journal_is_open());
}
TEST_ASSERT_TRUE(RAY_IS_ERR(d));
TEST_ASSERT_STR_EQ(ray_err_code(d), "domain");
ray_error_free(d);

cleanup_base(base);
PASS();
}

Expand Down
Loading
Loading