fb-cpp releases a Firebird interface a second time after the calls that end the life of a Firebird object. For example, Transaction::commit() calls ITransaction::commit() and then handle.reset():
handle->commit(&statusWrapper);
handle.reset();
But on success these Firebird calls already release the interface. doc/Using_OO_API.md says so for IResultSet::close(), IStatement::free(), IAttachment::detach() and dropDatabase(), IBlob::close() and cancel(), and IService::detach() ("On success releases interface"), and a test shows the same for ITransaction::commit() and rollback(). The reset() that follows is a second release of an object that is already gone.
The same pattern is in:
Transaction::commit(), Transaction::rollback()
Statement::free(), and the result set that is closed when a statement is executed again
Attachment::disconnect(), Attachment::dropDatabase()
Blob::close(), Blob::cancel()
ServiceManager (IService::detach())
How to reproduce
The program holds two extra references around each call and then looks at the reference count; one release is expected.
#include <firebird/Interface.h>
#include <fb-cpp/fb-cpp.h>
#include <iostream>
#include <string>
using namespace Firebird;
extern "C" IMaster* ISC_EXPORT fb_get_master_interface();
// Holds two extra references around a call; released() tells how many
// references the call gave back. Expected: 1.
class Probe
{
public:
explicit Probe(IReferenceCounted* p)
: ptr(p)
{
ptr->addRef();
before = ptr->release();
ptr->addRef();
ptr->addRef();
}
int released()
{
const int after = ptr->release();
if (after > 0)
ptr->release();
return before + 1 - after;
}
private:
IReferenceCounted* ptr;
int before = 0;
};
int main(int argc, char** argv)
{
const std::string database = argc > 1 ? argv[1] : "localhost:/tmp/fbcpp-release.fdb";
fbcpp::Client client{ fb_get_master_interface() };
fbcpp::Attachment attachment{ client, database, fbcpp::AttachmentOptions()
.setUserName("SYSDBA").setPassword("masterkey").setCreateDatabase(true) };
{
fbcpp::Transaction transaction{ attachment };
IReferenceCounted* handle = transaction.getHandle().get();
Probe probe(handle);
transaction.commit();
std::cout << "Transaction::commit(): " << probe.released() << " releases\n";
}
{
fbcpp::Transaction transaction{ attachment };
fbcpp::Statement statement{ attachment, transaction, "select 1 from rdb$database" };
statement.execute(transaction);
// the handles first: getHandle() returns a temporary reference
IReferenceCounted* resultSetHandle = statement.getResultSetHandle().get();
IReferenceCounted* statementHandle = statement.getStatementHandle().get();
Probe resultSet(resultSetHandle);
Probe stmt(statementHandle);
statement.free();
std::cout << "Statement::free(), IResultSet: " << resultSet.released() << " releases\n";
std::cout << "Statement::free(), IStatement: " << stmt.released() << " releases\n";
IReferenceCounted* transactionHandle = transaction.getHandle().get();
Probe tra(transactionHandle);
transaction.rollback();
std::cout << "Transaction::rollback(): " << tra.released() << " releases\n";
}
fbcpp::Attachment second{ client, database, fbcpp::AttachmentOptions()
.setUserName("SYSDBA").setPassword("masterkey") };
{
IReferenceCounted* attachmentHandle = second.getHandle().get();
Probe probe(attachmentHandle);
second.disconnect();
std::cout << "Attachment::disconnect(): " << probe.released() << " releases\n";
}
attachment.dropDatabase();
return 0;
}
Output, the same with the client libraries of Firebird 5.0.4 and 3.0.11:
Transaction::commit(): 2 releases
Statement::free(), IResultSet: 2 releases
Statement::free(), IStatement: 2 releases
Transaction::rollback(): 2 releases
Attachment::disconnect(): 2 releases
Measured the same way, the Firebird calls alone (ITransaction::commit(), rollback(), IResultSet::close(), IStatement::free(), IAttachment::detach()) release exactly once, with both client libraries; each interface has one reference before the call.
Checked with main at 30a341b (v1.0.0), a Firebird 5.0.4 server, on Linux with g++ 13.3.
Possible fix
After a successful call, forget the pointer without releasing it, for example with a member of FbRef that works like std::unique_ptr::release(). When the call fails, the interface is not released by Firebird, so the reference stays and is released as before.
fb-cpp releases a Firebird interface a second time after the calls that end the life of a Firebird object. For example,
Transaction::commit()callsITransaction::commit()and thenhandle.reset():handle->commit(&statusWrapper); handle.reset();But on success these Firebird calls already release the interface.
doc/Using_OO_API.mdsays so forIResultSet::close(),IStatement::free(),IAttachment::detach()anddropDatabase(),IBlob::close()andcancel(), andIService::detach()("On success releases interface"), and a test shows the same forITransaction::commit()androllback(). Thereset()that follows is a second release of an object that is already gone.The same pattern is in:
Transaction::commit(),Transaction::rollback()Statement::free(), and the result set that is closed when a statement is executed againAttachment::disconnect(),Attachment::dropDatabase()Blob::close(),Blob::cancel()ServiceManager(IService::detach())How to reproduce
The program holds two extra references around each call and then looks at the reference count; one release is expected.
Output, the same with the client libraries of Firebird 5.0.4 and 3.0.11:
Measured the same way, the Firebird calls alone (
ITransaction::commit(),rollback(),IResultSet::close(),IStatement::free(),IAttachment::detach()) release exactly once, with both client libraries; each interface has one reference before the call.Checked with
mainat 30a341b (v1.0.0), a Firebird 5.0.4 server, on Linux with g++ 13.3.Possible fix
After a successful call, forget the pointer without releasing it, for example with a member of
FbRefthat works likestd::unique_ptr::release(). When the call fails, the interface is not released by Firebird, so the reference stays and is released as before.