Problem/Motivation
AuditChainLogger::append() reads the latest row and inserts a new row even when the chain lock backend returns FALSE from acquire(). Without serialization, concurrent writers may use the same previous hash, which is incompatible with a linear chain.
Steps to reproduce
- Use Audit Chain 1.7.1 on a disposable Drupal site. The confirmed environment uses PHP 8.4 and PostgreSQL.
- Construct AuditChainLogger with the normal site services, replacing its lock dependency with a test backend whose acquire() always returns FALSE.
- Begin a database transaction, count audit_chain_log rows, call log() with synthetic event metadata, then count rows again.
- Roll back the test transaction.
Actual result: one row is appended despite lock refusal. Expected result: append must not proceed without serialization; evidence-required callers must receive a visible failure, or the event must enter a defined durable retry path.
This reproduction confirms ordinary append behavior. Local signing material was absent; the keyed path shares the append block but needs a separate regression test. A two-connection test is also needed for caller-owned outer transactions before claiming that early lock release causes a committed fork.
Proposed resolution
Make serialization mandatory and ensure it covers the database commit boundary. Test lock refusal, concurrent writers, nested transactions, rollback and exceptions. Do not repair existing forks by rewriting hashes or weakening verification.
Remaining tasks
- Add failing regression tests, including the keyed path.
- Verify behavior with supported database and lock backends.
- Implement and review a fix with documented failure handling.
User interface changes
None proposed.
API and data model changes
To be determined by the tested serialization design.
Comments
Comment #2
jmcerdaFixed in Audit Chain 1.7.2: https://www.drupal.org/project/audit_chain/releases/1.7.2
Both log() and logKeyed() now serialize appends with a database transaction-scoped mutex held through the caller transaction commit or rollback. The chain head uses a locking read. A missing mutex refuses the append; database deadlocks and lock timeouts abort the write. Evidence-required callers must abort the governed action when append fails.
Regression coverage includes missing mutex schema/row, nested rollback, failed head reads, repeatable update 10003, and keyed concurrent commit/rollback workers on PostgreSQL and MySQL. SQLite has separate kernel coverage.
Run database updates to install and seed the mutex table, and replace all old workers before resuming traffic. Mixed old and new writers do not share the serialization contract.
This release prevents the reproduced append defect. It does not repair existing forks, rewrite audit rows or hashes, or replace seals. Historical integrity findings require separate evidence-preserving review.