From eb7e4500873b73a626a74a34468587378ee4caa6 Mon Sep 17 00:00:00 2001 From: Evgen Byelozorov Date: Fri, 31 Jul 2026 14:59:16 +0200 Subject: [PATCH] fix(store): reject duplicate splayed column names --- src/store/splay.c | 39 +++++++++++++++++++++++++++++++------ test/rfl/system/splayed.rfl | 10 ++++++++++ 2 files changed, 43 insertions(+), 6 deletions(-) diff --git a/src/store/splay.c b/src/store/splay.c index 79d19c3ea..b50cacd83 100644 --- a/src/store/splay.c +++ b/src/store/splay.c @@ -82,6 +82,34 @@ static bool table_has_col_named(ray_t* tbl, const char* name, size_t len) { return false; } +static bool splay_col_name_safe(const char* name, size_t name_len) { + return name_len > 0 && name[0] != '.' && + !memchr(name, '/', name_len) && + !memchr(name, '\\', name_len) && + !memchr(name, '\0', name_len); +} + +static ray_err_t splay_validate_persisted_names(ray_t* tbl) { + int64_t nc = ray_table_ncols(tbl); + for (int64_t c = 0; c < nc; c++) { + ray_t* a = ray_sym_str(ray_table_col_name(tbl, c)); + if (!a || RAY_IS_ERR(a)) continue; + const char* an = ray_str_ptr(a); + size_t alen = ray_str_len(a); + if (!splay_col_name_safe(an, alen)) continue; + for (int64_t j = c + 1; j < nc; j++) { + ray_t* b = ray_sym_str(ray_table_col_name(tbl, j)); + if (!b || RAY_IS_ERR(b)) continue; + const char* bn = ray_str_ptr(b); + size_t blen = ray_str_len(b); + if (!splay_col_name_safe(bn, blen)) continue; + if (alen == blen && memcmp(an, bn, alen) == 0) + return RAY_ERR_DOMAIN; + } + } + return RAY_OK; +} + /* Remove regular files in `dir` that are not part of the just-written * table: not a dotfile (".d", ".sym", ".sym.lk"), not a current column. * Runs after the .d commit so a stale wider-schema file can never shadow @@ -119,6 +147,9 @@ static ray_err_t splay_save_impl(ray_t* tbl, const char* dir, const char* sym_pa if (!tbl || RAY_IS_ERR(tbl)) return RAY_ERR_TYPE; if (!dir) return RAY_ERR_IO; + ray_err_t name_err = splay_validate_persisted_names(tbl); + if (name_err != RAY_OK) return name_err; + /* Symfile/column collision guard. A column is written as `dir/`; * the symfile (and its `.lk` lock) is written at `sym_path`. A * column whose file lands on the symfile path — or its lock path — @@ -237,9 +268,7 @@ static ray_err_t splay_save_impl(ray_t* tbl, const char* dir, const char* sym_pa if (!name_atom) continue; const char* name = ray_str_ptr(name_atom); size_t name_len = ray_str_len(name_atom); - if (name_len == 0 || name[0] == '.' || - memchr(name, '/', name_len) || memchr(name, '\\', name_len) || - memchr(name, '\0', name_len)) + if (!splay_col_name_safe(name, name_len)) continue; /* unsafe name: no file, no .d entry */ char path[1024]; @@ -359,9 +388,7 @@ static ray_t* splay_load_dom_impl(const char* dir, ray_sym_domain_t* dom, /* Reject names with path separators, traversal, or starting with '.' * — these indicate a corrupt/hand-tampered .d. */ - if (name_len == 0 || name[0] == '.' || - memchr(name, '/', name_len) || memchr(name, '\\', name_len) || - memchr(name, '\0', name_len)) { + if (!splay_col_name_safe(name, name_len)) { ray_release(schema); ray_release(tbl); return ray_error("corrupt", diff --git a/test/rfl/system/splayed.rfl b/test/rfl/system/splayed.rfl index d9e6b9394..e7f8a7037 100644 --- a/test/rfl/system/splayed.rfl +++ b/test/rfl/system/splayed.rfl @@ -21,6 +21,16 @@ (at R-Small 'a) -- (at T-Small 'a) (at R-Small 'b) -- (at T-Small 'b) +;; ────────────── duplicate persisted names are rejected ────────────── +;; Splayed stores one file per column name. A table with duplicate column +;; names used to write both columns to the same path, so the later column +;; overwrote the earlier one and reload duplicated the later data. +(.sys.exec "rm -rf /tmp/rfl_splayed_dup") +(set J-L (table [sym qty px] (list ['A 'B] [10 20] [1.0 2.0]))) +(set J-R (table [sym qty venue] (list ['A 'C] [100 300] ['X 'Z]))) +(.db.splayed.set "/tmp/rfl_splayed_dup/join/" (full-join [sym] J-L J-R)) !- domain +(.db.splayed.set "/tmp/rfl_splayed_dup/csv/" (table [a a] (list [1 2] [10 20]))) !- domain + ;; ────────────── morsel-boundary table (1024 rows) ────────────── (set T-1024 (table [n] (list (til 1024)))) (.db.splayed.set "/tmp/rfl_splayed_1024/" T-1024)