Fix parallel building exploit via race condition (#141)

Co-authored-by: hdmaniak <bombo15.forum@gmail.com>
This commit is contained in:
hdmaniak2
2026-03-24 07:48:54 +01:00
committed by GitHub
parent a7bdac8392
commit e83985e7f7
2 changed files with 136 additions and 99 deletions
+116 -91
View File
@@ -309,6 +309,9 @@ class Building {
public function loadBuilding() { public function loadBuilding() {
global $database,$village,$session; global $database,$village,$session;
$this->basic = 0;
$this->inner = 0;
$this->plus = 0;
$this->buildArray = $database->getJobs($village->wid); $this->buildArray = $database->getJobs($village->wid);
$this->allocated = count($this->buildArray); $this->allocated = count($this->buildArray);
if($this->allocated > 0) { if($this->allocated > 0) {
@@ -345,130 +348,152 @@ class Building {
private function upgradeBuilding($id) { private function upgradeBuilding($id) {
global $database, $village, $session, $logging, ${'bid'.$village->resarray['f'.$id.'t']}; global $database, $village, $session, $logging, ${'bid'.$village->resarray['f'.$id.'t']};
if($this->allocated < $this->maxConcurrent) { if(!$database->getBuildLock($village->wid)) return;
$uprequire = $this->resourceRequired($id,$village->resarray['f'.$id.'t']); try {
$time = time() + $uprequire['time']; $this->loadBuilding();
$bindicate = $this->canBuild($id,$village->resarray['f'.$id.'t']);
// don't allow building above max levels and don't allow building if it's in demolition if($this->allocated < $this->maxConcurrent) {
if (in_array($bindicate, [1, 2, 3, 10, 11])) { $uprequire = $this->resourceRequired($id,$village->resarray['f'.$id.'t']);
header("Location: dorf2.php"); $time = time() + $uprequire['time'];
exit; $bindicate = $this->canBuild($id,$village->resarray['f'.$id.'t']);
}
$loop = ($bindicate == 9 ? 1 : 0); // don't allow building above max levels and don't allow building if it's in demolition
$loopsame = 0; if (in_array($bindicate, [1, 2, 3, 10, 11])) {
if($loop == 1) { $database->releaseBuildLock($village->wid);
foreach($this->buildArray as $build) { header("Location: dorf2.php");
if($build['field'] == $id) { exit;
$loopsame++;
$uprequire = $this->resourceRequired($id, $village->resarray['f'.$id.'t'], ($loopsame > 0 ? 2 : 1));
}
} }
if($session->tribe == 1 || ALLOW_ALL_TRIBE) {
if($id >= 19) { $loop = ($bindicate == 9 ? 1 : 0);
foreach($this->buildArray as $build) { $loopsame = 0;
if($build['field'] >= 19) { if($loop == 1) {
$time = $build['timestamp'] + $uprequire['time']; foreach($this->buildArray as $build) {
if($build['field'] == $id) {
$loopsame++;
$uprequire = $this->resourceRequired($id, $village->resarray['f'.$id.'t'], ($loopsame > 0 ? 2 : 1));
}
}
if($session->tribe == 1 || ALLOW_ALL_TRIBE) {
if($id >= 19) {
foreach($this->buildArray as $build) {
if($build['field'] >= 19) {
$time = $build['timestamp'] + $uprequire['time'];
}
}
}
else {
foreach($this->buildArray as $build) {
if($build['field'] <= 18) {
$time = $build['timestamp'] + $uprequire['time'];
}
} }
} }
} }
else { else {
foreach($this->buildArray as $build) { $time = $this->buildArray[0]['timestamp'] + $uprequire['time'];
if($build['field'] <= 18) {
$time = $build['timestamp'] + $uprequire['time'];
}
}
} }
} }
else { $level = $database->getResourceLevel($village->wid);
$time = $this->buildArray[0]['timestamp'] + $uprequire['time'];
if($database->addBuilding($village->wid, $id, $village->resarray['f'.$id.'t'], $loop, $time + ($loop == 1 ? ceil(60 / SPEED) : 0), 0, $level['f'.$id] + 1 + count($database->getBuildingByField($village->wid, $id)))) {
$database->modifyResource($village->wid, $uprequire['wood'], $uprequire['clay'], $uprequire['iron'], $uprequire['crop'], 0);
$logging->addBuildLog($village->wid, self::procResType($village->resarray['f'.$id.'t']), ($village->resarray['f'.$id] + ($loopsame > 0 ? 2 : 1)), 0);
$this->redirect($id);
} }
} }
$level = $database->getResourceLevel($village->wid); } finally {
$database->releaseBuildLock($village->wid);
if($database->addBuilding($village->wid, $id, $village->resarray['f'.$id.'t'], $loop, $time + ($loop == 1 ? ceil(60 / SPEED) : 0), 0, $level['f'.$id] + 1 + count($database->getBuildingByField($village->wid, $id)))) {
$database->modifyResource($village->wid, $uprequire['wood'], $uprequire['clay'], $uprequire['iron'], $uprequire['crop'], 0);
$logging->addBuildLog($village->wid, self::procResType($village->resarray['f'.$id.'t']), ($village->resarray['f'.$id] + ($loopsame > 0 ? 2 : 1)), 0);
$this->redirect($id);
}
} }
} }
private function downgradeBuilding($id) { private function downgradeBuilding($id) {
global $database, $village, $session, $logging; global $database, $village, $session, $logging;
if($this->allocated < $this->maxConcurrent) { if(!$database->getBuildLock($village->wid)) return;
$name = "bid".$village->resarray['f'.$id.'t']; try {
global $$name; $this->loadBuilding();
$dataarray = $$name;
$time = time() + round($dataarray[$village->resarray['f'.$id]-1]['time'] / 4); if($this->allocated < $this->maxConcurrent) {
$loop = 0; $name = "bid".$village->resarray['f'.$id.'t'];
if($this->inner == 1 || $this->basic == 1) { global $$name;
if(($session->plus || $village->resarray['f'.$id.'t']==40)&& $this->plus == 0) { $dataarray = $$name;
$loop = 1; $time = time() + round($dataarray[$village->resarray['f'.$id]-1]['time'] / 4);
$loop = 0;
if($this->inner == 1 || $this->basic == 1) {
if(($session->plus || $village->resarray['f'.$id.'t']==40)&& $this->plus == 0) {
$loop = 1;
}
} }
} if($loop == 1) {
if($loop == 1) { if($session->tribe == 1 || ALLOW_ALL_TRIBE) {
if($session->tribe == 1 || ALLOW_ALL_TRIBE) { if($id >= 19) {
if($id >= 19) { foreach($this->buildArray as $build) {
foreach($this->buildArray as $build) { if($build['field'] >= 19) {
if($build['field'] >= 19) { $time = $build['timestamp'] + round($dataarray[$village->resarray['f'.$id]-1]['time'] / 4);
$time = $build['timestamp'] + round($dataarray[$village->resarray['f'.$id]-1]['time'] / 4); }
} }
} }
} }
else {
$time = $this->buildArray[0]['timestamp'] + round($dataarray[$village->resarray['f'.$id]-1]['time'] / 4);
}
} }
else {
$time = $this->buildArray[0]['timestamp'] + round($dataarray[$village->resarray['f'.$id]-1]['time'] / 4);
}
}
$level = $database->getResourceLevel($village->wid); $level = $database->getResourceLevel($village->wid);
if($database->addBuilding($village->wid, $id, $village->resarray['f'.$id.'t'], $loop, $time, 0, 0, $level['f'.$id] + 1 + count($database->getBuildingByField($village->wid, $id)))){ if($database->addBuilding($village->wid, $id, $village->resarray['f'.$id.'t'], $loop, $time, 0, 0, $level['f'.$id] + 1 + count($database->getBuildingByField($village->wid, $id)))){
$logging->addBuildLog($village->wid, self::procResType($village->resarray['f'.$id.'t']), ($village->resarray['f'.$id] - 1), 2); $logging->addBuildLog($village->wid, self::procResType($village->resarray['f'.$id.'t']), ($village->resarray['f'.$id] - 1), 2);
header("Location: dorf2.php"); header("Location: dorf2.php");
exit(); exit();
}
} }
} finally {
$database->releaseBuildLock($village->wid);
} }
} }
private function constructBuilding($id, $tid) { private function constructBuilding($id, $tid) {
global $database, $village, $session, $logging; global $database, $village, $session, $logging;
if($this->allocated < $this->maxConcurrent) { if(!$database->getBuildLock($village->wid)) return;
if($tid == 16) $id = 39; try {
elseif($tid == 31 || $tid == 32 || $tid == 33) $id = 40; $this->loadBuilding();
$uprequire = $this->resourceRequired($id, $tid); if($this->allocated < $this->maxConcurrent) {
$time = time() + $uprequire['time']; if($tid == 16) $id = 39;
$bindicate = $this->canBuild($id, $tid); elseif($tid == 31 || $tid == 32 || $tid == 33) $id = 40;
$loop = ($bindicate == 9 ? 1 : 0);
if($loop == 1) { $uprequire = $this->resourceRequired($id, $tid);
foreach( $this->buildArray as $build) { $time = time() + $uprequire['time'];
if($build['field'] >= 19 || ($session->tribe <> 1 && !ALLOW_ALL_TRIBE)) { $bindicate = $this->canBuild($id, $tid);
$time = $build['timestamp'] + ceil(60 / SPEED) + $uprequire['time']; $loop = ($bindicate == 9 ? 1 : 0);
}
}
}
if($this->meetRequirement($tid)) { if($loop == 1) {
$level = $database->getResourceLevel($village->wid); foreach( $this->buildArray as $build) {
if($database->addBuilding($village->wid, $id, $tid, $loop, $time, 0, $level['f' . $id] + 1 + count($database->getBuildingByField($village->wid, $id)))){ if($build['field'] >= 19 || ($session->tribe <> 1 && !ALLOW_ALL_TRIBE)) {
$logging->addBuildLog($village->wid, self::procResType($tid), ($village->resarray['f' . $id] + 1), 1); $time = $build['timestamp'] + ceil(60 / SPEED) + $uprequire['time'];
$database->modifyResource($village->wid, $uprequire['wood'], $uprequire['clay'], $uprequire['iron'], $uprequire['crop'], 0); }
header("Location: dorf2.php"); }
exit; }
}
}else{ if($this->meetRequirement($tid)) {
header("location: dorf2.php"); $level = $database->getResourceLevel($village->wid);
exit; if($database->addBuilding($village->wid, $id, $tid, $loop, $time, 0, $level['f' . $id] + 1 + count($database->getBuildingByField($village->wid, $id)))){
} $logging->addBuildLog($village->wid, self::procResType($tid), ($village->resarray['f' . $id] + 1), 1);
}else{ $database->modifyResource($village->wid, $uprequire['wood'], $uprequire['clay'], $uprequire['iron'], $uprequire['crop'], 0);
header("Location: dorf2.php"); header("Location: dorf2.php");
exit; exit;
}
}else{
header("location: dorf2.php");
exit;
}
}else{
header("Location: dorf2.php");
exit;
}
} finally {
$database->releaseBuildLock($village->wid);
} }
} }
+12
View File
@@ -4556,6 +4556,18 @@ References: User ID/Message ID, Mode
return mysqli_query($this->dblink,$q); return mysqli_query($this->dblink,$q);
} }
function getBuildLock($wid) {
$wid = (int) $wid;
$result = mysqli_query($this->dblink, "SELECT GET_LOCK('build_village_$wid', 10) AS locked");
$row = mysqli_fetch_assoc($result);
return $row['locked'] == 1;
}
function releaseBuildLock($wid) {
$wid = (int) $wid;
mysqli_query($this->dblink, "SELECT RELEASE_LOCK('build_village_$wid')");
}
/** /**
* Get the time required to build a specified building * Get the time required to build a specified building
* *