Skip to content

UDR: EXTERNAL NAME module qualifier ignored on Linux when two modules register a routine of the same name — first-loaded module wins (5.0.3 and 5.0.4) #9105

Description

@ov-soft

Summary

When two different UDR modules each register a routine with the same
routine name and the same signature
, Firebird on Linux ignores the module
part of EXTERNAL NAME 'module!routine' and dispatches every such SQL routine
to the implementation in the module that was loaded first. A SQL function
explicitly bound to module B silently executes module A's code — no error, no
warning.

Reproduces on Linux 5.0.3 and 5.0.4. Does not reproduce on Windows
5.0.4
(correct per-module resolution, both embedded and SuperServer). Isolated
to the platform: it is not a version regression — upgrading 5.0.3 → 5.0.4 does
not fix it on Linux.

Environment

Setup Result
Linux (Debian), 5.0.3 BUG — both functions return the first-loaded module's value
Linux (Debian), 5.0.4 (build 5.0.4.1812) BUG — same (first-loaded module wins)
Windows, 5.0.4, embedded correct (1 | 2)
Windows, 5.0.4, SuperServer correct (1 | 2)

Plugin: stock udr_engine. Server architecture (embedded vs SuperServer) is ruled
out. The differentiator is the OS (Linux), not the engine version.

Likely cause (hypothesis)

Every UDR module exports the same entry-point symbol (firebird_udr_plugin), and
the C++ helper macros register routine factories into a module-static registry. On
Linux the shared objects appear to be loaded such that these identically-named
symbols/registries are merged across modules (typical dlopen with global
symbol visibility / RTLD_GLOBAL), so the first-loaded module's routine of a given
name shadows the same-named routine of every other module. On Windows each DLL has
an isolated export namespace, so no merge happens. Loading each UDR module with
RTLD_LOCAL (or otherwise isolating per-module symbols) would likely fix it.

Description

Two modules, udrcoll_a and udrcoll_b, each register a C++ UDR function whose
routine name is identical (collide) and whose signature is identical
(() RETURNS INTEGER). The only difference is the returned constant:
udrcoll_a returns 1, udrcoll_b returns 2.

Two SQL functions are declared, each pointing at its own module:

create or alter function coll_a returns integer external name 'udrcoll_a!collide' engine udr;
create or alter function coll_b returns integer external name 'udrcoll_b!collide' engine udr;

Steps to reproduce

  1. Build both modules (sources attached: mod_a.cpp, mod_b.cpp):

    g++ -shared -fPIC -O2 -std=c++17 -I$FB/include -I$FB/include/firebird mod_a.cpp -o udrcoll_a.so
    g++ -shared -fPIC -O2 -std=c++17 -I$FB/include -I$FB/include/firebird mod_b.cpp -o udrcoll_b.so
  2. Copy both .so into <firebird>/plugins/udr/ and restart the server.

  3. Run against any database:

    create or alter function coll_a returns integer external name 'udrcoll_a!collide' engine udr;
    create or alter function coll_b returns integer external name 'udrcoll_b!collide' engine udr;
    commit;
    select coll_a() as a_should_be_1, coll_b() as b_should_be_2 from rdb$database;

Expected result

coll_a → 1, coll_b → 2:

A_SHOULD_BE_1                   1
B_SHOULD_BE_2                   2

Actual result

Linux 5.0.3 (bug)

Both return 1coll_b, though declared as udrcoll_b!collide, runs
udrcoll_a's implementation (the module loaded first). Independent of call order:

-- select coll_a(), coll_b()
A_SHOULD_BE_1                   1
B_SHOULD_BE_2                   1
-- select coll_b(), coll_a()
B_SHOULD_BE_2                   1
A_SHOULD_BE_1                   1

Linux 5.0.4 (bug — same defect after upgrade)

Both return 2 this time — the first-loaded module happened to be udrcoll_b.
Which module wins depends on load order; the defect (module qualifier ignored) is
identical. Independent of call order:

-- select coll_a(), coll_b()
A_SHOULD_BE_1                   2
B_SHOULD_BE_2                   2
-- select coll_b(), coll_a()
B_SHOULD_BE_2                   2
A_SHOULD_BE_1                   2

Windows 5.0.4 (correct — for contrast)

SHOULD_BE_1                     1
SHOULD_BE_2                     2

Impact

Deploying a new version of a UDR plugin alongside an older one that still
exports routines with the same names causes the new plugin's routines to be
shadowed by the old module — the module! qualifier does not disambiguate them.
Real-world symptom on Linux: "we deployed the fixed .so and restarted, but the
old behaviour persists / a newly added output column comes back as NULL", with
nothing in the log. Because resolution follows load order, the failure is
non-deterministic across deployments.

Questions for maintainers

  1. Can you confirm the Linux-only behaviour and that it persists on 5.0.4?
  2. If the cause is global symbol visibility on dlopen, would loading UDR
    modules with RTLD_LOCAL be an acceptable fix?
  3. Is there a supported way today to run two UDR modules that expose
    same-named routines side by side on Linux, other than renaming the routines?

Attachments

mod_a.cpp, mod_b.cpp, register.sql, build.sh, README.md.

build.sh
mod_a.cpp
mod_b.cpp
README.md
register.sql

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