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
-
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
-
Copy both .so into <firebird>/plugins/udr/ and restart the server.
-
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 1 — coll_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
- Can you confirm the Linux-only behaviour and that it persists on 5.0.4?
- If the cause is global symbol visibility on
dlopen, would loading UDR
modules with RTLD_LOCAL be an acceptable fix?
- 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
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 routineto 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
1 | 2)1 | 2)Plugin: stock
udr_engine. Server architecture (embedded vs SuperServer) is ruledout. 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), andthe 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
dlopenwith globalsymbol visibility /
RTLD_GLOBAL), so the first-loaded module's routine of a givenname 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_aandudrcoll_b, each register a C++ UDR function whoseroutine name is identical (
collide) and whose signature is identical(
() RETURNS INTEGER). The only difference is the returned constant:udrcoll_areturns1,udrcoll_breturns2.Two SQL functions are declared, each pointing at its own module:
Steps to reproduce
Build both modules (sources attached:
mod_a.cpp,mod_b.cpp):Copy both
.sointo<firebird>/plugins/udr/and restart the server.Run against any database:
Expected result
coll_a→ 1,coll_b→ 2:Actual result
Linux 5.0.3 (bug)
Both return
1—coll_b, though declared asudrcoll_b!collide, runsudrcoll_a's implementation (the module loaded first). Independent of call order:Linux 5.0.4 (bug — same defect after upgrade)
Both return
2this time — the first-loaded module happened to beudrcoll_b.Which module wins depends on load order; the defect (module qualifier ignored) is
identical. Independent of call order:
Windows 5.0.4 (correct — for contrast)
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
.soand restarted, but theold 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
dlopen, would loading UDRmodules with
RTLD_LOCALbe an acceptable fix?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