Skip to content

Bug: COPY ... TO segfaults in Binder::bindCopyToClause with the prebuilt liblbug on Ubuntu 26.04 / GCC 15 #1381

Open

Description

@sunsided

Ladybug version

v0.21.2 (Rust crate lbug = "=0.21.2", default prebuilt liblbug-static-linux-x86_64-compat)

What operating system are you using?

Ubuntu 26.04 (GCC 15.2.0, glibc 2.43, OpenSSL 3.5.5). This is the image currently behind GitHub Actions' ubuntu-latest. Ubuntu 24.04 (GCC 13.3.0) is not affected.

What happened?

Any COPY (...) TO '...' segfaults while binding the statement when the Rust crate links the prebuilt static liblbug on Ubuntu 26.04. The same program on Ubuntu 24.04 writes the file and exits normally. Expected: the parquet file is written, as on 24.04.

Backtrace (gdb, from our application; the top frames are the same in the minimal reproduction below):

#0  std::__format::_Sink<char>::_M_write(std::basic_string_view<char>) ()
#1  std::__format::_Sink_iter<char> std::__format::__formatter_str<char>::format<...>(std::basic_string_view<char>, std::basic_format_context<...>&) ()
#2  decltype(auto) std::basic_format_arg<...>::_M_visit<std::__format::_Formatting_scanner<...>::_M_format_arg(...)>(...) ()
#3  lbug::binder::Binder::bindCopyToClause(lbug::parser::Statement const&) ()
#4  lbug::binder::Binder::bind(lbug::parser::Statement const&) ()
#5  lbug::main::ClientContext::prepareNoLock(...) ()
...
#10 lbug::main::Connection::query(std::basic_string_view<char>) ()
#11 lbug_rs::connection_query(lbug::main::Connection&, std::basic_string_view<char>) ()
#12 lbug_rs$cxxbridge1$connection_query ()
#13 <lbug::connection::Connection>::query ()

Likely cause (our analysis, not confirmed): frames #0–#2 resolve to addresses next to the locally compiled cxx bridge (lbug_rs$cxxbridge1$...), not next to the prebuilt core (lbug::binder::...). The prebuilt archive appears to be compiled with an older GCC, while the Rust crate's cxx bridge is compiled on the user's machine with GCC 15. Both instantiate libstdc++'s std::format templates as weak/COMDAT symbols, so the linker keeps one copy: the GCC 15 one from the bridge. The prebuilt's bindCopyToClause, compiled against the older <format> layout, then calls into it. std::format's ABI was not stable across these GCC versions, which would explain the crash.

Supporting evidence: building from source on the same Ubuntu 26.04 machine (LBUG_BUILD_FROM_SOURCE=1), so that everything is compiled by GCC 15, fixes it. Our full test suite, which hits this path, passes.

Possible fixes upstream, if the analysis holds: build the Linux prebuilts with the newest GCC you support, or compile them with -fvisibility=hidden / -fvisibility-inlines-hidden (or a version script) so the archive's std::format instantiations stay internal and don't merge with the consumer's.

Are there known steps to reproduce?

Cargo.toml:

[package]
name = "lbug-copy-to-repro"
version = "0.1.0"
edition = "2021"

[dependencies]
lbug = "=0.21.2"

src/main.rs:

use lbug::{Connection, Database, SystemConfig};

fn main() {
    let dir = std::env::temp_dir().join("lbug-copy-to-repro");
    let _ = std::fs::remove_dir_all(&dir);
    std::fs::create_dir_all(&dir).unwrap();
    let db = Database::new(dir.join("db"), SystemConfig::default()).unwrap();
    let conn = Connection::new(&db).unwrap();
    conn.query("CREATE NODE TABLE T(id INT64 PRIMARY KEY, name STRING)").unwrap();
    conn.query("CREATE (:T {id: 1, name: 'a'})").unwrap();
    let out = dir.join("out.parquet");
    let q = format!("COPY (MATCH (t:T) RETURN t.id, t.name) TO '{}'", out.display());
    println!("running: {q}");
    conn.query(&q).unwrap();
    println!("ok: wrote {}", out.display());
}

Run it in a clean container:

docker run --rm -v "$PWD":/src:ro ubuntu:26.04 bash -c '
  apt-get update -qq && apt-get install -y -qq curl ca-certificates build-essential pkg-config libssl-dev >/dev/null
  curl -sSf https://sh.rustup.rs | sh -s -- -y -q --profile minimal; . $HOME/.cargo/env
  cp -r /src /work && cd /work && cargo build -q && ./target/debug/lbug-copy-to-repro; echo "exit code: $?"'

Result:

Image GCC Result
ubuntu:26.04 15.2.0 Segmentation fault (core dumped), exit code 139
ubuntu:24.04 13.3.0 ok: wrote /tmp/lbug-copy-to-repro/out.parquet, exit code 0
ubuntu:26.04 with LBUG_BUILD_FROM_SOURCE=1 15.2.0 passes (checked with our application's test suite)

We currently work around it by setting LBUG_BUILD_FROM_SOURCE=1 on Linux in CI and release builds.

Activity

  1. sunsided commented on Oct 24, 2026

    @sunsided
    Author

    Follow-up: I've opened #1383 with a fix verified against this repro.

    One correction to the suggestions above: -fvisibility=hidden doesn't help here. Visibility only affects the dynamic symbol table, and the static linker still deduplicates COMDAT groups across objects regardless of visibility. Building the prebuilts with a newer GCC would only reverse the mismatch for consumers on older distributions.

    #1383 instead post-processes the bundled static archive behind a new, default-off option LBUG_LOCALIZE_STATIC_SYMBOLS, which the Linux prebuilt workflow enables:

    1. partially link with --force-group-allocation,
    2. make every non-lbug global local,
    3. weaken the leftover STB_GNU_UNIQUE definitions.

    I built v0.21.2 with the patch in manylinux_2_28 / gcc-toolset-13. The resulting archive passes the minimal repro on Ubuntu 26.04 and 24.04, and our 608-test downstream suite on 26.04.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions