Fixes from the phase-1 code review
- one locking protocol per location: backup steps and restores hold a shared lock, prune and untrash an exclusive one and refuse while any run (backup, restore, export) exists; starting a backup checks and creates the run under the exclusive lock, closing the race with prune - snapshot entries are validated when read (relative path without '.', '..', NUL, backslash or control characters); raw restore also checks that the resolved directory stays below the target; the restore API and service accept canonical paths only - recovery kit confirmation requires a recent password confirmation and the confirmation code printed in the current kit file (new code per kit version), proving the kit was actually downloaded - prune decides on the authenticated index files on the location and refreshes the local index cache from them - restore job marks the run failed on any error; restore runs without progress for 6 hours are failed so they cannot block cleanup forever - tests for safe paths and a malicious snapshot path
This commit is contained in:
@@ -33,6 +33,31 @@ class PruneService {
|
||||
}
|
||||
|
||||
public function untrash(Target $target, string $snapshotId): void {
|
||||
$this->lockExclusive($target);
|
||||
try {
|
||||
$this->doUntrash($target, $snapshotId);
|
||||
} finally {
|
||||
$this->locking->releaseLock(self::lockKey($target), ILockingProvider::LOCK_EXCLUSIVE);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Exclusive lock on the location, then make sure no run of any kind (backup, restore, export)
|
||||
* is in progress: their data may not be referenced by a snapshot yet, or is being read.
|
||||
*/
|
||||
private function lockExclusive(Target $target): void {
|
||||
try {
|
||||
$this->locking->acquireLock(self::lockKey($target), ILockingProvider::LOCK_EXCLUSIVE);
|
||||
} catch (LockedException) {
|
||||
throw new \RuntimeException('A backup, restore or cleanup of ' . $target->getName() . ' is in progress; try again later');
|
||||
}
|
||||
if ($this->runs->findRunning($target->getId()) !== []) {
|
||||
$this->locking->releaseLock(self::lockKey($target), ILockingProvider::LOCK_EXCLUSIVE);
|
||||
throw new \RuntimeException('A backup or restore of ' . $target->getName() . ' is running; try again later');
|
||||
}
|
||||
}
|
||||
|
||||
private function doUntrash(Target $target, string $snapshotId): void {
|
||||
$repo = $this->targets->repository($target);
|
||||
if (!array_key_exists($snapshotId, $repo->trash())) {
|
||||
throw new \InvalidArgumentException('That snapshot is not in the trash (or was already deleted)');
|
||||
@@ -84,14 +109,7 @@ class PruneService {
|
||||
if ($target->getAppendOnly()) {
|
||||
throw new \RuntimeException('Location ' . $target->getName() . ' is append-only: NG Backup does not delete anything there');
|
||||
}
|
||||
if ($this->runs->findRunning($target->getId(), BackupService::KIND_FULL) !== []) {
|
||||
throw new \RuntimeException('A backup to ' . $target->getName() . ' is running; try again later');
|
||||
}
|
||||
try {
|
||||
$this->locking->acquireLock(self::lockKey($target), ILockingProvider::LOCK_EXCLUSIVE);
|
||||
} catch (LockedException) {
|
||||
throw new \RuntimeException('Another backup or cleanup of ' . $target->getName() . ' is running');
|
||||
}
|
||||
$this->lockExclusive($target);
|
||||
try {
|
||||
$repo = $this->targets->repository($target);
|
||||
$times = [];
|
||||
|
||||
Reference in New Issue
Block a user