Home - Waterfall Grid T-Grid Console Builders Recent Builds Buildslaves Changesources - JSON API - About

Console View


Categories: connectors experimental galera main
Legend:   Passed Failed Warnings Failed Again Running Exception Offline No data

connectors experimental galera main
forkfun
Merge branch '10.11' into '11.4'
KhaledR57
MDEV-37859 mysqltest hex() fails on string arguments

func_hex() passed every argument through convert_base_helper(), which
parses the argument as a base 10 number. A string argument therefore
died with "invalid number 'abc' for base 10" instead of being converted.

HEX() is the only one of the base conversion functions that accepts a
string. In the server BIN() and OCT() are built as Item_func_conv() with
fixed bases and are numeric only, while HEX() has a dedicated Item with
a separate string path, because HEX() is the counterpart of UNHEX() and
has to serialise bytes.

Dispatch on the argument type: numeric arguments keep the existing
CONV(N, 10, 16) behaviour, string arguments are converted byte by byte
with String::set_hex(), which is the same call the server uses in
Item_func_hex::val_str_ascii_from_val_str().
drrtuy
fix: fix stack size warning.
bsrikanth-mariadb
MDEV-40383:innodb_gis.point_basic fails on replay

There are 2 problems: -
1. The REPLACE statement that is recorded doesn't store the
  value of geometry type field correctly.
2. The table definition that got recorded has fields with non-null constraint,
  and no default value is specified.
  Also, the "REPLACE INTO" statement that gets stored in the context,
  doesn't have any value specified for these non-null fields.

Solution is to: -
1. When using REPLACE INTO statement, store all the non-numeric values in HEX,
  whenever conversion from field's charset to output's charset is lossy.
2. Instead of storing only the column values that were projected in the
  query, store all the non-virtual column values into the recorded
  REPLACE INTO statement.

Implementation details: -
1. Introduce a new method is_target_cs_superset() in filesort.cc, to check
  if the output charset to which field's data is being written to, a superset
  of it. If so, non-numeric values being witten using REPLACE INTO
  statement are stored in string representation, else they are converted to
  HEX.
2. From join_read_const(), and join_read_system() methods in sql_select.cc,
  re-read the const row for all the non-virtual fields in the table.
  After the row is re-read and recorded, restore the table->read_set,
  table->status, and the const row, to the value that was before.
forkfun
Merge branch '10.11' into '11.4'
Daniel Black
MDEV-35738 mariadb build -fsanitize=pointer-compare

invalid pointer pairs are when the length/memory of one string are
intermixed with another.

For comp_err, the end null pointer was compared to soffset within
my_strtoll10. As we didn't need the end position a NULL arg option
was compatble.

For uca-dump, Address Sanitizer raises invalid pointer pair because
argv options (opt) don't have an obvious correlation of having a start
at opt_X.length, even though the lstrncmp makes this true.

The DBUG_ASSERTS of strmov (added MDEV-11752) where incompatible with
pointer-compare. Replaced strmov with static inline version in
m_string.h using memmove that allows overlaps, and being inline allows
the uneeded parts of the implementation to be optimized away.
bsrikanth-mariadb
MDEV-40383:innodb_gis.point_basic fails on replay

There are 2 problems: -
1. The REPLACE statement that is recorded doesn't store the
  value of geometry type field correctly.
2. The table definition that got recorded has fields with non-null constraint,
  and no default value is specified.
  Also, the "REPLACE INTO" statement that gets stored in the context,
  doesn't have any value specified for these non-null fields.

Solution is to: -
1. When using REPLACE INTO statement, store all the non-numeric values in HEX,
  whenever conversion from field's charset to output's charset is lossy.
2. Instead of storing only the column values that were projected in the
  query, store all the non-virtual column values into the recorded
  REPLACE INTO statement.

Implementation details: -
1. Introduce a new method is_target_cs_superset() in filesort.cc, to check
  if the output charset to which field's data is being written to, a superset
  of it. If so, non-numeric values being witten using REPLACE INTO
  statement are stored in string representation, else they are converted to
  HEX.
2. From join_read_const(), and join_read_system() methods in sql_select.cc,
  re-read the const row for all the non-virtual fields in the table.
  After the row is re-read and recorded, restore the table->read_set,
  table->status, and the const row, to the value that was before.
Daniel Black
MDEV-40488 disable connect.odbc_sqlite3 test on ASAN

Leak appears in libltdl which is opened by the unixodbc
driver. There doesn't appear to any mishandling at the
ODBC level of the connect storage engine.
forkfun
Merge branch '10.11' into '11.4'
Sergei Golubchik
MDEV-36147 MariaDB cannot open page-compressed InnoDB tables at startup if innodb_compression_algorithm other than zlib is specified

When innodb_compression_algorithm is set to a non-zlib algorithm (e.g.
lz4) and the provider plugin is loaded from mysql.plugin rather than
command line, InnoDB failed to start because it checked for the provider
at plugin initialization time, before plugin_load() reads mysql.plugin.

Fix:
* InnoDB returns HA_ERR_RETRY_INIT when the compression provider is
  missing.
* In sql_plugin.cc, the retry loop is changed to not reap until
  mysql.plugin has been loaded

Assisted-By: Claude:claude-4.6-sonnet
drrtuy
fix: extra try-catch during plugin init.
Sergei Golubchik
MDEV-37840 Server crashes when executing FLUSH PRIVILEGES after starting with skip-grant-tables and creating mysql.host table

hash_filo's mutex was lazily initialized in clear(), but grant_reload()
locks acl_cache->lock without first calling clear(). With
--skip-grant-tables, acl_cache is created but clear() is never called
(acl_reload() skips it on error), leaving the mutex uninitialized.

Fix: initialize the mutex eagerly in the hash_filo constructor and
remove the now-redundant init flag.

Assisted-By: Claude:claude-4.6-sonnet
Alexey Yurchenko
MDEV-40179 Found N prepared transactions after mariabackup SST

With log_bin=ON a transaction is committed via two-phase commit (the
binary log is the second participant), so it passes through the InnoDB
XA-prepare state. While a donor is held in BLOCK_COMMIT for a mariabackup
backup, its parallel appliers (wsrep_slave_threads > 1) leave one or more
such writesets prepared-but-not-yet-committed, and the snapshot captures
them. On a freshly SST'd joiner nothing resolves these prepared
transactions: binlog crash recovery does not run (the joiner has no in-use
binlog to recover from), and the wsrep continuity-based commit is inactive
because wsrep_emulate_bin_log is FALSE when log_bin is ON. The leftover
prepared transactions then abort startup with "Found <N> prepared
transactions!". Note this does not depend on the prepared set being
non-contiguous - even a contiguous run aborts, because nothing commits
or rolls it back.

Rollback these transactions in xarecover_handlerton(). If rollback fails
flag error to cause unireg_abort().
Andrei Elkin
MDEV-35691: Invalid access, use-after-free, on rli->description_event_for_exec

This is a review commit that simplifies a proposed patch
https://github.com/MariaDB/server/commit/a8f2693a4711f8d0bb7298fb33a9b4c766f3bd56#diff-0a7bd720f86eea398c217107e96dd389398fa31ecc0dcb80586a0895541b9ea4

It leverages an existing synchronization between the driver and worker
threads in rpl_parallel::do_event at handling FD with `create > 0`
for thread-safely optimizing the proposed patch's
master bug bitmap updating.

Further goes more extended description and prove of correctness of such
approach.

The arrangement of `Relay_log_info::master_bug_bitmask` as cached
result of processing of the master server's version in FD event is the
right direction (and very productive idea).
First note the claim A:

  *the master version of all replicated events in between of two
  successive FD events having `created > 0` is the same and is
  specified by the first one.*

According to the patch such `created>0` FD (call it the initial one)
will trigger updating the bitmask. Actually 'cos of the claim A all
other possibly following FD with `created == 0` may leave the bitmap
intact. And while the bitmap is set just once any replicated event bug
vulnerability is stably described by it, even though
`rli->relay_log.description_event_for_exec` keeps changing by
non-initial FD:s as now the master version is stashed outside of FD.

