diff options
author | jenkins-bot <jenkins-bot@gerrit.wikimedia.org> | 2025-04-07 11:38:35 +0000 |
---|---|---|
committer | Gerrit Code Review <gerrit@wikimedia.org> | 2025-04-07 11:38:35 +0000 |
commit | 88aef6c1421f3543c67c8ee923386971d6e4ece6 (patch) | |
tree | 66678dead7bcf6f75d9939e7a5924724dfb6a217 | |
parent | b53bd618cc8a8fc18f5b1c04b56eb6c4941d1fd1 (diff) | |
parent | 2b65587e4d92e7f27661e8821b14f74ade939cfa (diff) | |
download | mediawikicore-88aef6c1421f3543c67c8ee923386971d6e4ece6.tar.gz mediawikicore-88aef6c1421f3543c67c8ee923386971d6e4ece6.zip |
Merge "block: Fix DBS::acquireTarget() race using GET_LOCK()"
-rw-r--r-- | includes/api/ApiMessageTrait.php | 1 | ||||
-rw-r--r-- | includes/block/DatabaseBlockStore.php | 13 | ||||
-rw-r--r-- | languages/i18n/en.json | 2 | ||||
-rw-r--r-- | tests/api-testing/action/Block.js | 29 |
4 files changed, 44 insertions, 1 deletions
diff --git a/includes/api/ApiMessageTrait.php b/includes/api/ApiMessageTrait.php index 8f796101ce2b..9c2bd7575117 100644 --- a/includes/api/ApiMessageTrait.php +++ b/includes/api/ApiMessageTrait.php @@ -67,6 +67,7 @@ trait ApiMessageTrait { 'importuploaderrorpartial' => 'partialupload', 'importuploaderrorsize' => 'filetoobig', 'importuploaderrortemp' => 'notempdir', + 'ipb-block-not-found' => 'alreadyblocked', 'ipb_already_blocked' => 'alreadyblocked', 'ipb_blocked_as_range' => 'blockedasrange', 'ipb_cant_unblock' => 'cantunblock', diff --git a/includes/block/DatabaseBlockStore.php b/includes/block/DatabaseBlockStore.php index c33157b58157..20f662e044d5 100644 --- a/includes/block/DatabaseBlockStore.php +++ b/includes/block/DatabaseBlockStore.php @@ -1044,6 +1044,7 @@ class DatabaseBlockStore { $targetUserName = (string)$target; $targetUserId = $target->getUserIdentity()->getId( $this->wikiId ); $targetConds = [ 'bt_user' => $targetUserId ]; + $targetLockKey = $dbw->getDomainID() . ':block:u:' . $targetUserId; } else { $targetAddress = (string)$target; $targetUserName = null; @@ -1052,6 +1053,8 @@ class DatabaseBlockStore { 'bt_address' => $targetAddress, 'bt_auto' => $isAuto, ]; + $targetLockKey = $dbw->getDomainID() . ':block:' . + ( $isAuto ? 'a' : 'i' ) . ':' . $targetAddress; } $condsWithCount = $targetConds; @@ -1059,6 +1062,15 @@ class DatabaseBlockStore { $condsWithCount['bt_count'] = $expectedTargetCount; } + $dbw->lock( $targetLockKey, __METHOD__ ); + $func = __METHOD__; + $dbw->onTransactionCommitOrIdle( + static function () use ( $dbw, $targetLockKey, $func ) { + $dbw->unlock( $targetLockKey, "$func.closure" ); + }, + __METHOD__ + ); + // This query locks the index gap when the target doesn't exist yet, // so there is a risk of throttling adjacent block insertions, // especially on small wikis which have larger gaps. If this proves to @@ -1076,6 +1088,7 @@ class DatabaseBlockStore { ->select( [ 'bt_id', 'bt_count' ] ) ->from( 'block_target' ) ->where( $targetConds ) + ->forUpdate() ->caller( __METHOD__ ) ->fetchResultSet(); if ( $res->numRows() > 1 ) { diff --git a/languages/i18n/en.json b/languages/i18n/en.json index a2f807404712..2393d99a6656 100644 --- a/languages/i18n/en.json +++ b/languages/i18n/en.json @@ -2621,7 +2621,7 @@ "blockipsuccesssub": "Block succeeded", "blockipsuccesstext": "[[Special:Contributions/$1|$1]] has been blocked.<br />\nSee the [[Special:BlockList|block list]] to review blocks.", "ipb-empty-block": "The block submitted has no restrictions enabled.", - "ipb-block-not-found": "The block could not be made, but no existing block was found for \"$1\". If this problem persists, please [https://www.mediawiki.org/wiki/Special:MyLanguage/Help_talk:Blocking_users report it].", + "ipb-block-not-found": "The block could not be made, probably because another administrator tried to block this user at the same time. Check the block status and try again.", "ipb-blockingself": "You are about to block yourself! Are you sure you want to do that?", "ipb-confirmhideuser": "You are about to block a user with \"hide user\" enabled. This will suppress the user's name in all lists and log entries. Are you sure you want to do that?", "ipb-confirmaction": "If you are sure you really want to do it, please check the \"{{int:ipb-confirm}}\" field at the bottom.", diff --git a/tests/api-testing/action/Block.js b/tests/api-testing/action/Block.js new file mode 100644 index 000000000000..8053137494b5 --- /dev/null +++ b/tests/api-testing/action/Block.js @@ -0,0 +1,29 @@ +'use strict'; + +const { action, assert } = require( 'api-testing' ); + +describe( 'Block', () => { + const ip = '::' + Math.floor( Math.random() * 65534 ).toString( 16 ); + it( 'should not allow multiblocks without newblock (T389028)', async () => { + const mindy = await action.mindy(); + const token = await mindy.token(); + const promises = [ + mindy.request( { action: 'block', user: ip, token: token }, 'POST' ), + mindy.request( { action: 'block', user: ip, token: token }, 'POST' ) + ]; + const res = await Promise.all( promises ); + assert.lengthOf( res, 2 ); + assert.equal( res[ 0 ].status, 200 ); + assert.equal( res[ 1 ].status, 200 ); + + const goodIndex = 'block' in res[ 0 ].body ? 0 : 1; + const goodBody = res[ goodIndex ].body; + const badBody = res[ +!goodIndex ].body; + + assert.property( goodBody, 'block' ); + assert.isOk( goodBody.block.id ); + + assert.property( badBody, 'error' ); + assert.equal( badBody.error.code, 'alreadyblocked' ); + } ); +} ); |