Added various checks for region file validity (#393)

Check size, check header size, check location table offsets point to valid locations, check for shared offsets, prevent issues with corrupted or junk data
This commit is contained in:
Dylan K. Taylor 2017-04-27 09:14:02 +01:00 committed by GitHub
parent 716efe2549
commit 2a59977440
4 changed files with 130 additions and 10 deletions

View File

@ -0,0 +1,28 @@
<?php
/*
*
* ____ _ _ __ __ _ __ __ ____
* | _ \ ___ ___| | _____| |_| \/ (_)_ __ ___ | \/ | _ \
* | |_) / _ \ / __| |/ / _ \ __| |\/| | | '_ \ / _ \_____| |\/| | |_) |
* | __/ (_) | (__| < __/ |_| | | | | | | | __/_____| | | | __/
* |_| \___/ \___|_|\_\___|\__|_| |_|_|_| |_|\___| |_| |_|_|
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* @author PocketMine Team
* @link http://www.pocketmine.net/
*
*
*/
namespace pocketmine\level\format\io\region;
class CorruptedRegionException extends RegionException{
}

View File

@ -30,6 +30,7 @@ use pocketmine\level\format\io\ChunkUtils;
use pocketmine\level\format\SubChunk;
use pocketmine\level\generator\Generator;
use pocketmine\level\Level;
use pocketmine\level\LevelException;
use pocketmine\nbt\NBT;
use pocketmine\nbt\tag\{
ByteArrayTag, ByteTag, CompoundTag, IntArrayTag, IntTag, ListTag, LongTag, StringTag
@ -219,12 +220,9 @@ class McRegion extends BaseLevelProvider{
$isValid = (file_exists($path . "/level.dat") and is_dir($path . "/region/"));
if($isValid){
$files = glob($path . "/region/*.mc*");
if(empty($files)){ //possible glob() issue on some systems
$files = array_filter(scandir($path . "/region/"), function($file){
return substr($file, strrpos($file, ".") + 1, 2) === "mc"; //region file
});
}
$files = array_filter(scandir($path . "/region/"), function($file){
return substr($file, strrpos($file, ".") + 1, 2) === "mc"; //region file
});
foreach($files as $f){
if(substr($f, strrpos($f, ".") + 1) !== static::REGION_FILE_EXTENSION){
@ -442,6 +440,22 @@ class McRegion extends BaseLevelProvider{
protected function loadRegion(int $x, int $z){
if(!isset($this->regions[$index = Level::chunkHash($x, $z)])){
$this->regions[$index] = new RegionLoader($this, $x, $z, static::REGION_FILE_EXTENSION);
try{
$this->regions[$index]->open();
}catch(CorruptedRegionException $e){
$logger = $this->level->getServer()->getLogger();
$logger->error("Corrupted region file detected: " . $e->getMessage());
$this->regions[$index]->close(false); //Do not write anything to the file
$path = $this->regions[$index]->getFilePath();
$backupPath = $path . ".bak." . time();
rename($path, $backupPath);
$logger->error("Corrupted region file has been backed up to " . $backupPath);
$this->regions[$index] = new RegionLoader($this, $x, $z, static::REGION_FILE_EXTENSION);
$this->regions[$index]->open(); //this will create a new empty region to replace the corrupted one
}
}
}

View File

@ -0,0 +1,28 @@
<?php
/*
*
* ____ _ _ __ __ _ __ __ ____
* | _ \ ___ ___| | _____| |_| \/ (_)_ __ ___ | \/ | _ \
* | |_) / _ \ / __| |/ / _ \ __| |\/| | | '_ \ / _ \_____| |\/| | |_) |
* | __/ (_) | (__| < __/ |_| | | | | | | | __/_____| | | | __/
* |_| \___/ \___|_|\_\___|\__|_| |_|_|_| |_|\___| |_| |_|_|
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* @author PocketMine Team
* @link http://www.pocketmine.net/
*
*
*/
namespace pocketmine\level\format\io\region;
class RegionException extends \RuntimeException{
}

View File

@ -25,6 +25,7 @@ namespace pocketmine\level\format\io\region;
use pocketmine\level\format\Chunk;
use pocketmine\level\format\io\ChunkException;
use pocketmine\level\LevelException;
use pocketmine\utils\Binary;
use pocketmine\utils\MainLogger;
@ -32,7 +33,11 @@ class RegionLoader{
const VERSION = 1;
const COMPRESSION_GZIP = 1;
const COMPRESSION_ZLIB = 2;
const MAX_SECTOR_LENGTH = 256 << 12; //256 sectors, (1 MiB)
const REGION_HEADER_LENGTH = 8192; //4096 location table + 4096 timestamps
const MAX_REGION_FILE_SIZE = 32 * 32 * self::MAX_SECTOR_LENGTH + self::REGION_HEADER_LENGTH; //32 * 32 1MiB chunks + header size
public static $COMPRESSION_LEVEL = 7;
protected $x;
@ -51,10 +56,21 @@ class RegionLoader{
$this->z = $regionZ;
$this->levelProvider = $level;
$this->filePath = $this->levelProvider->getPath() . "region/r.$regionX.$regionZ.$fileExtension";
}
public function open(){
$exists = file_exists($this->filePath);
if(!$exists){
touch($this->filePath);
}else{
$fileSize = filesize($this->filePath);
if($fileSize > self::MAX_REGION_FILE_SIZE){
throw new CorruptedRegionException("Corrupted oversized region file found, should be a maximum of " . self::MAX_REGION_FILE_SIZE . " bytes, got " . $fileSize . " bytes");
}elseif($fileSize % 4096 !== 0){
throw new CorruptedRegionException("Region file should be padded to a multiple of 4KiB");
}
}
$this->filePointer = fopen($this->filePath, "r+b");
stream_set_read_buffer($this->filePointer, 1024 * 16); //16KB
stream_set_write_buffer($this->filePointer, 1024 * 16); //16KB
@ -170,9 +186,20 @@ class RegionLoader{
return $x + ($z << 5);
}
public function close(){
$this->writeLocationTable();
fclose($this->filePointer);
/**
* Writes the region header and closes the file
*
* @param bool $writeHeader
*/
public function close(bool $writeHeader = true){
if(is_resource($this->filePointer)){
if($writeHeader){
$this->writeLocationTable();
}
fclose($this->filePointer);
}
$this->levelProvider = null;
}
@ -255,14 +282,34 @@ class RegionLoader{
fseek($this->filePointer, 0);
$this->lastSector = 1;
$data = unpack("N*", fread($this->filePointer, 4 * 1024 * 2)); //1024 records * 4 bytes * 2 times
$headerRaw = fread($this->filePointer, self::REGION_HEADER_LENGTH);
if(($len = strlen($headerRaw)) !== self::REGION_HEADER_LENGTH){
throw new CorruptedRegionException("Invalid region file header, expected " . self::REGION_HEADER_LENGTH . " bytes, got " . $len . " bytes");
}
$data = unpack("N*", $headerRaw);
$usedOffsets = [];
for($i = 0; $i < 1024; ++$i){
$index = $data[$i + 1];
$offset = $index >> 8;
if($offset !== 0){
fseek($this->filePointer, ($offset << 12));
if(fgetc($this->filePointer) === false){ //Try and read from the location
throw new CorruptedRegionException("Region file location offset points to invalid location");
}elseif(isset($usedOffsets[$offset])){
throw new CorruptedRegionException("Found two chunk offsets pointing to the same location");
}else{
$usedOffsets[$offset] = true;
}
}
$this->locationTable[$i] = [$index >> 8, $index & 0xff, $data[1024 + $i + 1]];
if(($this->locationTable[$i][0] + $this->locationTable[$i][1] - 1) > $this->lastSector){
$this->lastSector = $this->locationTable[$i][0] + $this->locationTable[$i][1] - 1;
}
}
fseek($this->filePointer, 0);
}
private function writeLocationTable(){
@ -300,4 +347,7 @@ class RegionLoader{
return $this->z;
}
public function getFilePath() : string{
return $this->filePath;
}
}