Next observation is that
[updating](https://github.com/MariaDB/server/blob/knielsen_fixes_10.6/sql/log_event_server.cc#L2480) of the bitmap by the *initial* FD
```
  if (!ret)
  {
    /* Save the information describing this binlog */
    copy_crypto_data(rli->relay_log.description_event_for_exec);
    delete rli->relay_log.description_event_for_exec;
    rli->relay_log.description_event_for_exec= this;
+ if (created && server_id != (uint32) global_system_variables.server_id )  // the review proposal
      rli->calc_master_bug_bitmask(this); // if (created) ...
  }
```

is thread safe thanks to [the base's synchronization](https://github.com/MariaDB/server/blob/knielsen_fixes_10.6/sql/rpl_parallel.cc#L3031)
```
  if (unlikely(typ == FORMAT_DESCRIPTION_EVENT))
  {
    Format_description_log_event *fdev=
      static_cast<Format_description_log_event *>(ev);
    if (fdev->created)
    {
      /*
        This format description event marks a new binlog after a master server
        restart. We are going to close all temporary tables to clean up any
        possible left-overs after a prior master crash.

        Thus we need to wait for all prior events to execute to completion,
        in case they need access to any of the temporary tables.

        We also need to notify the worker thread running the prior incomplete
        event group (if any), as such event group signifies an incompletely
        written group cut short by a master crash, and must be rolled back.
      */
      if (current->queue_master_restart(serial_rgi, fdev) ||
          wait_for_workers_idle(rli->sql_driver_thd))
```
Note the initial slave's FD is excluded from being the bitmap updater.
bsrikanth-mariadb
MDEV-40383:innodb_gis.point_basic fails on replay

There are 2 problems: -
1. The REPLACE statement that is recorded doesn't store the
  value of geometry type field correctly.
2. The table definition that got recorded has fields with non-null constraint,
  and no default value is specified.
  Also, the "REPLACE INTO" statement that gets stored in the context,
  doesn't have any value specified for these non-null fields.

Solution is to: -
1. Store all the non-numeric values in HEX, whenever conversion from
  field's charset to output's charset is lossy, while using REPLACE INTO
  statement.
2. Instead of storing only the column values that were projected in the
  query, store all the column values into the recorded REPLACE INTO
  statement.

Implementation details: -
1. Modify the format_and_store_row() method in filesort.cc, to accept an
  argument that tells whether to store virtual columns. By default, its
  value is set to TRUE. However, it can be set to FALSE, when recording a
  row from base tables, and mysql.[table|column|index]_stats tables
  using REPLACE INTO statement.
2. From join_read_const(), and join_read_system() methods in sql_select.cc,
  re-read the const row for all the fields in the table. After the row
  is re-read and recorded, restore the table->read_set, table->status,
  and the const row, to the value that was before.
Georg Richter
CONC-842: Fix heap buffer over-read in mthd_my_read_rows

A security vulnerability was identified in mthd_my_read_rows() where incoming
field length prefixes ('len') were validated only against the destination buffer
boundary ('end_to'), but not against the remaining source network packet buffer
('end_cp').

Because 'end_to' includes additional slack allocation bytes intended for field
NUL-terminators, a crafted packet with an inflated field length could bypass
destination checks, causing memcpy() to read out-of-bounds memory past 'end_cp'.

Fix this by introducing two validation steps:
1. Pre-check (cp >= end_cp): If the network buffer is exhausted before reading
  a field length byte (e.g. 8-field packet with fields = 9 due to extended
  type info), safely set remaining field pointers to NULL without reading
  out-of-bounds or throwing CR_MALFORMED_PACKET.
2. Post-check (len > end_cp - cp): Verify 'len' does not exceed remaining
  source packet bytes before executing memcpy(). Abort with
  CR_MALFORMED_PACKET if exceeded.
Daniel Black
MDEV-40414 Server crash in Charset::charset upon JSON operations

JSON_EQUALS didn't check that ::val_str() of its arguments
where not-null before attempting to test their equality.

JSON_OVERLAPS also ensure that ::val_json() isn't null.
KhaledR57
MDEV-37865: IF() function is returning incorrect error

get_expr_function_type() compared a parsed function name against
function_table[] using strncasecmp() bounded by the length of the parsed
name. The comparison stopped at that length and never checked that the
table entry ended there, so any name that is a prefix of a known
function resolved to that function.

"if" matched "ifnull" and produced a misleading arity error. Names that
prefixed a function with a compatible signature were worse and
dispatched silently: l("AB") ran lcase(),t("  x  ") ran trim().
A typo in a test script ran a different function and the test still passed.

Store each entry's length in function_table[] with STRING_WITH_LEN() and
require it to match the parsed length. Report the unmatched name in the
"Unknown function" error.
Sergei Golubchik
MDEV-40337 store user vars in a dedicated memroot

user_var_entry objects and their names have a connection lifetime,
they exist until the connection ends (or is reset), and then they're
all deleted at once. This is exactly the use case for MEM_ROOT,
let's store them there.

Additionally, let's set MY_ROOT_USE_VMEM flag to keep this memroot
off the general heap where user_var_entry values are stored
and where heap buffer overflows can happen.

The latter makes memory allocations for the MEM_ROOT about 10x more
expensive, so let's always start with an empty memroot (= zero overhead
if no user variables are used) and on THD cleanup let's retain one
memroot block (= zero overhead if the next connection takes THD from the
cache and uses user variables up to one block size).

Assisted-By: Claude:claude-4.8-opus
Georg Richter
Add support for MYSQL_TYPE_VECTOR in ps protocol.

Added support for MYSQL_TYPE_VECTOR. Since MariaDB server doesn't
know MYSQL_TYPE_VECTOR we convert parameter types to MYSQL_TYPE_BLOB.

This commit also fixes a crash if an unknown field type was sent
from server.
  • win-connector_c_3: 'dojob cd win32 && del CMakeCache.txt && cmake --version && cmake ..\src -G "Visual Studio 17 2022" -A "Win32" -DCMAKE_BUILD_TYPE=RelWithDebInfo -DWITH_SSL=SCHANNEL -DWITH_MSI=ON -DCURL_INCLUDE_DIR=c:\tools\curl\include && cmake --build . --clean-first --config RelWithDebInfo --target package' failed -  stdio
Sergei Golubchik
MDEV-26910 mysqld_multi starts same instance multiple times with the risk to crash database

a group name may be present in a file more than once.
use hash to deduplicate.
Georg Richter
Fix ps.c test:

Declare constant numbers as float to avoud float to double
conversion warning/errors
  • cc-x-codbc-windows: 'dojob pwd if '3.4' == '3.4' ls win32/test SET TEST_DSN=master SET TEST_DRIVER=master SET TEST_PORT=3306 SET TEST_SCHEMA=odbcmaster if '3.4' == '3.4' cd win32/test if '3.4' == '3.4' ctest --output-on-failure' failed -  stdio
Alexey Yurchenko
MDEV-38147 error 1950 after mariabackup SST with gtid_strict_mode=ON

After a mariabackup SST the joiner could fail with

  ER_GTID_STRICT_OUT_OF_ORDER (error 1950)

while re-binlogging transactions received over IST.

The cause is that the binary log copied from the donor carries a
Gtid_list whose position can be ahead of the storage-engine snapshot
because getting snapshot and binlog are no mutually atomic, so transactions
can be present in the copied binlog that are not committed in the copied
engine snapshot.
After the SST the joiner reports the (committed) engine position to the
cluster, IST resends those transactions, and re-binlogging them under
gtid_strict_mode=ON collides with the ahead Gtid_list -> error 1950.
(MDEV-34483 made the engine snapshot stop short of the binlog, which is
what exposed this.)

The copied binary log carries no transactions the joiner needs - only a
Gtid_list - so instead of shipping and then having to truncate/reconcile
it, the joiner now starts a fresh binary log and seeds its GTID position
from the storage-engine checkpoint during recovery. That checkpoint is
the committed cluster position, i.e. exactly where IST resumes, so the
joiner's binary log stays in lockstep with the rest of the cluster and
no out-of-order GTID can occur.

This works for both wsrep_gtid_mode settings; only the binlog domain of
the cluster stream differs:

  - wsrep_gtid_mode=ON : wsrep_gtid_domain_id (cluster writes are
    re-tagged to it), which is the domain stored in the checkpoint;
  - wsrep_gtid_mode=OFF: gtid_domain_id (cluster writes keep the node's
    configured domain).

Async-replica positions (mysql.gtid_slave_pos) are part of the engine
snapshot and survive the SST unchanged, so a Galera node can still serve
as an async master or replica across the SST.

This commit:
- sql/log.cc: adds wsrep_seed_binlog_gtid_state(), called from
  do_binlog_recovery() when the joiner has no binary log, seeding the
  binlog GTID state for the cluster domain to the SE checkpoint position.
- scripts/wsrep_sst_mariabackup.sh: no longer moves the donor's binary
  log into place on the joiner.
- extra/mariabackup: backward compatibility: keep shipping binlog file
  in SST but
  - on donor fix the race between rotation and shipping so that the file
    shipped is the one that had been rotated;
  - on joiner discard shipped binlog in favour of one generated by
    wsrep_seed_binlog_gtid_state().
- sql/wsrep_sst.cc: logs the position actually adopted from storage
  (the authoritative post-SST position) rather than the script-reported
  one.
- sql/handler.cc: downgrades the "Discovered discontinuity in recovered
  wsrep transaction XIDs" message in wsrep_order_and_check_continuity()
  from warning to debug level. With parallel appliers a snapshot
  routinely captures prepared XIDs that are not contiguous with the
  engine checkpoint, so this is normal during SST recovery and of no
  value in regular operation; the transactions past the checkpoint are
  re-delivered by the cluster (IST/SST) regardless.
- Adds an MDEV-38147 MTR test reproducing the issue.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
Georg Richter
CONC-843: Backport fix for crash from 3.4 branch

If an unknown type was returned from server (e.g. MYSQL_TYPE_VECTOR)
client has to throw an error instead of crashing
drrtuy
fix: MDEV-40452 ASAN builds complain about enourmous stack consumption b/c of the reference pointer used for stack usage calculations.
forkfun
Merge branch '10.11' into '11.4'
KhaledR57
MDEV-37865: IF() function is returning incorrect error

get_expr_function_type() compared a parsed function name against
function_table[] using strncasecmp() bounded by the length of the parsed
name. The comparison stopped at that length and never checked that the
table entry ended there, so any name that is a prefix of a known
function resolved to that function.

"if" matched "ifnull" and produced a misleading arity error. Names that
prefixed a function with a compatible signature were worse and
dispatched silently: l("AB") ran lcase(),t("  x  ") ran trim().
A typo in a test script ran a different function and the test still passed.

Require the table entry to terminate at the parsed length as well, so an
unmatched name reaches the existing "Unknown function" error.
drrtuy
fix: clean DeltaAppender after at rollback or disconnect.
Mohammad Tafzeel Shams
MDEV-39800: Assertion `!(mode & 2048U) || (mode & 512U) || is_supremum' failed

ISSUE:

Lock bypassing optimization allows an X-lock request to skip
waiting locks when the requesting transaction already holds an
S-lock on the same record. This optimization is designed for
regular B-tree record locks that use heap-number-based conflict
detection.

Spatial index predicate locks use different semantics. They
perform MBR (Minimum Bounding Rectangle) overlap checks for
conflict detection and must not participate in bypass
optimization.

The assertion failure occurred because predicate insert
intention locks is not considered inside lock bypass code,
which assumed all insert intention locks must be either gap
locks or on the supremum record (MDEV-34877).

ut_ad(!(insert_intention) || (gap) || is_supremum)

Additionally, predicate locks could incorrectly enable
bypass_mode because the existing checks did not explicitly
exclude LOCK_PREDICATE locks.

FIX:

- lock_t::is_predicate(): Add a helper to identify spatial
  index locks.

- lock_t::can_be_bypassed(): Return false for predicate
  locks.

- lock_rec_has_to_wait_in_queue(): Update the assertion to
  allow predicate insert intention locks. Also add a
  !is_predicate() check to bypass_mode calculation.

- lock_rec_queue_validate_bypass(): Add an early return to
  skip bypass validation for predicate locks.
drrtuy
chore: renaming, extra docs and bump DuckDB to gamma.
Sergei Golubchik
MDEV-31024 Server crash / ASAN use-after-poison in Binary_string::free_buffer / Item_func_sformat::~Item_func_sformat

re-allocate Item_func_sformat::val_arg in shallow_copy()
to keep it in the same memroot as the item.
forkfun
MDEV-40303 SIGSEGV in Item_field::type_handler() on PS re-execution

Re-executing a prepared statement that reads a derived-table column
holding a scalar UNION subquery crashed the server: between executions
st_select_lex_unit::cleanup() frees union_result and sets 'cleaned' but
leaves 'prepared' set, so on re-fix set_row() walks a stale item_list
whose Item_field::field are NULL.

Fix: prepare() re-prepares a cleaned unit instead of treating it as
still prepared.
KhaledR57
MDEV-37865: IF() function is returning incorrect error

get_expr_function_type() compared a parsed function name against
function_table[] using strncasecmp() bounded by the length of the parsed
name. The comparison stopped at that length and never checked that the
table entry ended there, so any name that is a prefix of a known
function resolved to that function.

"if" matched "ifnull" and produced a misleading arity error. Names that
prefixed a function with a compatible signature were worse and
dispatched silently: l("AB") ran lcase(),t("  x  ") ran trim().
A typo in a test script ran a different function and the test still passed.

Require the table entry to terminate at the parsed length as well, so an
unmatched name reaches the existing "Unknown function" error.
drrtuy
chore: DuckDB build.sh exposes build with ASAN flag.
KhaledR57
MDEV-37859 mysqltest hex() fails on string arguments

func_hex() passed every argument through convert_base_helper(), which
parses the argument as a base 10 number. A string argument therefore
died with "invalid number 'abc' for base 10" instead of being converted.

HEX() is the only one of the base conversion functions that accepts a
string. In the server BIN() and OCT() are built as Item_func_conv() with
fixed bases and are numeric only, while HEX() has a dedicated Item with
a separate string path, because HEX() is the counterpart of UNHEX() and
has to serialise bytes.

Dispatch on the argument type: numeric arguments keep the existing
CONV(N, 10, 16) behaviour, string arguments are converted byte by byte
with String::set_hex(), which is the same call the server uses in
Item_func_hex::val_str_ascii_from_val_str().
Sergei Golubchik
MDEV-40341 store read-only sysvars in a read-only root

Store all READ_ONLY sysvar values in the read_only_root

Even though READ_ONLY sysvars are protected, for string variables
it usually means that the pointer cannot be changed. The value it
points to - the string itself - still can be. Let's store all
values of string READ_ONLY sysvars in the read_only_root.

sysvars that point directly into argv are copied to read_only_root.
sysvars that have their values calculated and allocated now
must be explicitly marked with PREALLOCATED to let it know they
have to be free()-d.

Assisted-By: Claude:claude-4.8-opus
KhaledR57
MDEV-37865: IF() function is returning incorrect error

get_expr_function_type() compared a parsed function name against
function_table[] using strncasecmp() bounded by the length of the parsed
name. The comparison stopped at that length and never checked that the
table entry ended there, so any name that is a prefix of a known
function resolved to that function.

"if" matched "ifnull" and produced a misleading arity error. Names that
prefixed a function with a compatible signature were worse and
dispatched silently: l("AB") ran lcase(),t("  x  ") ran trim().
A typo in a test script ran a different function and the test still passed.

Store each entry's length in function_table[] with STRING_WITH_LEN() and
require it to match the parsed length. Report the unmatched name in the
"Unknown function" error.
Sergei Golubchik
MDEV-40341 store read-only sysvars in a read-only segment

Protect all READ_ONLY sysvars from run-time changes.

Put them into a separate section and use my_virtual_mem_protect()
to make this section read-only before the server starts accepting
connections.

Verify that they're all protected in the sys_var constructor.

One exception: opt_noacl (--skip-grant-tables) can be changed
from 1 to 0 on FLUSH PRIVILEGES. Let's briefly drop the
protection for this 1->0 change. It can be needed only once
in a server lifetime and only if it was started with --skip-grant-tables

On shutdown the protection is removed, making variables writable
again because shutdown resets some of them during the cleanup

Assisted-By: Claude:claude-4.8-opus
Mohammad Tafzeel Shams
MDEV-40504: Fix memory leak in mariabackup incremental copy

ibx_copy_incremental_over_full() : Replace die() with
proper error handling to avoid memory leaks when RocksDB
backup directory operations fail.
Sergei Golubchik
generalize ROOT_FLAG_MPROTECT to work on Windows, rename accordingly

* it's called ROOT_FLAG_VMEM
* it does not imply any protection, ROOT_FLAG_MPROTECT was a misnomer
* it means that the memory is allocated using my_virtual_mem_commit,
  not my_malloc, so not on heap
* memory allocated this way can be protected with my_virtual_mem_protect()
* thus root using ROOT_FLAG_VMEM can be protected with protect_root()

Assisted-By: Claude:claude-5-sonnet