Repository navigation
Race condition in co-author list updates — move to one postmeta row per co-author #12
Description
Summary
The co-author list is currently stored as a single serialized array under the _map_co_authors postmeta key, and mutated with a read-modify-write pattern in Co_Authors::add_co_author() / ::remove_co_author(). Two concurrent writes (likely for the invite flow — "post goes viral, N users click at once") read the same snapshot, each append their own user, and last-write-wins drops the other additions silently.
Where
includes/class-map-co-authors.php, lines 68–104. Both add_co_author and remove_co_author fetch the current array via get_co_authors() and overwrite with update_post_meta().
Suggested fix — one row per co-author
Move to add_post_meta( $post_id, '_map_co_author', $user_id, false ) with unique = false, and query via get_post_meta( $post_id, '_map_co_author' ) which returns an array of values.
Benefits:
- Each add is an atomic
INSERT(after an existence check); no RMW window. delete_post_meta( $post_id, '_map_co_author', $user_id )is an atomic targeted delete.is_co_authorcan be a single narrow query against the meta table.- Same
wp_postmetatable, no schema changes.
The only concurrent-insert race remaining is two simultaneous adds for the same user, which is benign (idempotent check-then-insert — worst case, we get two identical rows; dedupe on read, or add a transient lock on the check).
Meta key change
Rename the meta key from _map_co_authors (array) to _map_co_author (singular, multiple rows). This makes them different keys so we don't trip over a stale serialized array if the plugin is downgraded, and makes the new shape self-describing.
Migration
On upgrade, one-shot migration:
add_action( 'init', __NAMESPACE__ . '\\maybe_migrate_co_authors_storage' );
function maybe_migrate_co_authors_storage(): void {
if ( '2' === get_option( 'map_db_version' ) ) {
return;
}
// Query all posts with the legacy meta, insert per-row, delete legacy.
// Batch via get_posts/paged. Guard with a transient/lock for safety.
update_option( 'map_db_version', '2' );
}Or, if willing to accept one-time data loss (plugin has no releases yet), just switch the key and document it. Given there are no release users yet, the latter is fine for this PR.
Interaction with #8
The register_post_meta() call needs updating to the new key. Tightening the auth_callback (issue #8) happens in the same block, so these two could land together in one PR if convenient — or #7 first, then #8 on top.
Tests to add / update
- All existing
Test_Co_Authorstests should pass against the new storage. - New test: concurrent add simulation — call
add_co_authorfor two different users after each has calledget_co_authors(simulate RMW) and assert both end up stored. - New test:
remove_co_authorremoves exactly the one user, leaves others.
References
Internal security review, Apr 2026. Likely lands alongside #8.
Activity
Security review batch: fix REST gates, invite storage, and co-author …
dd32 commentedon Apr 23, 2026
Fixed in b4a71c1 (on trunk).