Skip to content

Fix use-after-free when ending a transaction with registered users - #7

Open
mariuz wants to merge 1 commit into
MWASoftware:mainfrom
mariuz:fix-use-after-free-in-transaction-end
Open

Fix use-after-free when ending a transaction with registered users#7
mariuz wants to merge 1 commit into
MWASoftware:mainfrom
mariuz:fix-use-after-free-in-transaction-end

Conversation

@mariuz

@mariuz mariuz commented Jul 26, 2026

Copy link
Copy Markdown

Reading a WITH TIME ZONE column and then disconnecting raises
EObjectCheck: Object reference is Nil from inside
TFBTransaction.DoDefaultTransactionEnd, reached via
TFBAttachment.EndAllTransactions during Disconnect.

Cause

Reading a time zone value creates TFB30TimeZoneServices, which starts an
internal transaction for the rdb$time_zone_util lookups and registers itself
on it:

FTransaction := FAttachment.StartTransaction([...], taCommit);
(FTransaction as TFBTransaction).AddObject(self);

At disconnect, EndAllTransactions calls DoDefaultTransactionEnd on that
transaction. Its loop calls TransactionEnding on each registered user, and
the time zone services clears its own reference:

(FTransaction as TFBTransaction).Remove(self);
FTransaction := nil;

TInterfaceOwner stores raw TInterfacedObject pointers rather than counted
references, and EndAllTransactions iterates them without taking one, so that
was the last reference to the transaction. The object is destroyed while
DoDefaultTransactionEnd is still executing, and the Commit(Force) at the
end of that same method then runs on freed memory.

Fix

Hold an ITransaction reference to self for the duration of the method, so a
user's TransactionEnding cannot destroy the transaction out from under the
Commit/Rollback that follows.

Reproduction

Against Firebird 6.0.0, with a table holding an integer, a plain TIMESTAMP,
a TIMESTAMP WITH TIME ZONE and a TIME WITH TIME ZONE; each case opens one
TIBQuery, closes it, commits, then disconnects:

selected column before after
integer disconnects cleanly disconnects cleanly
TIMESTAMP disconnects cleanly disconnects cleanly
TIMESTAMP WITH TIME ZONE EObjectCheck disconnects cleanly
TIME WITH TIME ZONE EObjectCheck disconnects cleanly

It is not schema qualification, not the prepared statement (unpreparing first
does not help), and not global temporary tables; freeing the query and the
transaction before disconnecting does not help either, which is what pointed at
the attachment rather than the query.

Found while adding Firebird 4/5/6 support to Marathon, where it also surfaced
through the profiler: PLG$PROF_SESSIONS has TIMESTAMP WITH TIME ZONE
columns, so reading it made the following disconnect fault.

Reading a WITH TIME ZONE column and then disconnecting raises
EObjectCheck: Object reference is Nil from inside
TFBTransaction.DoDefaultTransactionEnd, reached via
TFBAttachment.EndAllTransactions during Disconnect.

The sequence: reading a time zone value creates TFB30TimeZoneServices,
which starts an internal transaction for the rdb$time_zone_util lookups
and registers itself on it with AddObject. At disconnect,
EndAllTransactions calls DoDefaultTransactionEnd on that transaction,
whose loop calls TransactionEnding on each registered user. The time
zone services then clears its own FTransaction, and since the interface
list holds raw object pointers rather than counted references, that was
the last reference - so the transaction is destroyed while
DoDefaultTransactionEnd is still running, and the Commit at the end of
that method operates on a freed object.

Holding an ITransaction reference to self for the duration of the method
keeps it alive until the work is finished.

