mirror of
https://we.phorge.it/source/phorge.git
synced 2024-11-22 14:52:41 +01:00
Provide "bin/files integrity" for debugging, maintaining and backfilling integrity hashes
Summary: Ref T12470. Provides an "integrity" utility which runs in these modes: - Verify: check that hashes match. - Compute: backfill missing hashes. - Strip: remove hashes. Useful for upgrading across a hash change. - Corrupt: intentionally corrupt hashes. Useful for debugging. - Overwrite: force hash recomputation. Users normally shouldn't need to run any of this stuff, but this provides a reasonable toolkit for managing integrity hashes. I'll recommend existing installs use `bin/files integrity --compute all` in the upgrade guidance to backfill hashes for existing files. Test Plan: - Ran the script in many modes against various files, saw expected operation, including: - Verified a file, corrupted it, saw it fail. - Verified a file, stripped it, saw it have no hash. - Stripped a file, computed it, got a clean verify. - Stripped a file, overwrote it, got a clean verify. - Corrupted a file, overwrote it, got a clean verify. - Overwrote a file, overwrote again, got a no-op. Reviewers: chad Reviewed By: chad Maniphest Tasks: T12470 Differential Revision: https://secure.phabricator.com/D17629
This commit is contained in:
parent
845a7d8716
commit
08a4225437
5 changed files with 391 additions and 7 deletions
|
@ -2810,6 +2810,7 @@ phutil_register_library_map(array(
|
|||
'PhabricatorFilesManagementEncodeWorkflow' => 'applications/files/management/PhabricatorFilesManagementEncodeWorkflow.php',
|
||||
'PhabricatorFilesManagementEnginesWorkflow' => 'applications/files/management/PhabricatorFilesManagementEnginesWorkflow.php',
|
||||
'PhabricatorFilesManagementGenerateKeyWorkflow' => 'applications/files/management/PhabricatorFilesManagementGenerateKeyWorkflow.php',
|
||||
'PhabricatorFilesManagementIntegrityWorkflow' => 'applications/files/management/PhabricatorFilesManagementIntegrityWorkflow.php',
|
||||
'PhabricatorFilesManagementMigrateWorkflow' => 'applications/files/management/PhabricatorFilesManagementMigrateWorkflow.php',
|
||||
'PhabricatorFilesManagementPurgeWorkflow' => 'applications/files/management/PhabricatorFilesManagementPurgeWorkflow.php',
|
||||
'PhabricatorFilesManagementRebuildWorkflow' => 'applications/files/management/PhabricatorFilesManagementRebuildWorkflow.php',
|
||||
|
@ -7941,6 +7942,7 @@ phutil_register_library_map(array(
|
|||
'PhabricatorFilesManagementEncodeWorkflow' => 'PhabricatorFilesManagementWorkflow',
|
||||
'PhabricatorFilesManagementEnginesWorkflow' => 'PhabricatorFilesManagementWorkflow',
|
||||
'PhabricatorFilesManagementGenerateKeyWorkflow' => 'PhabricatorFilesManagementWorkflow',
|
||||
'PhabricatorFilesManagementIntegrityWorkflow' => 'PhabricatorFilesManagementWorkflow',
|
||||
'PhabricatorFilesManagementMigrateWorkflow' => 'PhabricatorFilesManagementWorkflow',
|
||||
'PhabricatorFilesManagementPurgeWorkflow' => 'PhabricatorFilesManagementWorkflow',
|
||||
'PhabricatorFilesManagementRebuildWorkflow' => 'PhabricatorFilesManagementWorkflow',
|
||||
|
|
|
@ -336,7 +336,7 @@ abstract class PhabricatorFileStorageEngine extends Phobject {
|
|||
$known_integrity = $file->getIntegrityHash();
|
||||
if ($known_integrity !== null) {
|
||||
$new_integrity = $this->newIntegrityHash($formatted_data, $format);
|
||||
if ($known_integrity !== $new_integrity) {
|
||||
if (!phutil_hashes_are_identical($known_integrity, $new_integrity)) {
|
||||
throw new PhabricatorFileIntegrityException(
|
||||
pht(
|
||||
'File data integrity check failed. Dark forces have corrupted '.
|
||||
|
|
|
@ -0,0 +1,325 @@
|
|||
<?php
|
||||
|
||||
final class PhabricatorFilesManagementIntegrityWorkflow
|
||||
extends PhabricatorFilesManagementWorkflow {
|
||||
|
||||
protected function didConstruct() {
|
||||
$this
|
||||
->setName('integrity')
|
||||
->setSynopsis(pht('Verify or recalculate file integrity hashes.'))
|
||||
->setArguments(
|
||||
array(
|
||||
array(
|
||||
'name' => 'all',
|
||||
'help' => pht('Affect all files.'),
|
||||
),
|
||||
array(
|
||||
'name' => 'strip',
|
||||
'help' => pht(
|
||||
'DANGEROUS. Strip integrity hashes from files. This makes '.
|
||||
'files vulnerable to corruption or tampering.'),
|
||||
),
|
||||
array(
|
||||
'name' => 'corrupt',
|
||||
'help' => pht(
|
||||
'Corrupt integrity hashes for given files. This is intended '.
|
||||
'for debugging.'),
|
||||
),
|
||||
array(
|
||||
'name' => 'compute',
|
||||
'help' => pht(
|
||||
'Compute and update integrity hashes for files which do not '.
|
||||
'yet have them.'),
|
||||
),
|
||||
array(
|
||||
'name' => 'overwrite',
|
||||
'help' => pht(
|
||||
'DANGEROUS. Recompute and update integrity hashes, overwriting '.
|
||||
'invalid hashes. This may mark corrupt or dangerous files as '.
|
||||
'valid.'),
|
||||
),
|
||||
array(
|
||||
'name' => 'force',
|
||||
'short' => 'f',
|
||||
'help' => pht(
|
||||
'Execute dangerous operations without prompting for '.
|
||||
'confirmation.'),
|
||||
),
|
||||
array(
|
||||
'name' => 'names',
|
||||
'wildcard' => true,
|
||||
),
|
||||
));
|
||||
}
|
||||
|
||||
public function execute(PhutilArgumentParser $args) {
|
||||
$modes = array();
|
||||
|
||||
$is_strip = $args->getArg('strip');
|
||||
if ($is_strip) {
|
||||
$modes[] = 'strip';
|
||||
}
|
||||
|
||||
$is_corrupt = $args->getArg('corrupt');
|
||||
if ($is_corrupt) {
|
||||
$modes[] = 'corrupt';
|
||||
}
|
||||
|
||||
$is_compute = $args->getArg('compute');
|
||||
if ($is_compute) {
|
||||
$modes[] = 'compute';
|
||||
}
|
||||
|
||||
$is_overwrite = $args->getArg('overwrite');
|
||||
if ($is_overwrite) {
|
||||
$modes[] = 'overwrite';
|
||||
}
|
||||
|
||||
$is_verify = !$modes;
|
||||
if ($is_verify) {
|
||||
$modes[] = 'verify';
|
||||
}
|
||||
|
||||
if (count($modes) > 1) {
|
||||
throw new PhutilArgumentUsageException(
|
||||
pht(
|
||||
'You have selected multiple operation modes (%s). Choose a '.
|
||||
'single mode to operate in.',
|
||||
implode(', ', $modes)));
|
||||
}
|
||||
|
||||
$is_force = $args->getArg('force');
|
||||
if (!$is_force) {
|
||||
$prompt = null;
|
||||
if ($is_strip) {
|
||||
$prompt = pht(
|
||||
'Stripping integrity hashes is dangerous and makes files '.
|
||||
'vulnerable to corruption or tampering.');
|
||||
}
|
||||
|
||||
if ($is_corrupt) {
|
||||
$prompt = pht(
|
||||
'Corrupting integrity hashes will prevent files from being '.
|
||||
'accessed. This mode is intended only for development and '.
|
||||
'debugging.');
|
||||
}
|
||||
|
||||
if ($is_overwrite) {
|
||||
$prompt = pht(
|
||||
'Overwriting integrity hashes is dangerous and may mark files '.
|
||||
'which have been corrupted or tampered with as safe.');
|
||||
}
|
||||
|
||||
if ($prompt) {
|
||||
$this->logWarn(pht('DANGEROUS'), $prompt);
|
||||
|
||||
if (!phutil_console_confirm(pht('Continue anyway?'))) {
|
||||
throw new PhutilArgumentUsageException(pht('Aborted workflow.'));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$iterator = $this->buildIterator($args);
|
||||
if (!$iterator) {
|
||||
throw new PhutilArgumentUsageException(
|
||||
pht(
|
||||
'Either specify a list of files to affect, or use "--all" to '.
|
||||
'affect all files.'));
|
||||
}
|
||||
|
||||
$failure_count = 0;
|
||||
$total_count = 0;
|
||||
|
||||
foreach ($iterator as $file) {
|
||||
$total_count++;
|
||||
$display_name = $file->getMonogram();
|
||||
|
||||
$old_hash = $file->getIntegrityHash();
|
||||
|
||||
if ($is_strip) {
|
||||
if ($old_hash === null) {
|
||||
$this->logInfo(
|
||||
pht('SKIPPED'),
|
||||
pht(
|
||||
'File "%s" does not have an integrity hash to strip.',
|
||||
$display_name));
|
||||
} else {
|
||||
$file
|
||||
->setIntegrityHash(null)
|
||||
->save();
|
||||
|
||||
$this->logWarn(
|
||||
pht('STRIPPED'),
|
||||
pht(
|
||||
'Stripped integrity hash for "%s".',
|
||||
$display_name));
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
$need_hash = ($is_verify && $old_hash) ||
|
||||
($is_compute && ($old_hash === null)) ||
|
||||
($is_corrupt) ||
|
||||
($is_overwrite);
|
||||
if ($need_hash) {
|
||||
try {
|
||||
$new_hash = $file->newIntegrityHash();
|
||||
} catch (Exception $ex) {
|
||||
$failure_count++;
|
||||
|
||||
$this->logFail(
|
||||
pht('ERROR'),
|
||||
pht(
|
||||
'Unable to compute integrity hash for file "%s": %s',
|
||||
$display_name,
|
||||
$ex->getMessage()));
|
||||
|
||||
continue;
|
||||
}
|
||||
} else {
|
||||
$new_hash = null;
|
||||
}
|
||||
|
||||
// NOTE: When running in "corrupt" mode, we only corrupt the hash if
|
||||
// we're able to compute a valid hash. Some files, like chunked files,
|
||||
// do not support integrity hashing so corrupting them would create an
|
||||
// unusual state.
|
||||
|
||||
if ($is_corrupt) {
|
||||
if ($new_hash === null) {
|
||||
$this->logInfo(
|
||||
pht('IGNORED'),
|
||||
pht(
|
||||
'Storage for file "%s" does not support integrity hashing.',
|
||||
$display_name));
|
||||
} else {
|
||||
$file
|
||||
->setIntegrityHash('<corrupted>')
|
||||
->save();
|
||||
|
||||
$this->logWarn(
|
||||
pht('CORRUPTED'),
|
||||
pht(
|
||||
'Corrupted integrity hash for file "%s".',
|
||||
$display_name));
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($is_verify) {
|
||||
if ($old_hash === null) {
|
||||
$this->logInfo(
|
||||
pht('NONE'),
|
||||
pht(
|
||||
'File "%s" has no stored integrity hash.',
|
||||
$display_name));
|
||||
} else if ($new_hash === null) {
|
||||
$failure_count++;
|
||||
|
||||
$this->logWarn(
|
||||
pht('UNEXPECTED'),
|
||||
pht(
|
||||
'Storage for file "%s" does not support integrity hashing, '.
|
||||
'but the file has an integrity hash.',
|
||||
$display_name));
|
||||
} else if (phutil_hashes_are_identical($old_hash, $new_hash)) {
|
||||
$this->logOkay(
|
||||
pht('VALID'),
|
||||
pht(
|
||||
'File "%s" has a valid integrity hash.',
|
||||
$display_name));
|
||||
} else {
|
||||
$failure_count++;
|
||||
|
||||
$this->logFail(
|
||||
pht('MISMATCH'),
|
||||
pht(
|
||||
'File "%s" has an invalid integrity hash!',
|
||||
$display_name));
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($is_compute) {
|
||||
if ($old_hash !== null) {
|
||||
$this->logInfo(
|
||||
pht('SKIP'),
|
||||
pht(
|
||||
'File "%s" already has an integrity hash.',
|
||||
$display_name));
|
||||
} else if ($new_hash === null) {
|
||||
$this->logInfo(
|
||||
pht('IGNORED'),
|
||||
pht(
|
||||
'Storage for file "%s" does not support integrity hashing.',
|
||||
$display_name));
|
||||
} else {
|
||||
$file
|
||||
->setIntegrityHash($new_hash)
|
||||
->save();
|
||||
|
||||
$this->logOkay(
|
||||
pht('COMPUTE'),
|
||||
pht(
|
||||
'Computed and stored integrity hash for file "%s".',
|
||||
$display_name));
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($is_overwrite) {
|
||||
$same_hash = ($old_hash !== null) &&
|
||||
($new_hash !== null) &&
|
||||
phutil_hashes_are_identical($old_hash, $new_hash);
|
||||
|
||||
if ($new_hash === null) {
|
||||
$this->logInfo(
|
||||
pht('IGNORED'),
|
||||
pht(
|
||||
'Storage for file "%s" does not support integrity hashing.',
|
||||
$display_name));
|
||||
} else if ($same_hash) {
|
||||
$this->logInfo(
|
||||
pht('UNCHANGED'),
|
||||
pht(
|
||||
'File "%s" already has the correct integrity hash.',
|
||||
$display_name));
|
||||
} else {
|
||||
$file
|
||||
->setIntegrityHash($new_hash)
|
||||
->save();
|
||||
|
||||
$this->logOkay(
|
||||
pht('OVERWRITE'),
|
||||
pht(
|
||||
'Overwrote integrity hash for file "%s".',
|
||||
$display_name));
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if ($failure_count) {
|
||||
$this->logFail(
|
||||
pht('FAIL'),
|
||||
pht(
|
||||
'Processed %s file(s), encountered %s error(s).',
|
||||
new PhutilNumber($total_count),
|
||||
new PhutilNumber($failure_count)));
|
||||
} else {
|
||||
$this->logOkay(
|
||||
pht('DONE'),
|
||||
pht(
|
||||
'Processed %s file(s) with no errors.',
|
||||
new PhutilNumber($total_count)));
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
}
|
|
@ -493,9 +493,7 @@ final class PhabricatorFile extends PhabricatorFileDAO
|
|||
|
||||
$engine_class = get_class($engine);
|
||||
|
||||
$key = $this->getStorageFormat();
|
||||
$format = id(clone PhabricatorFileStorageFormat::requireFormat($key))
|
||||
->setFile($this);
|
||||
$format = $this->newStorageFormat();
|
||||
|
||||
$data_iterator = array($data);
|
||||
$formatted_iterator = $format->newWriteIterator($data_iterator);
|
||||
|
@ -756,9 +754,7 @@ final class PhabricatorFile extends PhabricatorFileDAO
|
|||
public function getFileDataIterator($begin = null, $end = null) {
|
||||
$engine = $this->instantiateStorageEngine();
|
||||
|
||||
$key = $this->getStorageFormat();
|
||||
$format = id(clone PhabricatorFileStorageFormat::requireFormat($key))
|
||||
->setFile($this);
|
||||
$format = $this->newStorageFormat();
|
||||
|
||||
$iterator = $engine->getRawFileDataIterator(
|
||||
$this,
|
||||
|
@ -1238,6 +1234,21 @@ final class PhabricatorFile extends PhabricatorFileDAO
|
|||
return idx($this->metadata, self::METADATA_INTEGRITY);
|
||||
}
|
||||
|
||||
public function newIntegrityHash() {
|
||||
$engine = $this->instantiateStorageEngine();
|
||||
|
||||
if ($engine->isChunkEngine()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$format = $this->newStorageFormat();
|
||||
|
||||
$storage_handle = $this->getStorageHandle();
|
||||
$data = $engine->readFile($storage_handle);
|
||||
|
||||
return $engine->newIntegrityHash($data, $format);
|
||||
}
|
||||
|
||||
/**
|
||||
* Write the policy edge between this file and some object.
|
||||
*
|
||||
|
@ -1406,6 +1417,16 @@ final class PhabricatorFile extends PhabricatorFileDAO
|
|||
return $this->assertAttachedKey($this->transforms, $key);
|
||||
}
|
||||
|
||||
public function newStorageFormat() {
|
||||
$key = $this->getStorageFormat();
|
||||
$template = PhabricatorFileStorageFormat::requireFormat($key);
|
||||
|
||||
$format = id(clone $template)
|
||||
->setFile($this);
|
||||
|
||||
return $format;
|
||||
}
|
||||
|
||||
|
||||
/* -( PhabricatorApplicationTransactionInterface )------------------------- */
|
||||
|
||||
|
|
|
@ -31,4 +31,40 @@ abstract class PhabricatorManagementWorkflow extends PhutilArgumentWorkflow {
|
|||
PhabricatorConsoleContentSource::SOURCECONST);
|
||||
}
|
||||
|
||||
protected function logInfo($label, $message) {
|
||||
$this->logRaw(
|
||||
tsprintf(
|
||||
"**<bg:blue> %s </bg>** %s\n",
|
||||
$label,
|
||||
$message));
|
||||
}
|
||||
|
||||
protected function logOkay($label, $message) {
|
||||
$this->logRaw(
|
||||
tsprintf(
|
||||
"**<bg:green> %s </bg>** %s\n",
|
||||
$label,
|
||||
$message));
|
||||
}
|
||||
|
||||
protected function logWarn($label, $message) {
|
||||
$this->logRaw(
|
||||
tsprintf(
|
||||
"**<bg:yellow> %s </bg>** %s\n",
|
||||
$label,
|
||||
$message));
|
||||
}
|
||||
|
||||
protected function logFail($label, $message) {
|
||||
$this->logRaw(
|
||||
tsprintf(
|
||||
"**<bg:red> %s </bg>** %s\n",
|
||||
$label,
|
||||
$message));
|
||||
}
|
||||
|
||||
private function logRaw($message) {
|
||||
fprintf(STDERR, '%s', $message);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue