Fix some atomic exploit

Fix some atomic exploit
This commit is contained in:
novgorodschi catalin
2026-08-13 11:51:54 +03:00
parent 7bad451a83
commit 35f4075734
8 changed files with 708 additions and 62 deletions
@@ -192,6 +192,52 @@ trait DatabaseMovementQueries {
return (mysqli_affected_rows($this->dblink) === 1);
}
/**
* Revendicare ATOMICA a unei miscari care urmeaza sa fie anulata din rally point
* (build.php?mode=troops&cancel=1&moveid=X).
*
* DE CE EXISTA: varianta veche din build.php facea, in trei pasi separati:
* SELECT COUNT(*) ... WHERE proc = 0 AND moveid = X -> daca Total == 1
* UPDATE ... SET proc = 1 WHERE proc = 0 AND moveid = X
* addMovement(4, to, from, REF-UL VECHI, now, end)
* Doua request-uri simultane treceau AMANDOUA de SELECT COUNT (ambele vad Total = 1),
* UPDATE-ul era idempotent, deci amandoua ajungeau la addMovement() si inserau DOUA
* randuri sort_type = 4 cu ACELASI ref. Cron-ul (AutomationTroopMovements) livreaza
* trupele o data PER RAND DE MOVEMENT, nu per ref => trupele se dublau la fiecare
* ciclu trimite-anuleaza: 30k -> 60k -> 120k -> 242k -> ...
*
* Acum proprietatea, tipul, fereastra de 90 de secunde si proc = 0 sunt toate in
* acelasi UPDATE conditionat. Doar request-ul care chiar modifica randul primeste
* true (affected_rows === 1); toate celelalte pierd cursa si nu creeaza nimic.
*
* ANOMALIE SEMNALATA, NU SCHIMBATA TACIT: codul vechi nu verifica deloc sort_type,
* deci se putea "anula" si un sort_type = 4 (retur) al carui `from` era satul tau -
* adica returul unei intariri straine care pleaca din satul tau - iar returul nou
* generat aducea acele trupe LA TINE. Restrictia sort_type IN (3, 5) inchide si
* acest vector si pastreaza exact cele doua cazuri legitime din 16_walking.tpl:
* atac/intarire iesita din sat (3) si colonisti (5).
*
* @param int $moveid ID-ul miscarii
* @param int $wid Satul curent (trebuie sa fie expeditorul)
* @return bool True doar pentru request-ul care a revendicat miscarea
*/
function claimMovementCancel($moveid, $wid) {
$moveid = (int) $moveid;
$wid = (int) $wid;
if ($moveid <= 0 || $wid <= 0) return false;
$limit = time() - 90;
$q = "UPDATE " . TB_PREFIX . "movement SET proc = 1 WHERE moveid = $moveid AND proc = 0 AND `from` = $wid AND sort_type IN (3, 5) AND starttime > $limit";
if (!mysqli_query($this->dblink, $q)) {
return false;
}
return (mysqli_affected_rows($this->dblink) === 1);
}
// no need to cache this method
function getA2b($ckey) {
list($ckey) = $this->escape_input($ckey);
@@ -740,4 +786,4 @@ trait DatabaseMovementQueries {
self::$prisonersCache = [];
}
}
}
@@ -95,6 +95,79 @@ trait DatabaseTroopQueries {
$id = (int) $id;
mysqli_query($this->dblink, "SELECT RELEASE_LOCK('enforce_$id')");
}
/**
* Lock pentru procesarea returului trupelor dintr-o oaza.
*
* Este separat de enforce_$id deoarece o oaza poate avea mai multe
* randuri de reinforcement.
*
* Scop:
* Request A -> citeste reinforcement din oasis
* Request B -> citeste acelasi reinforcement
* => fara lock, ambele pot crea movement de retur.
*/
function getOasisReturnLock($wref) {
$wref = (int) $wref;
$result = mysqli_query(
$this->dblink,
"SELECT GET_LOCK('oasis_return_$wref', 10) AS locked"
);
if (!$result) {
return false;
}
$row = mysqli_fetch_assoc($result);
return isset($row['locked']) && (int)$row['locked'] === 1;
}
/**
* Elibereaza lock-ul pentru returul unei oaze.
*/
function releaseOasisReturnLock($wref) {
$wref = (int) $wref;
mysqli_query(
$this->dblink,
"SELECT RELEASE_LOCK('oasis_return_$wref')"
);
}
/**
* Citeste DIRECT din DB toate reinforcement-urile aflate in oaza.
*
* IMPORTANT:
* Nu folosim cache aici. Functia este apelata DUPA obtinerea lock-ului
* pentru a preveni TOCTOU/race condition.
*/
function getOasisEnforceByWref($wref, $use_cache = false) {
$wref = (int) $wref;
if ($wref <= 0) {
return [];
}
$q = "
SELECT e.*, o.conqured
FROM " . TB_PREFIX . "enforcement AS e
LEFT JOIN " . TB_PREFIX . "odata AS o
ON e.vref = o.wref
WHERE e.vref = $wref
AND o.wref = $wref
AND o.conqured > 0
";
$result = mysqli_query($this->dblink, $q);
if (!$result) {
return [];
}
return $this->mysqli_fetch_all($result);
}
/**
* Add the unit table(s) and troops if presents