Reproduced against Firebird 6.0.0 with a twenty-line program: an integer
column and a plain TIMESTAMP disconnect cleanly, TIMESTAMP WITH TIME
ZONE and TIME WITH TIME ZONE do not. With this change all four
disconnect cleanly.
@mariuz
mariuz force-pushed the fix-use-after-free-in-transaction-end branch from fbb29ba to 16e49b4 Compare July 26, 2026 18:32
mariuz added a commit to mariuz/gmarathon-freepascal that referenced this pull request Jul 29, 2026
The WITH TIME ZONE disconnect fault is a use-after-free in fbintf, not
something this codebase can fix from outside. Root cause: reading a time
zone value creates TFB30TimeZoneServices, which starts an internal
transaction for the rdb$time_zone_util lookups and registers itself on
it. At disconnect, DoDefaultTransactionEnd calls TransactionEnding on
each registered user; the time zone services clears its own reference,
and since the interface list holds raw object pointers rather than
counted references, that is the last one. The transaction is destroyed
while DoDefaultTransactionEnd is still running and its closing Commit
operates on freed memory.

Fix proposed upstream as MWASoftware/fbintf#7: hold a reference to self
for the duration of the method. Verified against Firebird 6.0.0 - before
it, TIMESTAMP WITH TIME ZONE and TIME WITH TIME ZONE fault while an
integer and a plain TIMESTAMP do not; after it, all four disconnect
cleanly.

Kept as a patch file rather than by pointing the submodule at a fork, so
that git submodule update keeps working against MWASoftware for
everyone. Marathon does not depend on it being applied: its own queries
cast such columns to text and SafeDisconnect contains the fault
otherwise, and the smoke test reports which of the two states it is in.

patches/README.md also records what was deliberately not patched -
parallel workers and inline ODS upgrade would mean proposing untested
API upstream, TIBQuery parameter types would change binding behaviour
with no regression suite to justify it, and the fb_info_crypt_state
classification did not take effect in testing so its cause is not yet
understood.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
mariuz added a commit to mariuz/gmarathon-freepascal that referenced this pull request Jul 29, 2026
Until MWASoftware/fbintf#7 is merged, lib/fbintf points at
https://github.com/mariuz/fbintf at the commit carrying the
use-after-free fix, so that a fresh git submodule update --init
--recursive builds with it instead of needing the patch applied by hand.
The fork tracks upstream and carries nothing else.

patches/README.md records the single step that undoes this once the pull
request is merged - repoint the URL, drop the branch pin, move the
submodule to an upstream commit containing the fix, and delete the patch
file. The patch is kept alongside the fork so the change stays readable
here and can be applied to any other checkout.

The workarounds this makes unnecessary in principle - casting
WITH TIME ZONE columns to text, and SafeDisconnect - deliberately stay.
They serve anyone building against a stock fbintf, and the casts are
also how the IANA zone name reaches the grid at all rather than being
only a workaround.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
mariuz added a commit to mariuz/gmarathon-freepascal that referenced this pull request Jul 29, 2026
Unblocked by patching the submodules rather than waiting on upstream.
Two things were missing. consts_pub.inc predates Firebird 5 and defines
no isc_spb_bkp_parallel_workers - value 21 in Firebird's own shipped
header, where isc_spb_res_parallel_workers is the same code - and
IBXServices built its service parameter block without one.

The fbintf side now defines the constants and TIBXBackupRestoreService
gained a published ParallelWorkers property, sent only when greater than
one and the server is Firebird 5 or later, so nothing changes for
existing callers or for older servers, which reject the parameter. The
Maintenance dialog offers a worker count on the Backup tab, defaulting
to 1.

Verified against a live Firebird 6.0.0 server: with one worker the
parameter is not sent, with four it is, and the resulting file restores
to a working database. The "with four it is" half was checked with a
temporary trace at the point the parameter is added, because the server
clamps an absurd worker count rather than rejecting it - so a successful
backup on its own proves nothing about whether the parameter was ever
sent.

Both submodules now point at forks pinned to the changes, so a fresh
clone builds with them. Three pull requests are open upstream:
MWASoftware/fbintf#7 (transaction use-after-free), fbintf#8 (the
constants) and ibx4lazarus#23 (the property). The fbintf fork keeps the
two changes on separate branches so those pull requests stay
independent, with a marathon-integration branch carrying both for the
submodule to pin. patches/README.md records the one-step revert for each
once merged.

Sweep is not covered: it takes the parameter through a different service
and that was not exercised here.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant