mirror of
https://github.com/amadvance/snapraid.git
synced 2026-10-12 02:10:54 +00:00
3357 lines
117 KiB
C
3357 lines
117 KiB
C
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
// Copyright (C) 2011 Andrea Mazzoleni
|
|
|
|
#include "os/portable.h"
|
|
|
|
#include "support.h"
|
|
#include "util.h"
|
|
#include "elem.h"
|
|
#include "import.h"
|
|
#include "search.h"
|
|
#include "state.h"
|
|
#include "parity.h"
|
|
#include "handle.h"
|
|
#include "raid/raid.h"
|
|
#include "raid/combo.h"
|
|
|
|
/**
|
|
* SnapRAID check/fix model
|
|
*
|
|
* Check verifies the data described by the content file against the files
|
|
* currently present on the data disks. For blocks with a current hash, the
|
|
* data is verified directly by hashing it. Unless running in audit-only mode,
|
|
* parity is also read and can be used both to recover unreadable or corrupted
|
|
* data and to verify the stored parity itself.
|
|
*
|
|
* Fix uses the same verification and recovery logic as check, but writes
|
|
* validated recovered data and parity back to disk. Recovery is deliberately
|
|
* conservative: obtaining a solution from the RAID equations is not enough;
|
|
* the result must also be validated independently before it can be accepted.
|
|
*
|
|
* Block state model
|
|
* -----------------
|
|
*
|
|
* Check/fix must interpret block states differently because the content file
|
|
* may describe an incomplete sync. In that case, the logical block state and
|
|
* the physical parity on disk do not necessarily describe the same point in
|
|
* time.
|
|
*
|
|
* State Stored hash Current file Physical parity
|
|
* ------------------------------------------------------------------------
|
|
* EMPTY none none zero contribution
|
|
* BLK CURRENT present synchronized
|
|
* CHG OLD/ZERO/INVALID present may be OLD or CURRENT
|
|
* REP NEW present may be OLD or NEW
|
|
* DELETED OLD/INVALID none may be OLD or zero
|
|
* REBUILD CURRENT present parity untrusted / rebuild pending
|
|
*
|
|
* BLK has a hash for the data that should currently be present and normally
|
|
* represents a synchronized contribution.
|
|
*
|
|
* REP also has a hash for the current data, so its current contents can be
|
|
* verified. However, parity is still invalid because an interrupted sync may
|
|
* have left physical parity representing either the old or the new block.
|
|
*
|
|
* CHG is different: its stored hash describes the OLD data, not the current
|
|
* file contents. A readable CHG block therefore cannot be validated against
|
|
* block->hash. Its current contents are kept as the best available copy, but
|
|
* the old hash is useful when considering parity that may still represent the
|
|
* state before sync.
|
|
*
|
|
* DELETED has no current file data. Its stored hash, when available, describes
|
|
* the previous contribution that may still be present in physical parity.
|
|
*
|
|
* Interrupted sync recovery model
|
|
* -------------------------------
|
|
*
|
|
* When CHG, REP or DELETED blocks are present, check/fix cannot know whether
|
|
* an interrupted sync updated the physical parity before stopping. Recovery
|
|
* therefore tries two different interpretations of the same parity.
|
|
*
|
|
* The first strategy assumes that parity represents the CURRENT state:
|
|
*
|
|
* BLK -> current data is represented
|
|
* CHG -> current data is represented
|
|
* REP -> new/current data is represented
|
|
* DELETED -> contributes zero
|
|
* REBUILD -> data and hash are CURRENT, but physical parity remains untrusted
|
|
*
|
|
* Under this assumption, readable current data can be used directly and only
|
|
* blocks known to be bad or missing have to be reconstructed. BLK and REP
|
|
* recovery can be validated with their stored current hashes. CHG recovery is
|
|
* more subtle because only its OLD hash is available; a recovered CHG may
|
|
* therefore remain ambiguous and be marked out-of-date.
|
|
*
|
|
* If that strategy cannot produce a validated recovery, a second strategy
|
|
* assumes that parity still represents the state BEFORE the interrupted sync:
|
|
*
|
|
* BLK -> synchronized data is represented
|
|
* CHG -> OLD data is represented
|
|
* REP -> OLD data is represented
|
|
* DELETED -> OLD data is represented
|
|
* REBUILD -> data and hash remain CURRENT; only physical parity is untrusted
|
|
*
|
|
*
|
|
* REBUILD is orthogonal to the OLD/CURRENT sync-history distinction. Unlike
|
|
* CHG, REP and DELETED, it does not represent a different data generation.
|
|
* Its data and stored hash are CURRENT in both strategies. Only its physical
|
|
* parity is untrusted because an in-place --force-full rebuild may have been
|
|
* interrupted.
|
|
*
|
|
* Under this assumption, the current contents of CHG and REP cannot be used
|
|
* as parity inputs even when they are perfectly readable. CHG, REP and
|
|
* DELETED must instead be treated as unknown old contributions, reconstructed
|
|
* only as needed to make recovery of a bad BLK possible.
|
|
*
|
|
* These reconstructed old contributions are auxiliary recovery state, not
|
|
* current user data. They are marked out-of-date so they cannot validate the
|
|
* recovery and so an old CHG/REP result is never promoted as known-good
|
|
* current data.
|
|
*
|
|
* A CHG whose OLD hash is ZERO is a useful special case: its old contribution
|
|
* is known exactly and can be supplied as an all-zero block without consuming
|
|
* a RAID recovery equation. Similarly, if an old CHG or DELETED block can be
|
|
* obtained from the import set using its stored OLD hash, that known old
|
|
* contribution reduces the number of unknowns. REP cannot use this shortcut
|
|
* because its stored hash describes the NEW data, not the old contribution.
|
|
*
|
|
* Recovery validation model
|
|
* -------------------------
|
|
*
|
|
* Solving the RAID equations does not by itself prove that the reconstructed
|
|
* data is correct. One of the parity blocks used for reconstruction may itself
|
|
* be damaged, and after an interrupted sync different parity levels may not
|
|
* necessarily provide the expected state.
|
|
*
|
|
* For F unknown data blocks, recovery therefore requires independent evidence:
|
|
*
|
|
* - If at least one recovered block has a trustworthy current hash, F parity
|
|
* equations are sufficient. The candidate recovery is accepted only if all
|
|
* available trustworthy hashes of recovered blocks match.
|
|
*
|
|
* - If no recovered block has a trustworthy hash, F+1 parity blocks are
|
|
* required. F parity blocks are used to solve the data equations and the
|
|
* remaining parity block is reserved as an independent check of the result.
|
|
*
|
|
* When more parity levels are available, repair_step() tries their different
|
|
* combinations. Missing parity levels are skipped, and a candidate obtained
|
|
* from a bad parity combination is rejected when its hash or independent
|
|
* parity check fails.
|
|
*
|
|
* This distinction is important: a successful RAID decode is only a candidate
|
|
* recovery. It becomes an accepted recovery only after independent validation.
|
|
*
|
|
* Parity validation model
|
|
* -----------------------
|
|
*
|
|
* Physical parity is checked or rewritten only when it is both meaningful and
|
|
* expected to be valid.
|
|
*
|
|
* used_parity means that at least one live data block uses the parity position.
|
|
* Parity for a position containing no live data is irrelevant and sync is not
|
|
* required to keep such unused parity updated.
|
|
*
|
|
* valid_parity means that no block at the position is CHG, REP, DELETED or
|
|
* REBUILD.
|
|
*
|
|
* For CHG, REP and DELETED, parity is invalid because its data-generation
|
|
* relationship is uncertain after an interrupted sync. For REBUILD the reason
|
|
* is different: the data and hash are CURRENT, but physical parity itself is
|
|
* deliberately untrusted until the in-place rebuild completes.
|
|
* When one of these states is present, parity may legitimately differ from
|
|
* parity recomputed from the current data because sync has not completed.
|
|
* Such a difference must not be reported or "fixed" by check/fix; the next
|
|
* sync is responsible for bringing that parity position to the current state.
|
|
*
|
|
* Parity is therefore compared and repaired only when:
|
|
*
|
|
* used_parity && valid_parity
|
|
*
|
|
* A parity level excluded from fix is not necessarily useless. When accessible
|
|
* it is still opened for reading because it can provide an additional recovery
|
|
* equation or independent validation, while remaining protected from writes.
|
|
*
|
|
* Fix and quarantine model
|
|
* ------------------------
|
|
*
|
|
* failed_struct distinguishes two properties of a recovery entry.
|
|
*
|
|
* is_bad means that the current file block is missing, unreadable or does not
|
|
* match its expected current hash and therefore needs recovery.
|
|
*
|
|
* recovery tracks the data state of the block in buffer[index]:
|
|
*
|
|
* RECOVERY_NONE not recovered (no valid replacement data available)
|
|
* RECOVERY_CURRENT certified CURRENT data (via import/search or RAID)
|
|
* RECOVERY_OUTOFDATE recovered candidate that cannot be certified CURRENT
|
|
*
|
|
* Outside repair(), file_recover() writes RECOVERY_CURRENT data to the original
|
|
* file and reports it as fixed. An out-of-date candidate (RECOVERY_OUTOFDATE)
|
|
* is written as best-effort data, but the containing file remains marked damaged
|
|
* and is quarantined under the .unrecoverable name.
|
|
*
|
|
* Missing files are created under their normal names. They are quarantined only
|
|
* if recovery produces data that cannot be certified as CURRENT.
|
|
*
|
|
* A later fix starts a new recovery attempt under the normal filename even if a
|
|
* previous .unrecoverable file exists. A previous quarantine cannot safely be
|
|
* reused as recovery input because it may contain an ambiguous recovered CHG.
|
|
* Such a CHG may have caused the file to be quarantined precisely because its
|
|
* CURRENT generation could not be proven. That provenance is runtime state and
|
|
* is not persisted in the .unrecoverable file. Reopening it in a later fix would
|
|
* make the CHG merely appear readable and could incorrectly accept it as CURRENT.
|
|
*
|
|
* The previous quarantine is therefore kept separate while the new recovery is
|
|
* in progress. If the new attempt succeeds completely, the obsolete quarantine
|
|
* is removed. If the new attempt also becomes unrecoverable, it replaces the
|
|
* previous quarantine.
|
|
*
|
|
* A quarantined file may be reopened later during the same fix run. In that case
|
|
* FILE_IS_QUARANTINED identifies it as belonging to the current failed recovery and
|
|
* handle_open_create() continues operating on its .unrecoverable file.
|
|
*
|
|
* A partial -S/-B fix operates only at block level and therefore never performs
|
|
* file-level finalization.
|
|
*/
|
|
|
|
/****************************************************************************/
|
|
/* check */
|
|
|
|
static const char* es(int err)
|
|
{
|
|
if (is_hw(err))
|
|
return "error_io";
|
|
else
|
|
return "error";
|
|
}
|
|
|
|
#define RECOVERY_NONE 0 /**< Not recovered (no valid candidate available) */
|
|
#define RECOVERY_CURRENT 1 /**< Recovered and certified CURRENT (via import/search or RAID) */
|
|
#define RECOVERY_OUTOFDATE 2 /**< Recovered candidate that cannot be certified CURRENT (best-effort / OLD / ambiguous) */
|
|
|
|
/**
|
|
* A block participating in recovery.
|
|
*
|
|
* Entries include blocks that are actually missing or corrupted, but also
|
|
* readable CHG, REP and DELETED blocks whose old/current contribution may be
|
|
* needed to interpret parity left by an interrupted sync.
|
|
*/
|
|
struct failed_struct {
|
|
/**
|
|
* If the current file block is known to require recovery because it is
|
|
* missing, unreadable, or doesn't match its expected current hash.
|
|
*
|
|
* DELETED blocks and readable CHG/REP auxiliary entries are not bad even
|
|
* though they may participate in the recovery equations.
|
|
*
|
|
* is_bad describes the state of the original file block. It may remain set
|
|
* even when buffer[index] already contains valid replacement data.
|
|
*/
|
|
int is_bad;
|
|
|
|
/**
|
|
* Data state of the buffer associated with this entry:
|
|
*
|
|
* RECOVERY_NONE:
|
|
* The block is not recovered or does not contain valid replacement data.
|
|
*
|
|
* RECOVERY_CURRENT:
|
|
* buffer[index] contains certified CURRENT data.
|
|
* During repair() strategy construction, this marks blocks obtained
|
|
* and validated independently through import/search, making them reusable
|
|
* as known inputs across strategies whose data generation is unchanged
|
|
* (such as BLK and REBUILD).
|
|
* Upon successful recovery, repair() marks all recovered non-outofdate blocks
|
|
* with RECOVERY_CURRENT.
|
|
* Outside repair(), file_recover() writes this data to the original file
|
|
* and marks it FIXED.
|
|
*
|
|
* RECOVERY_OUTOFDATE:
|
|
* buffer[index] contains a recovered candidate that cannot be proven to
|
|
* represent the required CURRENT contents (e.g. reconstructed under the
|
|
* pre-sync parity strategy, or ambiguous CHG).
|
|
* If an out-of-date block is also is_bad, fix may still preserve the recovered
|
|
* bytes as the best available copy, but the containing file remains damaged
|
|
* and quarantined as .unrecoverable.
|
|
*/
|
|
int recovery;
|
|
|
|
unsigned index; /**< Index of the failed block. */
|
|
struct snapraid_block* block; /**< The failed block */
|
|
struct snapraid_disk* disk; /**< The failed disk. */
|
|
struct snapraid_file* file; /**< The failed file. 0 for DELETED block. */
|
|
block_off_t file_pos; /**< Offset inside the file */
|
|
struct snapraid_handle* handle; /**< The handle containing the failed block, or 0 for a DELETED block */
|
|
};
|
|
|
|
/**
|
|
* Check if a block hash matches the specified buffer.
|
|
*
|
|
* This function validates hash identity only and deliberately omits the
|
|
* zero-padding check beyond the logical file size. It is used when testing
|
|
* candidate identity against a stored hash whose original logical size and
|
|
* padding provenance may differ from the CURRENT file metadata (such as
|
|
* during CHG generation inference).
|
|
*
|
|
* Return ==0 if equal, -1 if mismatch.
|
|
*/
|
|
static int block_hash_cmp(struct snapraid_state* state, int rehash, struct snapraid_block* block, size_t pos_size, unsigned char* buffer)
|
|
{
|
|
unsigned char hash[HASH_MAX];
|
|
|
|
/* compute the hash according to the block-hash semantics */
|
|
if (rehash) {
|
|
memhash_block(state->prevhash, state->prevhashseed, hash, buffer, pos_size, state->block_size);
|
|
} else {
|
|
memhash_block(state->hash, state->hashseed, hash, buffer, pos_size, state->block_size);
|
|
}
|
|
|
|
/* compare the hash */
|
|
if (memcmp(hash, block->hash, BLOCK_HASH_SIZE) != 0) {
|
|
return -1;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
/**
|
|
* Check if block data matches the specified block and is canonical for the current logical size.
|
|
*
|
|
* Unlike block_hash_cmp(), this function validates both hash identity and the
|
|
* canonical zero-padding requirement beyond the CURRENT logical file size.
|
|
* For legacy hashes (Murmur3/Spooky2), the hash covers only logical bytes, so
|
|
* trailing garbage or corrupted parity could otherwise pass validation and later
|
|
* corrupt parity recalculations. For MuseAir, this enforces the canonical RAID
|
|
* block invariant.
|
|
*
|
|
* Use this for all normal CURRENT data validation (e.g. is_hash_matching()).
|
|
*
|
|
* Return ==0 if equal and valid, -1 if mismatch.
|
|
*/
|
|
static int block_data_cmp(struct snapraid_state* state, int rehash, struct snapraid_block* block, size_t pos_size, unsigned char* buffer)
|
|
{
|
|
if (block_hash_cmp(state, rehash, block, pos_size, buffer) != 0) {
|
|
return -1;
|
|
}
|
|
|
|
/*
|
|
* Legacy hashes cover only the logical bytes belonging to the file, while RAID
|
|
* operates on the complete zero-padded block. Verify the padding separately:
|
|
* otherwise corruption confined to the reconstructed tail could pass a legacy
|
|
* hash check and later produce incorrect parity. This is redundant for MuseAir,
|
|
* but also makes the canonical-block invariant explicit.
|
|
*/
|
|
if (pos_size < state->block_size) {
|
|
if (membcmp(buffer + pos_size, 0, state->block_size - pos_size) != 0) {
|
|
return -1;
|
|
}
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
/**
|
|
* Check if the specified block has a trustworthy CURRENT hash under the given strategy.
|
|
*
|
|
* In the OLD recovery strategy, physical parity reflects the pre-sync state,
|
|
* so RAID reconstructs the OLD content of this block. However, for a REP
|
|
* block, block->hash describes the NEW (post-replace) data. Comparing the
|
|
* reconstructed OLD bytes against the NEW hash would falsely fail validation.
|
|
*/
|
|
static int block_has_recovery_hash(const struct snapraid_block* block, int is_old_strategy)
|
|
{
|
|
unsigned state = block_state_get(block);
|
|
|
|
if (!block_has_updated_hash(block))
|
|
return 0;
|
|
|
|
if (is_old_strategy && state == BLOCK_STATE_REP)
|
|
return 0;
|
|
|
|
return 1;
|
|
}
|
|
|
|
/**
|
|
* Check if the hash of all the failed blocks we are expecting to recover are now matching.
|
|
*/
|
|
static int is_hash_matching(struct snapraid_state* state, int rehash, unsigned diskmax, struct failed_struct* failed, unsigned* failed_map, unsigned failed_count, void** buffer, int is_old_strategy)
|
|
{
|
|
unsigned j;
|
|
int hash_checked;
|
|
|
|
hash_checked = 0; /* keep track if we check at least one block */
|
|
|
|
/* check if the recovered blocks are OK */
|
|
for (j = 0; j < failed_count; ++j) {
|
|
/* if the block has a hash to check in this strategy */
|
|
if (block_has_recovery_hash(failed[failed_map[j]].block, is_old_strategy)) {
|
|
/* if a hash doesn't match, fail the check */
|
|
size_t pos_size = file_block_size(failed[failed_map[j]].file, failed[failed_map[j]].file_pos, state->block_size);
|
|
if (block_data_cmp(state, rehash, failed[failed_map[j]].block, pos_size, buffer[failed[failed_map[j]].index]) != 0) {
|
|
log_tag("repair_hash_error:%u: Hash mismatch\n", failed_map[j]);
|
|
return 0;
|
|
}
|
|
|
|
hash_checked = 1;
|
|
}
|
|
}
|
|
|
|
/*
|
|
* If nothing checked, we reject it
|
|
* note that we are excluding this case at upper level
|
|
* but checking again doesn't hurt
|
|
*/
|
|
if (!hash_checked) {
|
|
/* LCOV_EXCL_START */
|
|
return 0;
|
|
/* LCOV_EXCL_STOP */
|
|
}
|
|
|
|
/*
|
|
* If we checked something, and no block failed the check
|
|
* recompute all the redundancy information
|
|
*/
|
|
raid_gen(diskmax, state->level, state->block_size, buffer, 0);
|
|
return 1;
|
|
}
|
|
|
|
/**
|
|
* Check if specified parity is now matching with a recomputed one.
|
|
*/
|
|
static int is_parity_matching(struct snapraid_state* state, unsigned diskmax, unsigned i, void** buffer, void** buffer_recov)
|
|
{
|
|
/* recompute parity, note that we don't need parity over i */
|
|
raid_gen(diskmax, i + 1, state->block_size, buffer, 0);
|
|
|
|
/* if the recovered parity block matches */
|
|
if (memcmp(buffer[diskmax + i], buffer_recov[i], state->block_size) == 0) {
|
|
/* recompute all the redundancy information */
|
|
raid_gen(diskmax, state->level, state->block_size, buffer, 0);
|
|
return 1;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
/**
|
|
* Try to recover the selected failed inputs and validate the reconstructed
|
|
* result using either a CURRENT hash or an additional RAID equation not used
|
|
* for reconstruction.
|
|
*
|
|
* Return:
|
|
* 0 if a recovery was successfully validated;
|
|
* >0 if one or more recovery candidates were tried but all failed validation;
|
|
* <0 if no usable recovery/validation strategy exists.
|
|
*
|
|
* On success all parity levels are recomputed in buffer.
|
|
*/
|
|
static int repair_step(struct snapraid_state* state, int rehash, block_off_t pos, unsigned diskmax, struct failed_struct* failed, unsigned* failed_map, unsigned failed_count, void** buffer, void** buffer_recov, int is_old_strategy)
|
|
{
|
|
unsigned i, n;
|
|
int error;
|
|
int has_hash;
|
|
int id[LEV_MAX];
|
|
int ip[LEV_MAX];
|
|
|
|
/* no fix required, already checked at higher level, but just to be sure */
|
|
if (failed_count == 0) {
|
|
/* LCOV_EXCL_START */
|
|
/* recompute only the parity */
|
|
raid_gen(diskmax, state->level, state->block_size, buffer, 0);
|
|
return 0;
|
|
/* LCOV_EXCL_STOP */
|
|
}
|
|
|
|
n = state->level;
|
|
error = 0;
|
|
|
|
/* if failures exceed parity level, recovery is impossible */
|
|
if (failed_count > n) {
|
|
log_tag("recover_strategy_error:%" PRIu64 ": Impossible to recover from %u failures with %u parity\n",
|
|
pos, failed_count, n);
|
|
return -1;
|
|
}
|
|
|
|
/* setup vector of failed disk indexes */
|
|
for (i = 0; i < failed_count; ++i)
|
|
id[i] = failed[failed_map[i]].index;
|
|
|
|
/*
|
|
* RAID decoding alone cannot tell whether the parity equations used for the
|
|
* recovery were themselves correct.
|
|
*
|
|
* If at least one failed input has a trustworthy CURRENT hash, recovered data
|
|
* can provide independent validation. Otherwise one additional parity equation
|
|
* is reserved exclusively for checking the candidate result.
|
|
*
|
|
* Which block states provide a trustworthy hash depends on the strategy:
|
|
*
|
|
* - CURRENT strategy (is_old_strategy == 0): BLK, REP, and REBUILD have
|
|
* trustworthy CURRENT hashes. CHG blocks do not (their stored hash describes
|
|
* pre-sync OLD data).
|
|
*
|
|
* - OLD strategy (is_old_strategy == 1): only BLK and REBUILD provide trustworthy
|
|
* hashes (BLK represents synchronized data covered by pre-sync parity; REBUILD
|
|
* data is always CURRENT). REP is excluded because its stored hash describes
|
|
* NEW data while RAID reconstructs its pre-sync OLD contribution. CHG and DELETED
|
|
* have no updated hash.
|
|
*
|
|
* The additional parity is an independent RAID equation, not an independent
|
|
* hash of the recovered data. The candidate is accepted only if the complete
|
|
* reconstructed block set satisfies this extra equation for the whole block.
|
|
* Therefore an accidental false match requires the data differences to satisfy
|
|
* the corresponding RAID relation at every byte position. Such correlated
|
|
* differences are possible in principle because RAID equations are linear, but
|
|
* are extremely unlikely for unrelated data corruption.
|
|
*/
|
|
has_hash = 0;
|
|
for (i = 0; i < failed_count; ++i) {
|
|
if (block_has_recovery_hash(failed[failed_map[i]].block, is_old_strategy))
|
|
has_hash = 1;
|
|
}
|
|
|
|
/*
|
|
* If we don't have a hash, but we have an extra parity
|
|
* (strictly-less failures than number of parities)
|
|
*/
|
|
if (!has_hash && failed_count < n) {
|
|
/* number of parity to use, one more to check the recovering */
|
|
unsigned r = failed_count + 1;
|
|
|
|
/* all combinations (r of n) parities */
|
|
combination_first(r, n, ip);
|
|
do {
|
|
/* if a parity is missing, do nothing */
|
|
for (i = 0; i < r; ++i) {
|
|
if (buffer_recov[ip[i]] == 0)
|
|
break;
|
|
}
|
|
if (i != r)
|
|
continue;
|
|
|
|
/* copy the parities to use, one less because the last is used for checking */
|
|
for (i = 0; i < r - 1; ++i)
|
|
memcpy(buffer[diskmax + ip[i]], buffer_recov[ip[i]], state->block_size);
|
|
|
|
/* recover using one less parity, the ip[r-1] one */
|
|
raid_data(r - 1, id, ip, diskmax, state->block_size, buffer);
|
|
|
|
/* use the remaining ip[r-1] parity to check the result */
|
|
if (is_parity_matching(state, diskmax, ip[r - 1], buffer, buffer_recov))
|
|
return 0;
|
|
|
|
/* log */
|
|
log_tag("recover_parity_error:%" PRIu64 ":", pos);
|
|
for (i = 0; i < r; ++i) {
|
|
if (i != 0)
|
|
log_tag(",");
|
|
log_tag("%s", lev_config_name(ip[i]));
|
|
}
|
|
log_tag(": Parity mismatch\n");
|
|
++error;
|
|
} while (combination_next(r, n, ip));
|
|
}
|
|
|
|
/*
|
|
* If we have a hash, and enough parities
|
|
* (less-or-equal failures than number of parities)
|
|
*/
|
|
if (has_hash && failed_count <= n) {
|
|
/* number of parities to use equal to the number of failures */
|
|
unsigned r = failed_count;
|
|
|
|
/* all combinations (r of n) parities */
|
|
combination_first(r, n, ip);
|
|
do {
|
|
/* if a parity is missing, do nothing */
|
|
for (i = 0; i < r; ++i) {
|
|
if (buffer_recov[ip[i]] == 0)
|
|
break;
|
|
}
|
|
if (i != r)
|
|
continue;
|
|
|
|
/* copy the parities to use */
|
|
for (i = 0; i < r; ++i)
|
|
memcpy(buffer[diskmax + ip[i]], buffer_recov[ip[i]], state->block_size);
|
|
|
|
/* recover */
|
|
raid_data(r, id, ip, diskmax, state->block_size, buffer);
|
|
|
|
/* use the hash to check the result */
|
|
if (is_hash_matching(state, rehash, diskmax, failed, failed_map, failed_count, buffer, is_old_strategy))
|
|
return 0;
|
|
|
|
/* log */
|
|
log_tag("recover_hash_error:%" PRIu64 ":", pos);
|
|
for (i = 0; i < r; ++i) {
|
|
if (i != 0)
|
|
log_tag("/");
|
|
log_tag("%s", lev_config_name(ip[i]));
|
|
}
|
|
log_tag(": Hash mismatch\n");
|
|
++error;
|
|
} while (combination_next(r, n, ip));
|
|
}
|
|
|
|
/* return the number of failed attempts, or -1 if no strategy */
|
|
if (error)
|
|
return error;
|
|
|
|
log_tag("recover_strategy_error:%" PRIu64 ": No strategy to recover from %u failures with %u parity %s hash\n",
|
|
pos, failed_count, n, has_hash ? "with" : "without");
|
|
return -1;
|
|
}
|
|
|
|
/**
|
|
* Try to recover all the bad blocks of a parity stripe.
|
|
*
|
|
* Recovery after an interrupted sync is ambiguous because physical parity may
|
|
* represent either the CURRENT filesystem state or the OLD state that existed
|
|
* before the interrupted sync. For this reason two recovery strategies are
|
|
* attempted.
|
|
*
|
|
* This recovery logic relies on the following invariant:
|
|
*
|
|
* After an interrupted sync, the data-disk filesystem must not be changed
|
|
* before running recovery or before a subsequent sync completes successfully.
|
|
*
|
|
* Re-running sync without changing the filesystem is allowed, even if that sync
|
|
* is interrupted again. Under this invariant an unsynchronized CHG has only two
|
|
* relevant generations:
|
|
*
|
|
* OLD identified by the stored CHG hash;
|
|
* CURRENT represented by the unchanged live filesystem contents.
|
|
*
|
|
* Physical parity may contain either generation because the interrupted sync
|
|
* may or may not have updated it, but there cannot be a third intermediate data
|
|
* generation created by a later filesystem change.
|
|
*
|
|
* This invariant is essential for CHG generation inference. If the filesystem
|
|
* is changed after an interrupted sync, for example by adding, deleting,
|
|
* replacing or modifying files, a stored CHG/DELETED OLD hash may no longer
|
|
* identify the generation actually represented by physical parity. In that
|
|
* situation candidate != stored OLD would not be sufficient to prove CURRENT.
|
|
*
|
|
* 1. CURRENT strategy.
|
|
*
|
|
* Assume that parity was already updated by the interrupted sync.
|
|
* Readable blocks are therefore used as their CURRENT values and only bad
|
|
* blocks are reconstructed.
|
|
*
|
|
* BLK, REP and REBUILD have hashes identifying their required CURRENT
|
|
* contents and can therefore be independently validated.
|
|
*
|
|
* CHG is different: its stored hash identifies the OLD contents, not the
|
|
* CURRENT ones. After repair_step() has independently validated the RAID
|
|
* solution, the recovered CHGs are examined as a group.
|
|
*
|
|
* Under the recovery invariant above, a recovered CHG candidate different
|
|
* from its stored OLD contents cannot belong to the OLD generation and
|
|
* therefore proves that the validated RAID solution is CURRENT.
|
|
*
|
|
* All CHGs reconstructed by the same repair_step() belong to that same
|
|
* validated RAID solution. Consequently, if at least one recovered CHG
|
|
* proves that the solution is CURRENT, all CHGs reconstructed by that
|
|
* solution are CURRENT, including CHGs whose OLD hash is INVALID or whose
|
|
* OLD and CURRENT contents happen to be identical.
|
|
*
|
|
* If no recovered CHG distinguishes the solution from OLD, all recovered
|
|
* CHGs remain RECOVERY_OUTOFDATE. This does not mean that their contents are
|
|
* necessarily wrong or actually old, only that CURRENT cannot be proven.
|
|
*
|
|
* 2. OLD strategy.
|
|
*
|
|
* If the CURRENT strategy fails, assume that physical parity still
|
|
* represents the state before the interrupted sync.
|
|
*
|
|
* CHG, REP and DELETED entries are therefore converted to their OLD
|
|
* contribution when possible, or reconstructed as auxiliary RAID unknowns.
|
|
* This strategy is useful only when there is bad BLK/REBUILD data to
|
|
* recover. Any bad CHG/REP reconstructed under this strategy remains
|
|
* RECOVERY_OUTOFDATE because it cannot be certified as the required CURRENT data.
|
|
*
|
|
* REBUILD is orthogonal to the OLD/CURRENT sync-history distinction. Its data
|
|
* and stored hash are always CURRENT, while its physical parity is untrusted
|
|
* because an in-place --force-full rebuild may have been interrupted. REBUILD
|
|
* therefore never requires conversion to an OLD data contribution and does not
|
|
* participate in the CHG generation inference. A matching REBUILD hash proves
|
|
* the recovered REBUILD data, but does not prove whether CHG/REP/DELETED parity
|
|
* belongs to the OLD or CURRENT sync generation.
|
|
*
|
|
* repair_step() is responsible only for solving and independently validating
|
|
* the RAID equations. This function is responsible for interpreting the
|
|
* resulting solution according to the OLD/CURRENT history of unsynchronized
|
|
* blocks and the recovery invariant described above.
|
|
*
|
|
* Parameters:
|
|
*
|
|
* state SnapRAID state and configuration.
|
|
*
|
|
* rehash If non-zero, hashes must be computed using the previous hash
|
|
* algorithm/seed while processing a rehash operation.
|
|
*
|
|
* pos Parity block position being recovered. Used for logging.
|
|
*
|
|
* diskmax Number of data disks participating in the RAID stripe.
|
|
*
|
|
* failed Array describing all blocks relevant to recovery. Entries may
|
|
* represent actually bad blocks, but also readable CHG/REP or
|
|
* DELETED blocks needed by the OLD recovery strategy.
|
|
*
|
|
* failed_map Temporary array used to select which failed[] entries are RAID
|
|
* unknowns for each call to repair_step().
|
|
*
|
|
* failed_count Number of valid entries in failed[].
|
|
*
|
|
* buffer Data and generated parity buffers. Data buffers are indexed by
|
|
* disk, followed by generated parity buffers.
|
|
*
|
|
* buffer_recov Physical parity blocks read from disk. A NULL entry means that
|
|
* the corresponding parity level is unavailable or cannot be
|
|
* trusted for recovery.
|
|
*
|
|
* buffer_scratch Scratch buffers used to hold OLD RAID unknowns without
|
|
* destroying certified CURRENT data.
|
|
*
|
|
* Return value:
|
|
*
|
|
* 0 A RAID solution was successfully validated under one of the two history
|
|
* hypotheses. Some originally bad CHG/REP entries may still be marked
|
|
* RECOVERY_OUTOFDATE, meaning that their required CURRENT contents could not
|
|
* be proven. The caller must inspect the recovery status before considering such
|
|
* blocks successfully recovered.
|
|
*
|
|
* >0 One or more recovery candidates were attempted but failed independent
|
|
* validation. The value is the accumulated number of failed attempts.
|
|
*
|
|
* <0 No usable recovery strategy could be attempted successfully, for example
|
|
* because there were too many failures or insufficient usable parity.
|
|
*
|
|
* On successful return buffer[] contains the recovered data and parity
|
|
* recomputed from that data.
|
|
*/
|
|
static int repair(struct snapraid_state* state, int rehash, block_off_t pos, unsigned diskmax, struct failed_struct* failed, unsigned* failed_map, unsigned failed_count, void** buffer, void** buffer_recov, void** buffer_scratch)
|
|
{
|
|
int ret;
|
|
int error;
|
|
unsigned j, k;
|
|
unsigned n;
|
|
int something_to_recover;
|
|
int something_unsynced;
|
|
int too_many_unknowns;
|
|
struct {
|
|
unsigned j;
|
|
void* buffer;
|
|
} rep_saved[LEV_MAX];
|
|
unsigned scratch_count;
|
|
|
|
error = 0;
|
|
|
|
for (j = 0; j < failed_count; ++j)
|
|
failed[j].recovery = RECOVERY_NONE;
|
|
|
|
/*
|
|
* Nothing is missing or damaged.
|
|
*
|
|
* repair() is also responsible for leaving buffer[] with parity
|
|
* recomputed from the data buffers, so do that even if there is no
|
|
* actual recovery to perform.
|
|
*/
|
|
if (failed_count == 0) {
|
|
raid_gen(diskmax, state->level, state->block_size, buffer, 0);
|
|
return 0;
|
|
}
|
|
|
|
/*
|
|
* Log all entries participating in recovery.
|
|
*
|
|
* failed[] contains more than just bad blocks. Readable CHG/REP and
|
|
* DELETED entries may also be present because their OLD contribution can
|
|
* be needed by the pre-sync recovery strategy below.
|
|
*/
|
|
for (j = 0; j < failed_count; ++j) {
|
|
const char* desc;
|
|
const char* hash;
|
|
const char* data;
|
|
struct snapraid_block* block = failed[j].block;
|
|
unsigned block_state = block_state_get(block);
|
|
|
|
switch (block_state) {
|
|
case BLOCK_STATE_DELETED : desc = "delete"; break;
|
|
case BLOCK_STATE_CHG : desc = "change"; break;
|
|
case BLOCK_STATE_REP : desc = "replace"; break;
|
|
case BLOCK_STATE_BLK : desc = "block"; break;
|
|
case BLOCK_STATE_REBUILD : desc = "rebuild"; break;
|
|
/* LCOV_EXCL_START */
|
|
default : desc = "unknown"; break;
|
|
/* LCOV_EXCL_STOP */
|
|
}
|
|
|
|
if (block == BLOCK_NULL) {
|
|
hash = "none";
|
|
} else if (hash_is_invalid(block->hash)) {
|
|
hash = "lost";
|
|
} else if (hash_is_zero(block->hash)) {
|
|
hash = "zero";
|
|
} else {
|
|
hash = "known";
|
|
}
|
|
|
|
if (failed[j].is_bad)
|
|
data = "bad";
|
|
else
|
|
data = "good";
|
|
|
|
if (failed[j].file) {
|
|
struct snapraid_disk* disk = failed[j].disk;
|
|
struct snapraid_file* file = failed[j].file;
|
|
block_off_t file_pos = failed[j].file_pos;
|
|
|
|
log_tag("repair_entry:%u:%s:%s:%s:%s:%s:%" PRIu64 ":\n", j, desc, hash, data, disk->name, esc_tag(file->sub), file_pos);
|
|
} else {
|
|
log_tag("repair_entry:%u:%s:%s:%s:\n", j, desc, hash, data);
|
|
}
|
|
}
|
|
|
|
/*
|
|
* Under the recovery invariant documented above, an interrupted sync leaves
|
|
* exactly two possible data generations for unsynchronized blocks:
|
|
*
|
|
* OLD the state identified by the pre-sync metadata;
|
|
* CURRENT the unchanged live filesystem state.
|
|
*
|
|
* The content file tells us which blocks were not fully synchronized, but it
|
|
* cannot tell us whether physical parity had already advanced from OLD to
|
|
* CURRENT before the interruption.
|
|
*
|
|
* We therefore try the only two possible history hypotheses:
|
|
*
|
|
* 1. parity represents CURRENT data;
|
|
* 2. parity still represents OLD data.
|
|
*
|
|
* Repeated interrupted sync attempts do not add another generation as long as
|
|
* the filesystem remains unchanged between them.
|
|
*
|
|
* REBUILD does not introduce another data-generation hypothesis. Its data is
|
|
* CURRENT in both cases; only the physical parity associated with it is
|
|
* independently untrusted.
|
|
*
|
|
* If sync completed normally there are no CHG/REP/DELETED states and the
|
|
* distinction disappears.
|
|
*/
|
|
|
|
/*
|
|
* First strategy: parity represents CURRENT data.
|
|
*
|
|
* Under the CURRENT hypothesis:
|
|
*
|
|
* BLK hash CURRENT, parity normally synchronized
|
|
* REP hash CURRENT, parity may represent OLD or CURRENT
|
|
* REBUILD hash CURRENT, data CURRENT, physical parity untrusted
|
|
* CHG hash OLD, parity may represent OLD or CURRENT
|
|
* DELETED no CURRENT data, parity may represent OLD or zero
|
|
*
|
|
* Readable blocks already contain their CURRENT filesystem contents, so
|
|
* only bad blocks have to be reconstructed.
|
|
*
|
|
* BLK/REP/REBUILD can first be fetched from import/search because their
|
|
* stored hash identifies exactly the contents required by this strategy.
|
|
*
|
|
* CHG cannot use this shortcut because fetching its stored hash would
|
|
* retrieve OLD contents.
|
|
*/
|
|
n = 0;
|
|
something_to_recover = 0;
|
|
|
|
for (j = 0; j < failed_count; ++j) {
|
|
if (failed[j].is_bad) {
|
|
unsigned block_state = block_state_get(failed[j].block);
|
|
|
|
assert(block_state != BLOCK_STATE_DELETED);
|
|
|
|
if ((block_state == BLOCK_STATE_BLK || block_state == BLOCK_STATE_REP || block_state == BLOCK_STATE_REBUILD)
|
|
&& (state_import_fetch(state, rehash, failed[j].block, buffer[failed[j].index]) == 0
|
|
|| state_search_fetch(state, rehash, failed[j].file, failed[j].file_pos, failed[j].block, buffer[failed[j].index]) == 0)
|
|
) {
|
|
/*
|
|
* The required CURRENT contents were obtained directly
|
|
* using their CURRENT hash, so this block no longer
|
|
* consumes a RAID recovery equation.
|
|
*
|
|
* Keep is_bad set because the file block still needs to be
|
|
* written back. RECOVERY_CURRENT records that buffer[index] already
|
|
* contains certified CURRENT data.
|
|
*/
|
|
failed[j].recovery = RECOVERY_CURRENT;
|
|
log_tag("repair_hash_import:%u: Fixed by import\n", j);
|
|
} else {
|
|
/*
|
|
* Otherwise reconstruct the block from parity.
|
|
*/
|
|
failed_map[n] = j;
|
|
++n;
|
|
something_to_recover = 1;
|
|
}
|
|
}
|
|
}
|
|
|
|
/*
|
|
* All originally bad blocks were recovered through import/search.
|
|
*/
|
|
if (!something_to_recover) {
|
|
log_tag("recover_sync:%" PRIu64 ":%u: Skipped for already recovered\n", pos, n);
|
|
|
|
raid_gen(diskmax, state->level, state->block_size, buffer, 0);
|
|
return 0;
|
|
}
|
|
|
|
/*
|
|
* Recover the selected bad blocks and independently validate the RAID
|
|
* solution.
|
|
*
|
|
* repair_step() validates the reconstructed vector either using one or
|
|
* more trustworthy CURRENT hashes, or, when no such hash exists, using
|
|
* one additional independent parity equation.
|
|
*
|
|
* BLK, REP and REBUILD have CURRENT hashes and can therefore validate
|
|
* their own recovered data directly. In particular, a REBUILD hash is
|
|
* trustworthy even though the physical parity used to obtain the candidate
|
|
* is not.
|
|
*
|
|
* Recovered CHGs require an additional history interpretation because
|
|
* they have no stored CURRENT hash. Under the recovery invariant there are
|
|
* only OLD and CURRENT generations, allowing their generation to be inferred
|
|
* after the RAID solution itself has been validated.
|
|
*/
|
|
ret = repair_step(state, rehash, pos, diskmax, failed, failed_map, n, buffer, buffer_recov, 0);
|
|
|
|
if (ret == 0) {
|
|
int has_recovered_chg;
|
|
int current_generation_proven;
|
|
|
|
has_recovered_chg = 0;
|
|
current_generation_proven = 0;
|
|
|
|
/*
|
|
* Determine the generation of all CHGs reconstructed by this RAID
|
|
* solution as a group.
|
|
*
|
|
* This inference relies on the recovery invariant documented by repair():
|
|
* the filesystem has not changed since the interrupted sync. Therefore the
|
|
* only possible CHG data generations are OLD and CURRENT, and the stored CHG
|
|
* hash identifies OLD.
|
|
*
|
|
* Under this invariant:
|
|
*
|
|
* candidate != OLD
|
|
*
|
|
* proves that this particular candidate cannot belong to the OLD generation.
|
|
* Since no third generation is possible, it therefore proves CURRENT.
|
|
*
|
|
* This would NOT be valid if the filesystem had been changed after the
|
|
* interrupted sync. For example, deleting and later reallocating a parity
|
|
* position may preserve a historical CHG/DELETED hash while physical parity
|
|
* represents another generation. In such a history candidate != stored hash
|
|
* would only prove that the candidate differs from that historical hash, not
|
|
* that it is CURRENT.
|
|
*
|
|
* All CHGs reconstructed by this repair_step() belong to the same validated
|
|
* RAID solution. Consequently, once one recovered CHG proves that the
|
|
* solution is CURRENT, that generation classification applies to every CHG
|
|
* reconstructed by the same solution.
|
|
*
|
|
* This is intentionally a solution-level decision rather than a block-level
|
|
* one.
|
|
*
|
|
* For example:
|
|
*
|
|
* CHG A has an INVALID OLD hash;
|
|
* CHG B has candidate != OLD.
|
|
*
|
|
* CHG B identifies the validated RAID solution as CURRENT, so CHG A is also
|
|
* CURRENT despite being unable to make that determination by itself.
|
|
*
|
|
* Similarly:
|
|
*
|
|
* CHG A candidate == OLD;
|
|
* CHG B candidate != OLD.
|
|
*
|
|
* CHG A may simply have identical OLD and CURRENT bytes. Matching OLD is
|
|
* therefore not evidence that CHG A is out-of-date once another CHG has
|
|
* identified the whole solution as CURRENT.
|
|
*
|
|
* REBUILD does not participate in this generation inference. Its recovered
|
|
* contents may be independently proven CURRENT by its hash, but this only
|
|
* validates REBUILD itself and says nothing about whether CHG parity belongs
|
|
* to the OLD or CURRENT sync generation.
|
|
*
|
|
* If no recovered CHG distinguishes the solution from OLD, the RAID solution
|
|
* remains valid but its generation is ambiguous. In that case all recovered
|
|
* CHGs are marked RECOVERY_OUTOFDATE together.
|
|
*/
|
|
for (j = 0; j < failed_count; ++j) {
|
|
unsigned block_state;
|
|
|
|
if (!failed[j].is_bad)
|
|
continue;
|
|
|
|
block_state = block_state_get(failed[j].block);
|
|
|
|
if (block_state != BLOCK_STATE_CHG) {
|
|
/*
|
|
* BLK, REP and REBUILD have CURRENT hashes, so their own
|
|
* recovered contents can be validated directly and don't
|
|
* need this OLD/CURRENT generation inference.
|
|
*
|
|
* REBUILD is deliberately not a generation discriminator:
|
|
* its data is CURRENT independently from the state of its
|
|
* physical parity.
|
|
*/
|
|
assert(block_state == BLOCK_STATE_BLK || block_state == BLOCK_STATE_REP || block_state == BLOCK_STATE_REBUILD);
|
|
continue;
|
|
}
|
|
|
|
has_recovered_chg = 1;
|
|
|
|
/*
|
|
* With an INVALID OLD hash this CHG cannot tell us whether
|
|
* the candidate differs from OLD. It neither proves CURRENT
|
|
* nor disproves it.
|
|
*/
|
|
if (hash_is_invalid(failed[j].block->hash))
|
|
continue;
|
|
|
|
if (hash_is_zero(failed[j].block->hash)) {
|
|
/*
|
|
* ZERO exactly identifies the OLD contribution under the
|
|
* recovery invariant.
|
|
*
|
|
* A non-zero candidate therefore proves candidate != OLD and,
|
|
* since no third generation is possible, identifies the solution
|
|
* as CURRENT.
|
|
*
|
|
* A zero candidate is ambiguous because OLD and CURRENT
|
|
* may legitimately contain the same zero block.
|
|
*/
|
|
if (membcmp(buffer[failed[j].index], 0, state->block_size) != 0)
|
|
current_generation_proven = 1;
|
|
} else {
|
|
unsigned hash_kind;
|
|
|
|
hash_kind = rehash ? state->prevhash : state->hash;
|
|
|
|
/*
|
|
* The stored CHG hash identifies OLD contents under the recovery invariant.
|
|
* Generation detection must compare hash identity only and must not apply the
|
|
* CURRENT logical-size padding requirement.
|
|
*
|
|
* When the hash covers the complete canonical RAID block (memhash_is_block()),
|
|
* candidate identity is independent of logical size: a mismatch proves
|
|
* candidate != OLD and therefore identifies this validated RAID solution as CURRENT.
|
|
*
|
|
* For hashes that depend on the logical size (Murmur3/Spooky2), the OLD
|
|
* logical size is not available. Because a mismatch against CURRENT pos_size
|
|
* cannot distinguish candidate != OLD from a change in file size, the
|
|
* conservative approach keeps the generation ambiguous and does not treat
|
|
* the mismatch as proof of CURRENT.
|
|
*/
|
|
if (memhash_is_block(hash_kind)) {
|
|
size_t pos_size;
|
|
|
|
pos_size = file_block_size(failed[j].file, failed[j].file_pos, state->block_size);
|
|
|
|
if (block_hash_cmp(state, rehash, failed[j].block, pos_size, buffer[failed[j].index]) != 0)
|
|
current_generation_proven = 1;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (has_recovered_chg) {
|
|
if (current_generation_proven)
|
|
log_tag("repair_generation_current:%" PRIu64 ": Recovered CHG proves CURRENT generation\n", pos);
|
|
else
|
|
log_tag("repair_generation_unknown:%" PRIu64 ": Recovered CHG generation is ambiguous\n", pos);
|
|
}
|
|
|
|
/* mark all recovered blocks according to their proven generation */
|
|
for (j = 0; j < failed_count; ++j) {
|
|
unsigned block_state;
|
|
|
|
if (!failed[j].is_bad)
|
|
continue;
|
|
|
|
block_state = block_state_get(failed[j].block);
|
|
|
|
if (block_state == BLOCK_STATE_CHG) {
|
|
if (current_generation_proven) {
|
|
/*
|
|
* At least one recovered CHG distinguishes this validated
|
|
* RAID solution from OLD.
|
|
*
|
|
* Under the recovery invariant this proves that the solution
|
|
* is CURRENT. The CURRENT classification therefore applies to
|
|
* every CHG reconstructed by the same solution, including CHGs
|
|
* with an INVALID OLD hash or with OLD == CURRENT contents.
|
|
*/
|
|
failed[j].recovery = RECOVERY_CURRENT;
|
|
} else {
|
|
/*
|
|
* The RAID solution itself was successfully validated,
|
|
* but none of its recovered CHGs distinguishes CURRENT
|
|
* from OLD.
|
|
*
|
|
* This does not prove that the bytes are actually old or
|
|
* wrong. It only means that CURRENT cannot be proven.
|
|
*
|
|
* Preserve all recovered CHGs as best-effort data but
|
|
* mark them out-of-date so the caller does not report
|
|
* them as successfully recovered CURRENT contents.
|
|
*/
|
|
failed[j].recovery = RECOVERY_OUTOFDATE;
|
|
}
|
|
} else {
|
|
/* BLK, REP and REBUILD have trustworthy CURRENT hashes */
|
|
failed[j].recovery = RECOVERY_CURRENT;
|
|
}
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
if (ret > 0)
|
|
error += ret;
|
|
|
|
if (ret < 0)
|
|
log_tag("recover_sync:%" PRIu64 ":%u: Failed with no attempts\n", pos, n);
|
|
else
|
|
log_tag("recover_sync:%" PRIu64 ":%u: Failed with %d attempts\n", pos, n, ret);
|
|
|
|
/*
|
|
* Second strategy: parity still represents OLD data.
|
|
*
|
|
* The CURRENT attempt failed, so retry under the opposite history
|
|
* hypothesis: physical parity still represents the state before the
|
|
* interrupted sync.
|
|
*
|
|
* This interpretation also relies on the recovery invariant. Because no
|
|
* filesystem changes occurred after the interruption, CHG/DELETED OLD
|
|
* hashes still refer to the only pre-CURRENT generation that physical
|
|
* parity may represent.
|
|
*
|
|
* Under this hypothesis:
|
|
*
|
|
* BLK OLD parity contains the synchronized data we want to
|
|
* recover;
|
|
*
|
|
* REBUILD data and hash are CURRENT and unchanged by this history
|
|
* choice. Physical parity remains independently untrusted;
|
|
*
|
|
* CHG parity contains OLD while the readable file contains
|
|
* CURRENT;
|
|
*
|
|
* REP parity contains OLD while its stored hash describes
|
|
* CURRENT;
|
|
*
|
|
* DELETED parity contains OLD contents that are no longer present
|
|
* in the current filesystem.
|
|
*
|
|
* Every CHG/REP/DELETED contribution must therefore be converted to OLD
|
|
* when possible, or reconstructed as an auxiliary RAID unknown.
|
|
*
|
|
* REBUILD does not require such conversion because it has no alternate OLD
|
|
* data contribution associated with this state. If REBUILD itself is bad,
|
|
* it is recovered as required CURRENT data and independently validated
|
|
* through its CURRENT hash.
|
|
*
|
|
* The reconstructed OLD CHG/REP/DELETED values exist only to make recovery
|
|
* of bad BLK/REBUILD data possible. They are not themselves considered a
|
|
* successful recovery of CURRENT unsynchronized data.
|
|
*/
|
|
n = 0;
|
|
something_to_recover = 0;
|
|
something_unsynced = 0;
|
|
scratch_count = 0;
|
|
too_many_unknowns = 0;
|
|
|
|
for (j = 0; j < failed_count; ++j) {
|
|
unsigned block_state = block_state_get(failed[j].block);
|
|
|
|
/*
|
|
* Only CHG/REP/DELETED make the OLD strategy different from the
|
|
* CURRENT one because they have different OLD and CURRENT data
|
|
* contributions.
|
|
*
|
|
* REBUILD is deliberately excluded. Its data contribution is CURRENT
|
|
* in both history hypotheses; only its physical parity is untrusted,
|
|
* and repair_step() deals with that through independent validation.
|
|
*/
|
|
if (block_state == BLOCK_STATE_DELETED || block_state == BLOCK_STATE_CHG || block_state == BLOCK_STATE_REP) {
|
|
/*
|
|
* There is at least one entry whose OLD and CURRENT parity
|
|
* contributions may differ, making this history hypothesis
|
|
* distinct from the CURRENT attempt.
|
|
*/
|
|
something_unsynced = 1;
|
|
|
|
if (block_state == BLOCK_STATE_CHG
|
|
&& hash_is_zero(failed[j].block->hash)
|
|
) {
|
|
/*
|
|
* ZERO exactly identifies the OLD CHG contribution under
|
|
* the recovery invariant.
|
|
*
|
|
* Supply it directly without consuming a RAID equation.
|
|
*
|
|
* For a readable CHG, is_bad is false, so this temporary
|
|
* OLD buffer will not later be written back to the file.
|
|
*/
|
|
memset(buffer[failed[j].index], 0, state->block_size);
|
|
} else if ((block_state == BLOCK_STATE_CHG || block_state == BLOCK_STATE_DELETED)
|
|
&& hash_is_unique(failed[j].block->hash)
|
|
&& state_import_fetch(state, rehash, failed[j].block, buffer[failed[j].index]) == 0) {
|
|
/*
|
|
* CHG and DELETED retain a hash identifying OLD contents
|
|
* under the recovery invariant, so import can provide their
|
|
* exact pre-sync contribution.
|
|
*
|
|
* REP cannot use this shortcut because its stored hash
|
|
* identifies CURRENT/NEW contents instead.
|
|
*
|
|
* Again, readable auxiliary entries have is_bad == 0 and
|
|
* this OLD value will not be written to the user file.
|
|
*/
|
|
} else {
|
|
/*
|
|
* Adding this OLD unknown would exceed the number of available parity
|
|
* equations. No OLD recovery strategy can possibly use it, so stop before
|
|
* consuming a scratch buffer or changing independently validated CURRENT data.
|
|
*/
|
|
if (n >= state->level) {
|
|
too_many_unknowns = 1;
|
|
break;
|
|
}
|
|
|
|
/*
|
|
* If buffer[index] already contains CURRENT data validated by import/search,
|
|
* swap it with a preallocated scratch buffer so that the OLD reconstruction
|
|
* is solved into scratch without overwriting the certified CURRENT bytes.
|
|
*/
|
|
if (block_state == BLOCK_STATE_REP && failed[j].is_bad && failed[j].recovery == RECOVERY_CURRENT) {
|
|
assert(scratch_count < state->level);
|
|
rep_saved[scratch_count].j = j;
|
|
rep_saved[scratch_count].buffer = buffer[failed[j].index];
|
|
buffer[failed[j].index] = buffer_scratch[scratch_count];
|
|
++scratch_count;
|
|
}
|
|
|
|
/*
|
|
* The required OLD contribution is unavailable directly,
|
|
* so recover it as another RAID unknown.
|
|
*/
|
|
failed_map[n] = j;
|
|
++n;
|
|
|
|
/*
|
|
* Do not set something_to_recover.
|
|
*
|
|
* Reconstructing only OLD CHG/REP/DELETED values has no
|
|
* user-visible purpose. They are useful only as auxiliary
|
|
* state for recovering a BLK/REBUILD block.
|
|
*/
|
|
}
|
|
} else if (failed[j].is_bad) {
|
|
/*
|
|
* All generation-dependent unsynchronized states were handled
|
|
* above.
|
|
*
|
|
* A bad BLK is synchronized data represented by the pre-sync
|
|
* parity.
|
|
*
|
|
* A bad REBUILD also belongs here because its required data and
|
|
* hash are CURRENT and do not have a separate OLD version. Its
|
|
* physical parity is untrusted, but repair_step() accepts the
|
|
* candidate only after independent validation.
|
|
*
|
|
* If CURRENT recovery already obtained this BLK/REBUILD through
|
|
* import/search, buffer[index] contains certified CURRENT data.
|
|
* BLK and REBUILD have the same required data contribution under
|
|
* both history hypotheses, so that buffer remains a known RAID
|
|
* input and must not consume another recovery equation.
|
|
*
|
|
* Keep is_bad unchanged because the original file block still
|
|
* needs to be written back after the overall recovery succeeds.
|
|
*/
|
|
assert(block_state == BLOCK_STATE_BLK || block_state == BLOCK_STATE_REBUILD);
|
|
|
|
if (failed[j].recovery != RECOVERY_CURRENT) {
|
|
if (n >= state->level) {
|
|
too_many_unknowns = 1;
|
|
break;
|
|
}
|
|
|
|
something_to_recover = 1;
|
|
|
|
failed_map[n] = j;
|
|
++n;
|
|
}
|
|
}
|
|
}
|
|
|
|
/*
|
|
* Retry only when:
|
|
*
|
|
* - there is BLK/REBUILD data that we actually need to recover; and
|
|
*
|
|
* - there is some generation-dependent CHG/REP/DELETED state making
|
|
* this strategy different from the CURRENT attempt.
|
|
*
|
|
* REBUILD alone does not justify this retry because changing from the
|
|
* CURRENT to the OLD history hypothesis does not change its data input.
|
|
*
|
|
* Otherwise this strategy either has no useful CURRENT data to recover
|
|
* or would just repeat the same recovery equations already attempted.
|
|
*/
|
|
if (!too_many_unknowns && something_to_recover && something_unsynced) {
|
|
ret = repair_step(state, rehash, pos, diskmax, failed, failed_map, n, buffer, buffer_recov, 1);
|
|
|
|
if (ret == 0) {
|
|
/*
|
|
* The RAID solution is valid under the OLD-history hypothesis.
|
|
*
|
|
* Recovered BLK data is useful CURRENT data because its
|
|
* synchronized contents are represented by OLD parity.
|
|
*
|
|
* Recovered REBUILD data is also required CURRENT data, but for
|
|
* a different reason: REBUILD has no alternate OLD data
|
|
* generation and its candidate is independently validated by
|
|
* its CURRENT hash despite its untrusted physical parity.
|
|
*/
|
|
/*
|
|
* Restore the independently proven CURRENT bytes for REP blocks that
|
|
* used a scratch buffer during the OLD strategy.
|
|
*/
|
|
for (k = 0; k < scratch_count; ++k) {
|
|
unsigned idx = rep_saved[k].j;
|
|
|
|
buffer[failed[idx].index] = rep_saved[k].buffer;
|
|
failed[idx].recovery = RECOVERY_CURRENT;
|
|
log_tag("repair_hash_import:%u: Restored CURRENT data after OLD retry\n", idx);
|
|
}
|
|
scratch_count = 0;
|
|
|
|
/*
|
|
* Mark all recovered blocks according to their proven generation.
|
|
* Any bad CHG/REP reconstructed here without validated CURRENT data is
|
|
* OLD-compatible auxiliary data and cannot be certified as CURRENT.
|
|
* Bad BLK and REBUILD blocks are certified CURRENT by their hash.
|
|
*/
|
|
for (j = 0; j < failed_count; ++j) {
|
|
unsigned block_state;
|
|
|
|
/*
|
|
* Skip blocks that do not need recovery, or blocks already validated as
|
|
* CURRENT via import/search (including imported REPs restored above from
|
|
* scratch buffers) to prevent demoting them to RECOVERY_OUTOFDATE.
|
|
*/
|
|
if (!failed[j].is_bad || failed[j].recovery == RECOVERY_CURRENT)
|
|
continue;
|
|
|
|
block_state = block_state_get(failed[j].block);
|
|
|
|
if (block_state == BLOCK_STATE_CHG || block_state == BLOCK_STATE_REP) {
|
|
/* out-of-date entries cannot be certified CURRENT */
|
|
failed[j].recovery = RECOVERY_OUTOFDATE;
|
|
log_tag("repair_hash_unknown:%u: Surely old data\n", j);
|
|
} else {
|
|
/* BLK and REBUILD have trustworthy CURRENT hashes */
|
|
failed[j].recovery = RECOVERY_CURRENT;
|
|
}
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
if (ret > 0)
|
|
error += ret;
|
|
|
|
if (ret < 0)
|
|
log_tag("recover_unsync:%" PRIu64 ":%u: Failed with no attempts\n", pos, n);
|
|
else
|
|
log_tag("recover_unsync:%" PRIu64 ":%u: Failed with %d attempts\n", pos, n, ret);
|
|
} else {
|
|
if (too_many_unknowns)
|
|
log_tag("recover_unsync:%" PRIu64 ":%u: Skipped for too many failures\n", pos, n + 1);
|
|
else
|
|
log_tag("recover_unsync:%" PRIu64 ":%u: Skipped for%s%s\n", pos, n, !something_to_recover ? " nothing to recover" : "", !something_unsynced ? " nothing unsynced" : "");
|
|
}
|
|
|
|
/* restore original buffer pointers and state if OLD strategy failed or was skipped */
|
|
for (k = 0; k < scratch_count; ++k) {
|
|
unsigned idx = rep_saved[k].j;
|
|
|
|
buffer[failed[idx].index] = rep_saved[k].buffer;
|
|
failed[idx].recovery = RECOVERY_CURRENT;
|
|
}
|
|
|
|
/*
|
|
* Neither history hypothesis produced a usable RAID solution.
|
|
*
|
|
* Return the accumulated number of failed validation attempts when at
|
|
* least one candidate was actually tried. Otherwise return -1 to indicate
|
|
* that no usable recovery strategy was available.
|
|
*/
|
|
if (error)
|
|
return error;
|
|
else
|
|
return -1;
|
|
}
|
|
|
|
/**
|
|
* Post process all the files at the specified block index ::i.
|
|
* For each file, if we are at the last block, close it,
|
|
* adjust the timestamp, and print the result.
|
|
*
|
|
* This works only if the whole file is processed, including its last block.
|
|
* This doesn't always happen, like with an explicit end block.
|
|
*
|
|
* In such case, the check/fix command won't report any information of the
|
|
* files partially checked.
|
|
*/
|
|
static int file_post(struct snapraid_state* state, int fix, int partial, block_off_t i, struct snapraid_handle* handle, unsigned diskmax)
|
|
{
|
|
unsigned j;
|
|
int ret;
|
|
|
|
/*
|
|
* For all the files print the final status, and do the final time fix
|
|
* we also ensure to close files after processing the last block
|
|
*/
|
|
for (j = 0; j < diskmax; ++j) {
|
|
struct snapraid_block* block;
|
|
struct snapraid_disk* disk;
|
|
struct snapraid_file* collide_file;
|
|
struct snapraid_file* file;
|
|
block_off_t file_pos;
|
|
|
|
disk = handle[j].disk;
|
|
if (!disk) {
|
|
/* if no disk, nothing to do */
|
|
continue;
|
|
}
|
|
|
|
block = fs_par2block_find(disk, i);
|
|
if (!block_has_file(block)) {
|
|
/* if no file, nothing to do */
|
|
continue;
|
|
}
|
|
|
|
file = fs_par2file_get(disk, i, &file_pos);
|
|
|
|
/* if it isn't the last block in the file */
|
|
if (!file_block_is_last(file, file_pos)) {
|
|
/* nothing to do */
|
|
continue;
|
|
}
|
|
|
|
/* if the file is excluded, we have nothing to adjust as the file is never written */
|
|
if (file_flag_has(file, FILE_IS_EXCLUDED)
|
|
|| (state->opt.syncedonly && file_flag_has(file, FILE_IS_UNSYNCED))) {
|
|
/* nothing to do, but close the file */
|
|
goto close_and_continue;
|
|
}
|
|
|
|
/* finish the fix process if it's the last block of the files */
|
|
if (fix) {
|
|
if (handle[j].file != file) {
|
|
/* LCOV_EXCL_START */
|
|
log_fatal(EINTERNAL, "Internal inconsistency in file handle '%s' instead of '%s'\n",
|
|
handle[j].file != 0 ? handle[j].file->sub : "<none>", file->sub);
|
|
os_abort();
|
|
/* LCOV_EXCL_STOP */
|
|
}
|
|
/*
|
|
* A partial fix operates only at block level. Reaching the last block of a
|
|
* file doesn't imply that the whole file was processed, so don't perform any
|
|
* file-level finalization, including marking it finished, reporting its final
|
|
* status, or restoring its timestamp.
|
|
* This is intentional also when an explicit block range happens to cover all
|
|
* the parity blocks, to keep -S/-B behavior independent of the parity size.
|
|
*/
|
|
if (partial)
|
|
goto close_and_continue;
|
|
|
|
/*
|
|
* Mark that we finished with this file
|
|
* to identify later any NOT finished ones
|
|
*/
|
|
file_flag_set(file, FILE_IS_FINISHED);
|
|
|
|
/* if the file is damaged, meaning that a fix failed */
|
|
if (file_flag_has(file, FILE_IS_DAMAGED)) {
|
|
log_tag("status:unrecoverable:%s:%s\n", disk->name, esc_tag(file->sub));
|
|
msg_info("unrecoverable %s\n", fmt_term(disk, file->sub));
|
|
|
|
/* and do not set the time if damaged */
|
|
goto close_and_continue;
|
|
}
|
|
|
|
/* if the file is not fixed, meaning that it is untouched */
|
|
if (!file_flag_has(file, FILE_IS_FIXED)) {
|
|
/* nothing to do, but close the file */
|
|
goto close_and_continue;
|
|
}
|
|
|
|
/*
|
|
* If the file is closed or different than the one expected, reopen it
|
|
* a different open file could happen when filtering for bad blocks
|
|
*/
|
|
if (handle[j].file != file) {
|
|
/* keep a pointer at the file we are going to close for error reporting */
|
|
struct snapraid_file* report = handle[j].file;
|
|
ret = handle_close(&handle[j]);
|
|
if (ret != 0) {
|
|
/* LCOV_EXCL_START */
|
|
log_tag("%s::%s:%s: Close error. %s.\n", es(errno), disk->name, esc_tag(report->sub), strerror(errno));
|
|
log_fatal_errno(errno, disk->name);
|
|
return -1;
|
|
/* LCOV_EXCL_STOP */
|
|
}
|
|
|
|
/* reopen the file for writing, as required to set the mtime on Windows */
|
|
ret = handle_open_create(&handle[j], file, state->file_mode, 0);
|
|
if (ret != 0) {
|
|
/* LCOV_EXCL_START */
|
|
log_tag("%s:%" PRIu64 ":%s:%s: Open error. %s.\n", es(errno), i, disk->name, esc_tag(file->sub), strerror(errno));
|
|
log_fatal_errno(errno, disk->name);
|
|
return -1;
|
|
/* LCOV_EXCL_STOP */
|
|
}
|
|
}
|
|
|
|
/* search for the corresponding inode */
|
|
uint64_t inode = handle[j].st.st_ino; /* don't know the exact type of st_ino and we cannot pass it by pointer in the search */
|
|
if (inode != INODE_INVALID)
|
|
collide_file = tommy_hashdyn_search(&disk->inodeset, file_inode_compare_to_arg, &inode, file_inode_hash(inode));
|
|
else
|
|
collide_file = 0;
|
|
|
|
/*
|
|
* If the inode is already in the database and it refers to a different file name,
|
|
* we can fix the file time ONLY if the time and size allow to differentiate
|
|
* between the two files
|
|
*
|
|
* For example, suppose we delete a bunch of files with all the same size and time,
|
|
* when recreating them the inodes may be reused in a different order,
|
|
* and at the next sync some files may have matching inode/size/time even if different name
|
|
* not allowing sync to detect that the file is changed and not renamed
|
|
*/
|
|
if (!collide_file /* if not in the database, there is no collision */
|
|
|| strcmp(collide_file->sub, file->sub) == 0 /* if the name is the same, it's the right collision */
|
|
|| collide_file->size != file->size /* if the size is different, the collision is identified */
|
|
|| collide_file->mtime_sec != file->mtime_sec /* if the mtime is different, the collision is identified */
|
|
|| collide_file->mtime_nsec != file->mtime_nsec /* same for mtime_nsec */
|
|
) {
|
|
/* set the original modification time */
|
|
ret = handle_utime(&handle[j]);
|
|
if (ret == -1) {
|
|
/* LCOV_EXCL_START */
|
|
log_tag("%s:%" PRIu64 ":%s:%s: Time error. %s.\n", es(errno), i, disk->name, esc_tag(file->sub), strerror(errno));
|
|
log_fatal_errno(errno, disk->name);
|
|
|
|
/* mark the file as damaged */
|
|
file_flag_set(file, FILE_IS_DAMAGED);
|
|
return -1;
|
|
/* LCOV_EXCL_STOP */
|
|
}
|
|
} else {
|
|
log_tag("collision:%s:%s:%s: Not setting modification time to avoid inode collision\n", disk->name, esc_tag(file->sub), esc_tag(collide_file->sub));
|
|
}
|
|
|
|
/*
|
|
* Close the normal file before removing the previous quarantine.
|
|
* A delayed close error means that recovery is not yet successful,
|
|
* and the previous .unrecoverable file must remain available.
|
|
*/
|
|
ret = handle_close(&handle[j]);
|
|
if (ret != 0) {
|
|
/* LCOV_EXCL_START */
|
|
log_tag("%s::%s:%s: Close error. %s.\n", es(errno), disk->name, esc_tag(file->sub), strerror(errno));
|
|
log_fatal_errno(errno, disk->name);
|
|
return -1;
|
|
/* LCOV_EXCL_STOP */
|
|
}
|
|
|
|
/*
|
|
* The normal file has now been completely recovered, finalized, and closed.
|
|
* Remove any quarantine left by an earlier failed recovery attempt.
|
|
*/
|
|
ret = handle_unrecoverable_remove(&handle[j], file);
|
|
if (ret != 0) {
|
|
/* LCOV_EXCL_START */
|
|
log_tag("%s:%" PRIu64 ":%s:%s: Remove unrecoverable error. %s.\n", es(errno), i, disk->name, esc_tag(file->sub), strerror(errno));
|
|
log_fatal_errno(errno, disk->name);
|
|
return -1;
|
|
/* LCOV_EXCL_STOP */
|
|
}
|
|
|
|
log_tag("status:recovered:%s:%s\n", disk->name, esc_tag(file->sub));
|
|
msg_info("recovered %s\n", fmt_term(disk, file->sub));
|
|
} else {
|
|
/*
|
|
* We are not fixing, but only checking
|
|
* print just the final status
|
|
*/
|
|
if (file_flag_has(file, FILE_IS_DAMAGED)) {
|
|
log_tag("status:unrecoverable:%s:%s\n", disk->name, esc_tag(file->sub));
|
|
msg_info("unrecoverable %s\n", fmt_term(disk, file->sub));
|
|
} else if (file_flag_has(file, FILE_IS_FIXED)) {
|
|
log_tag("status:recoverable:%s:%s\n", disk->name, esc_tag(file->sub));
|
|
msg_info("recoverable %s\n", fmt_term(disk, file->sub));
|
|
} else {
|
|
/* we don't use msg_verbose() because it also goes into the log */
|
|
if (msg_level >= MSG_VERBOSE) {
|
|
log_tag("status:correct:%s:%s\n", disk->name, esc_tag(file->sub));
|
|
msg_info("correct %s\n", fmt_term(disk, file->sub));
|
|
}
|
|
}
|
|
}
|
|
|
|
close_and_continue:
|
|
/*
|
|
* If the opened file is the correct one, close it
|
|
* in case of excluded and fragmented files it's possible
|
|
* that the opened file is not the current one
|
|
*/
|
|
if (handle[j].file == file) {
|
|
/*
|
|
* Ensure to close the file just after finishing with it
|
|
* to avoid keeping it open without any possible use
|
|
*/
|
|
ret = handle_close(&handle[j]);
|
|
if (ret != 0) {
|
|
/* LCOV_EXCL_START */
|
|
log_tag("%s::%s:%s: Close error. %s.\n", es(errno), disk->name, esc_tag(file->sub), strerror(errno));
|
|
log_fatal_errno(errno, disk->name);
|
|
return -1;
|
|
/* LCOV_EXCL_STOP */
|
|
}
|
|
}
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
/**
|
|
* Check if we have to process the specified block index ::i.
|
|
*/
|
|
static int block_is_enabled(struct snapraid_state* state, block_off_t i, struct snapraid_handle* handle, unsigned diskmax)
|
|
{
|
|
unsigned j;
|
|
unsigned l;
|
|
|
|
/* filter for bad blocks */
|
|
if (state->opt.badblockonly) {
|
|
snapraid_info info;
|
|
|
|
/* get block specific info */
|
|
info = info_get(&state->infoarr, i);
|
|
|
|
/*
|
|
* Filter specifically only for bad blocks
|
|
*/
|
|
return info_get_bad(info);
|
|
}
|
|
|
|
/* filter for the parity */
|
|
if (state->opt.badfileonly) {
|
|
snapraid_info info;
|
|
|
|
/* get block specific info */
|
|
info = info_get(&state->infoarr, i);
|
|
|
|
/*
|
|
* If the block is bad, it has to be processed
|
|
*
|
|
* This is not necessary in normal cases because if a block is bad,
|
|
* it necessary needs to have a file related to it, and files with
|
|
* bad blocks are fully included.
|
|
*
|
|
* But some files may be excluded by additional filter options,
|
|
* so it's not always true, and this ensures to always check all
|
|
* the bad blocks.
|
|
*/
|
|
if (info_get_bad(info))
|
|
return 1;
|
|
} else {
|
|
/* if a parity is not excluded, include all blocks, even unused ones */
|
|
for (l = 0; l < state->level; ++l) {
|
|
if (!state->parity[l].is_excluded_by_filter) {
|
|
return 1;
|
|
}
|
|
}
|
|
}
|
|
|
|
/* filter for the files */
|
|
for (j = 0; j < diskmax; ++j) {
|
|
struct snapraid_block* block;
|
|
|
|
/* if no disk, nothing to check */
|
|
if (!handle[j].disk)
|
|
continue;
|
|
|
|
block = fs_par2block_find(handle[j].disk, i);
|
|
|
|
/*
|
|
* Try to recover all files, even the ones without hash
|
|
* because in some cases we can recover also them
|
|
*/
|
|
if (block_has_file(block)) {
|
|
struct snapraid_file* file = fs_par2file_get(handle[j].disk, i, 0);
|
|
if (!file_flag_has(file, FILE_IS_EXCLUDED)) { /* only if the file is not filtered out */
|
|
return 1;
|
|
}
|
|
}
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
/**
|
|
* Write recovered data to files and update file status flags.
|
|
*/
|
|
static int file_recover(struct snapraid_state* state, int fix, int partial, block_off_t block_pos, struct failed_struct* failed, unsigned failed_count, void** buffer, unsigned* recovered_error)
|
|
{
|
|
unsigned j;
|
|
|
|
for (j = 0; j < failed_count; ++j) {
|
|
int ret;
|
|
|
|
/* nothing to do if it doesn't need recovering */
|
|
if (!failed[j].is_bad)
|
|
continue;
|
|
|
|
/* nothing to do if it could not be recovered */
|
|
if (failed[j].recovery == RECOVERY_NONE)
|
|
continue;
|
|
|
|
if (!fix) {
|
|
/*
|
|
* In check mode, report as recoverable only blocks whose
|
|
* recovered contents can be certified as the required CURRENT data.
|
|
*
|
|
* repair() may return success after validating the RAID solution while
|
|
* leaving an unsynchronized bad block marked RECOVERY_OUTOFDATE. Such a block
|
|
* is still unrecoverable as CURRENT data and must make the containing
|
|
* file DAMAGED rather than FIXED.
|
|
*/
|
|
if (failed[j].recovery == RECOVERY_OUTOFDATE)
|
|
file_flag_set(failed[j].file, FILE_IS_DAMAGED);
|
|
else
|
|
file_flag_set(failed[j].file, FILE_IS_FIXED);
|
|
continue;
|
|
}
|
|
|
|
/* do not fix if the file is excluded */
|
|
if (file_flag_has(failed[j].file, FILE_IS_EXCLUDED)
|
|
|| (state->opt.syncedonly && file_flag_has(failed[j].file, FILE_IS_UNSYNCED)))
|
|
continue;
|
|
|
|
/*
|
|
* Write the best recovered candidate even when it is out-of-date. Such data
|
|
* may still be useful to the user or to a later multistep recovery.
|
|
*
|
|
* An out-of-date candidate is useful best-effort data,
|
|
* but it cannot be certified as the required CURRENT
|
|
* contents. Persist this fact before modifying the file
|
|
* so that a SIGKILL cannot leave unproven recovered data
|
|
* under the normal filename.
|
|
*/
|
|
if (failed[j].recovery == RECOVERY_OUTOFDATE) {
|
|
file_flag_set(failed[j].file, FILE_IS_DAMAGED);
|
|
|
|
/*
|
|
* Quarantine is a file-level operation because it renames the whole file.
|
|
* A partial -S/-B fix may still write the selected best-effort block, but it
|
|
* must not rename or quarantine the complete file outside the requested range.
|
|
*/
|
|
if (!partial) {
|
|
ret = handle_quarantine(failed[j].handle, failed[j].file, state->file_mode);
|
|
if (ret == -1) {
|
|
/* LCOV_EXCL_START */
|
|
log_tag("%s:%" PRIu64 ":%s:%s: Quarantine error. %s.\n", es(errno), block_pos, failed[j].disk->name, esc_tag(failed[j].file->sub), strerror(errno));
|
|
log_fatal_errno(errno, failed[j].disk->name);
|
|
log_fatal(errno, "Stopping at block %" PRIu64 "\n", block_pos);
|
|
return -1;
|
|
/* LCOV_EXCL_STOP */
|
|
}
|
|
}
|
|
}
|
|
|
|
ret = handle_write(failed[j].handle, failed[j].file_pos, buffer[failed[j].index], state->block_size);
|
|
if (ret == -1) {
|
|
/* LCOV_EXCL_START */
|
|
int saved_errno = errno;
|
|
|
|
/* mark the file as damaged */
|
|
file_flag_set(failed[j].file, FILE_IS_DAMAGED);
|
|
|
|
/*
|
|
* Quarantine is a file-level operation because it renames the whole file.
|
|
* A partial -S/-B fix must leave the complete file under its normal name even
|
|
* when a selected block write fails; FILE_IS_DAMAGED still records the failure.
|
|
*/
|
|
if (!partial) {
|
|
ret = handle_quarantine(failed[j].handle, failed[j].file, state->file_mode);
|
|
if (ret == -1) {
|
|
log_tag("%s:%" PRIu64 ":%s:%s: Quarantine error. %s.\n", es(errno), block_pos, failed[j].disk->name, esc_tag(failed[j].file->sub), strerror(errno));
|
|
log_fatal_errno(errno, failed[j].disk->name);
|
|
}
|
|
}
|
|
|
|
errno = saved_errno;
|
|
log_tag("%s:%" PRIu64 ":%s:%s: Write error. %s.\n", es(errno), block_pos, failed[j].disk->name, esc_tag(failed[j].file->sub), strerror(errno));
|
|
log_fatal_errno(errno, failed[j].disk->name);
|
|
log_fatal(errno, "Stopping at block %" PRIu64 "\n", block_pos);
|
|
|
|
return -1;
|
|
/* LCOV_EXCL_STOP */
|
|
}
|
|
|
|
/*
|
|
* A recovery write changes the file modification time. With syncedonly,
|
|
* later recovery writes are allowed only while the file still matches
|
|
* the metadata stored in the content file. Restore the original mtime
|
|
* after every successful write so an interruption between blocks does
|
|
* not make a partially recovered file appear unsynced on the next run.
|
|
*/
|
|
if (state->opt.syncedonly) {
|
|
ret = handle_utime(failed[j].handle);
|
|
if (ret == -1) {
|
|
/* LCOV_EXCL_START */
|
|
file_flag_set(failed[j].file, FILE_IS_DAMAGED);
|
|
log_tag("%s:%" PRIu64 ":%s:%s: Time error. %s.\n", es(errno), block_pos, failed[j].disk->name, esc_tag(failed[j].file->sub), strerror(errno));
|
|
log_fatal_errno(errno, failed[j].disk->name);
|
|
log_fatal(errno, "Stopping at block %" PRIu64 "\n", block_pos);
|
|
return -1;
|
|
/* LCOV_EXCL_STOP */
|
|
}
|
|
}
|
|
|
|
/* if we are not sure that the recovered content is uptodate */
|
|
if (failed[j].recovery == RECOVERY_OUTOFDATE)
|
|
continue;
|
|
|
|
/*
|
|
* Mark the file as containing some fixes
|
|
* note that it could be also marked as damaged in other iterations
|
|
*/
|
|
file_flag_set(failed[j].file, FILE_IS_FIXED);
|
|
|
|
log_tag("fixed:%" PRIu64 ":%s:%s: Fixed data error at position %" PRIu64 "\n", block_pos, failed[j].disk->name, esc_tag(failed[j].file->sub), failed[j].file_pos);
|
|
++*recovered_error;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int state_check_process(struct snapraid_state* state, int fix, struct snapraid_parity_handle** parity, block_off_t blockstart, block_off_t blockmax, int partial)
|
|
{
|
|
struct snapraid_handle* handle;
|
|
unsigned diskmax;
|
|
block_off_t i;
|
|
unsigned j;
|
|
void* buffer_alloc;
|
|
void** buffer;
|
|
void** buffer_scratch;
|
|
unsigned buffermax;
|
|
ssize_t ret;
|
|
data_off_t countsize;
|
|
block_off_t countpos;
|
|
block_off_t countmax;
|
|
unsigned soft_error;
|
|
unsigned io_error;
|
|
unsigned silent_error;
|
|
unsigned unrecoverable_error;
|
|
unsigned recovered_error;
|
|
struct failed_struct* failed;
|
|
unsigned* failed_map;
|
|
unsigned l;
|
|
bit_vect_t* block_enabled;
|
|
struct snapraid_bw bw;
|
|
|
|
handle = handle_mapping(state, &diskmax);
|
|
|
|
/* initialize the bandwidth context */
|
|
bw_init(&bw, state->opt.bwlimit);
|
|
|
|
/* share the bandwidth context with all handles */
|
|
for (j = 0; j < diskmax; ++j)
|
|
handle[j].bw = &bw;
|
|
for (j = 0; j < state->level; ++j)
|
|
if (parity[j])
|
|
parity[j]->bw = &bw;
|
|
|
|
/* we need 1 * data + 2 * parity + 1 * scratch */
|
|
buffermax = diskmax + 3 * state->level;
|
|
|
|
buffer = malloc_nofail_vector_align(buffermax, state->block_size, &buffer_alloc);
|
|
if (!state->opt.skip_self)
|
|
mtest_vector(buffermax, state->block_size, buffer);
|
|
|
|
buffer_scratch = buffer + diskmax + 2 * state->level;
|
|
|
|
failed = nalloc_nofail(diskmax, sizeof(struct failed_struct));
|
|
failed_map = nalloc_nofail(diskmax, sizeof(unsigned));
|
|
|
|
soft_error = 0;
|
|
io_error = 0;
|
|
silent_error = 0;
|
|
unrecoverable_error = 0;
|
|
recovered_error = 0;
|
|
|
|
msg_progress("Selecting...\n");
|
|
|
|
/* first count the number of blocks to process */
|
|
countmax = 0;
|
|
block_enabled = calloc_nofail(1, bit_vect_size(blockmax)); /* preinitialize to 0 */
|
|
for (i = blockstart; i < blockmax; ++i) {
|
|
if (!block_is_enabled(state, i, handle, diskmax))
|
|
continue;
|
|
bit_vect_set(block_enabled, i);
|
|
++countmax;
|
|
}
|
|
|
|
if (fix)
|
|
msg_progress("Fixing...\n");
|
|
else if (!state->opt.auditonly)
|
|
msg_progress("Checking...\n");
|
|
else
|
|
msg_progress("Hashing...\n");
|
|
|
|
/* check all the blocks in files */
|
|
countsize = 0;
|
|
countpos = 0;
|
|
|
|
int alert = state_progress_begin(state, blockstart, blockmax, countmax);
|
|
if (alert > 0)
|
|
goto end;
|
|
if (alert < 0)
|
|
goto bail;
|
|
|
|
for (i = blockstart; i < blockmax; ++i) {
|
|
unsigned failed_count;
|
|
int valid_parity;
|
|
int used_parity;
|
|
snapraid_info info;
|
|
int rehash;
|
|
|
|
if (!bit_vect_test(block_enabled, i)) {
|
|
/* continue with the next block */
|
|
continue;
|
|
}
|
|
|
|
/*
|
|
* If we have valid parity, and it makes sense to check its content.
|
|
* If we already know that the parity is invalid, we just read the file
|
|
* but we don't report parity errors
|
|
* Note that with auditonly, we anyway skip the full parity check,
|
|
* because we also don't read it at all
|
|
*/
|
|
valid_parity = 1;
|
|
|
|
/* if the parity is used by at least one file */
|
|
used_parity = 0;
|
|
|
|
/* keep track of the number of failed blocks */
|
|
failed_count = 0;
|
|
|
|
/* get block specific info */
|
|
info = info_get(&state->infoarr, i);
|
|
|
|
/* if we have to use the old hash */
|
|
rehash = info_get_rehash(info);
|
|
|
|
/* for each disk, process the block */
|
|
for (j = 0; j < diskmax; ++j) {
|
|
ssize_t read_size;
|
|
unsigned char hash[HASH_MAX];
|
|
struct snapraid_disk* disk;
|
|
struct snapraid_block* block;
|
|
struct snapraid_file* file;
|
|
block_off_t file_pos;
|
|
unsigned block_state;
|
|
|
|
/* if the disk position is not used */
|
|
disk = handle[j].disk;
|
|
if (!disk) {
|
|
/* use an empty block */
|
|
memset(buffer[j], 0, state->block_size);
|
|
continue;
|
|
}
|
|
|
|
/* if the disk block is not used */
|
|
block = fs_par2block_find(disk, i);
|
|
if (block == BLOCK_NULL) {
|
|
/* use an empty block */
|
|
memset(buffer[j], 0, state->block_size);
|
|
continue;
|
|
}
|
|
|
|
/* get the state of the block */
|
|
block_state = block_state_get(block);
|
|
|
|
/* if the parity is not valid */
|
|
if (block_has_invalid_parity(block)) {
|
|
/*
|
|
* Mark the parity as invalid, and don't try to check/fix it
|
|
* because it will be recomputed at the next sync
|
|
*/
|
|
valid_parity = 0;
|
|
/* follow */
|
|
}
|
|
|
|
/* if the block is DELETED */
|
|
if (block_state == BLOCK_STATE_DELETED) {
|
|
/* use an empty block */
|
|
memset(buffer[j], 0, state->block_size);
|
|
|
|
/*
|
|
* DELETED is not a file repair target. It is kept only because its OLD
|
|
* contribution may still be present in parity and may be required by the
|
|
* pre-sync recovery hypothesis.
|
|
*/
|
|
failed[failed_count].is_bad = 0;
|
|
failed[failed_count].recovery = RECOVERY_NONE;
|
|
failed[failed_count].index = j;
|
|
failed[failed_count].block = block;
|
|
failed[failed_count].disk = disk;
|
|
failed[failed_count].file = 0;
|
|
failed[failed_count].file_pos = 0;
|
|
failed[failed_count].handle = 0;
|
|
++failed_count;
|
|
continue;
|
|
}
|
|
|
|
/* here we are sure that the parity is used by a file */
|
|
used_parity = 1;
|
|
|
|
/* get the file of this block */
|
|
file = fs_par2file_get(disk, i, &file_pos);
|
|
|
|
/* if we are only hashing, we can skip excluded files and don't even read them */
|
|
if (state->opt.auditonly && file_flag_has(file, FILE_IS_EXCLUDED)) {
|
|
/*
|
|
* Use an empty block
|
|
* in true, this is unnecessary, because we are not checking any parity
|
|
* but we keep it for completeness
|
|
*/
|
|
memset(buffer[j], 0, state->block_size);
|
|
continue;
|
|
}
|
|
|
|
/* if the file is closed or different than the current one */
|
|
if (handle[j].file == 0 || handle[j].file != file) {
|
|
/* keep a pointer at the file we are going to close for error reporting */
|
|
struct snapraid_file* report = handle[j].file;
|
|
ret = handle_close(&handle[j]);
|
|
if (ret == -1) {
|
|
/* LCOV_EXCL_START */
|
|
log_tag("%s::%s:%s: Close error. %s.\n", es(errno), disk->name, esc_tag(report->sub), strerror(errno));
|
|
log_fatal_errno(errno, disk->name);
|
|
log_fatal(errno, "Stopping at block %" PRIu64 "\n", i);
|
|
|
|
++unrecoverable_error;
|
|
goto bail;
|
|
/* LCOV_EXCL_STOP */
|
|
}
|
|
|
|
/* if fixing, and the file is not excluded, we must open for writing */
|
|
if (fix && !file_flag_has(file, FILE_IS_EXCLUDED)) {
|
|
if (!state->opt.syncedonly) {
|
|
/* ordinary fix: create the file, open for writing and resize if required */
|
|
ret = handle_open_create(&handle[j], file, state->file_mode, 1);
|
|
if (ret == -1) {
|
|
/* LCOV_EXCL_START */
|
|
log_tag("%s:%" PRIu64 ":%s:%s: Create error. %s.\n", es(errno), i, disk->name, esc_tag(file->sub), strerror(errno));
|
|
log_fatal_errno(errno, disk->name);
|
|
log_fatal(errno, "Stopping at block %" PRIu64 "\n", i);
|
|
|
|
++unrecoverable_error;
|
|
goto bail;
|
|
/* LCOV_EXCL_STOP */
|
|
}
|
|
|
|
/* check if the file was just created */
|
|
if (handle[j].created != 0) {
|
|
/*
|
|
* If fragmented, it may be reopened, so remember that the file
|
|
* was originally missing
|
|
*/
|
|
file_flag_set(file, FILE_IS_CREATED);
|
|
}
|
|
} else {
|
|
/* in syncedonly mode, open for writing but do not create missing files */
|
|
if (!file_flag_has(file, FILE_IS_MISSING)) {
|
|
ret = handle_open_create(&handle[j], file, state->file_mode, 0);
|
|
} else {
|
|
errno = ENOENT;
|
|
ret = -1;
|
|
}
|
|
|
|
/* if missing, mark as unsynced and let common error handler record the missing block */
|
|
if (ret == -1 && errno == ENOENT) {
|
|
file_flag_set(file, FILE_IS_UNSYNCED);
|
|
} else if (ret == -1) {
|
|
/* LCOV_EXCL_START */
|
|
log_tag("%s:%" PRIu64 ":%s:%s: Open error. %s.\n", es(errno), i, disk->name, esc_tag(file->sub), strerror(errno));
|
|
log_fatal_errno(errno, disk->name);
|
|
log_fatal(errno, "Stopping at block %" PRIu64 "\n", i);
|
|
|
|
++unrecoverable_error;
|
|
goto bail;
|
|
/* LCOV_EXCL_STOP */
|
|
}
|
|
}
|
|
} else {
|
|
/* open the file only for reading */
|
|
if (!file_flag_has(file, FILE_IS_MISSING)) {
|
|
ret = handle_open(&handle[j], file, state->file_mode, state->opt.expected_missing ? log_expected : 0);
|
|
} else {
|
|
errno = ENOENT;
|
|
ret = -1; /* if the file is missing, we cannot open it */
|
|
}
|
|
}
|
|
|
|
if (ret == -1) {
|
|
/* save the failed block for the check/fix */
|
|
failed[failed_count].is_bad = 1;
|
|
failed[failed_count].recovery = RECOVERY_NONE;
|
|
failed[failed_count].index = j;
|
|
failed[failed_count].block = block;
|
|
failed[failed_count].disk = disk;
|
|
failed[failed_count].file = file;
|
|
failed[failed_count].file_pos = file_pos;
|
|
failed[failed_count].handle = &handle[j];
|
|
++failed_count;
|
|
|
|
log_tag("%s:%" PRIu64 ":%s:%s: Open error at position %" PRIu64 ". %s.\n", es(errno), i, disk->name, esc_tag(file->sub), file_pos, strerror(errno));
|
|
|
|
if (is_hw(errno)) {
|
|
++io_error;
|
|
} else {
|
|
++soft_error;
|
|
}
|
|
|
|
/*
|
|
* Mark the file as missing, to avoid to retry to open it again
|
|
* note that this can be done only if we are not fixing it
|
|
* or if syncedonly is active so it won't be recreated
|
|
*/
|
|
file_flag_set(file, FILE_IS_MISSING);
|
|
continue;
|
|
}
|
|
|
|
/* if it's the first open, and not excluded */
|
|
if (!file_flag_has(file, FILE_IS_OPENED)
|
|
&& !file_flag_has(file, FILE_IS_EXCLUDED)) {
|
|
|
|
/* check if the file is changed */
|
|
if (handle[j].st.st_size != file->size
|
|
|| handle[j].st.st_mtime != file->mtime_sec
|
|
|| STAT_NSEC(&handle[j].st) != file->mtime_nsec
|
|
/* don't check the inode to support file-system without persistent inodes */
|
|
) {
|
|
/* report that the file is not synced */
|
|
file_flag_set(file, FILE_IS_UNSYNCED);
|
|
}
|
|
}
|
|
|
|
/*
|
|
* Missing data inside a short file is handled block by block by normal
|
|
* recovery writes, which extend the file as needed.
|
|
*
|
|
* Extra data past the expected end has no content block and therefore requires
|
|
* a file-level truncate during a full fix.
|
|
*
|
|
* A partial -S/-B fix operates only at block level, so it must leave any
|
|
* untracked tail untouched, even if the file is larger than expected.
|
|
*/
|
|
if (!file_flag_has(file, FILE_IS_OPENED)
|
|
&& !file_flag_has(file, FILE_IS_EXCLUDED)
|
|
&& !partial
|
|
&& !(state->opt.syncedonly && file_flag_has(file, FILE_IS_UNSYNCED))
|
|
&& handle[j].st.st_size > file->size
|
|
) {
|
|
log_error(ESOFT, "File '%s' is larger than expected.\n", handle[j].path);
|
|
log_tag("error:%" PRIu64 ":%s:%s: Size error\n", i, disk->name, esc_tag(file->sub));
|
|
++soft_error;
|
|
|
|
if (fix) {
|
|
ret = handle_truncate(&handle[j], file);
|
|
if (ret == -1) {
|
|
/* LCOV_EXCL_START */
|
|
log_tag("%s:%" PRIu64 ":%s:%s: Truncate error. %s.\n", es(errno), i, disk->name, esc_tag(file->sub), strerror(errno));
|
|
log_fatal_errno(errno, disk->name);
|
|
log_fatal(errno, "Stopping at block %" PRIu64 "\n", i);
|
|
|
|
++unrecoverable_error;
|
|
goto bail;
|
|
/* LCOV_EXCL_STOP */
|
|
}
|
|
|
|
file_flag_set(file, FILE_IS_FIXED);
|
|
|
|
log_tag("fixed:%" PRIu64 ":%s:%s: Fixed size\n", i, disk->name, esc_tag(file->sub));
|
|
++recovered_error;
|
|
}
|
|
}
|
|
|
|
/*
|
|
* Mark the file as opened at least one time
|
|
* this is used to avoid to check the unsynced and size
|
|
* more than one time, in case the file is reopened later
|
|
*/
|
|
file_flag_set(file, FILE_IS_OPENED);
|
|
}
|
|
|
|
/* read from the file */
|
|
if (file_flag_has(file, FILE_IS_MISSING)) {
|
|
/* if the file is reported missing, don't even try to read it */
|
|
errno = ENOENT;
|
|
read_size = -1;
|
|
} else {
|
|
read_size = handle_read(&handle[j], file_pos, buffer[j], state->block_size, state->opt.expected_missing ? log_expected : 0);
|
|
}
|
|
if (read_size == -1) {
|
|
/* save the failed block for the check/fix */
|
|
failed[failed_count].is_bad = 1; /* it's bad because we cannot read it */
|
|
failed[failed_count].recovery = RECOVERY_NONE;
|
|
failed[failed_count].index = j;
|
|
failed[failed_count].block = block;
|
|
failed[failed_count].disk = disk;
|
|
failed[failed_count].file = file;
|
|
failed[failed_count].file_pos = file_pos;
|
|
failed[failed_count].handle = &handle[j];
|
|
++failed_count;
|
|
|
|
log_tag("%s:%" PRIu64 ":%s:%s: Read error at position %" PRIu64 ". %s.\n", es(errno), i, disk->name, esc_tag(file->sub), file_pos, strerror(errno));
|
|
|
|
if (is_hw(errno)) {
|
|
++io_error;
|
|
} else {
|
|
++soft_error;
|
|
}
|
|
|
|
/* if we are reading at the end, mark the file as missing to avoid to try to read it again at the next block */
|
|
if (errno == ENOENT) {
|
|
file_flag_set(file, FILE_IS_MISSING);
|
|
}
|
|
continue;
|
|
}
|
|
|
|
countsize += read_size;
|
|
|
|
/*
|
|
* Always insert CHG blocks, the repair functions needs all of them
|
|
* because the parity may be still referring at the old state
|
|
* and the repair must be aware of it
|
|
*/
|
|
if (block_state == BLOCK_STATE_CHG) {
|
|
/*
|
|
* A CHG block has no hash for the current data. The stored hash
|
|
* refers to the previous content, and after an interrupted sync
|
|
* parity may refer either to the old or to the new content.
|
|
*
|
|
* If the block is readable, preserve it and assume it is the best
|
|
* available copy of the current data. We must not try to validate it
|
|
* against block->hash, as a match would only identify the old data
|
|
* and a mismatch would not prove that the current data is correct.
|
|
*
|
|
* This assumption applies only to data belonging to the current recovery
|
|
* attempt. A CHG from a previous .unrecoverable file cannot safely be reused:
|
|
* it may itself be an ambiguous recovery result whose CURRENT generation was
|
|
* not proven. Once persisted, that provenance cannot be inferred from the
|
|
* bytes alone.
|
|
*/
|
|
failed[failed_count].is_bad = 0; /* we assume the CHG block correct */
|
|
failed[failed_count].recovery = RECOVERY_NONE;
|
|
failed[failed_count].index = j;
|
|
failed[failed_count].block = block;
|
|
failed[failed_count].disk = disk;
|
|
failed[failed_count].file = file;
|
|
failed[failed_count].file_pos = file_pos;
|
|
failed[failed_count].handle = &handle[j];
|
|
++failed_count;
|
|
continue;
|
|
}
|
|
|
|
assert(block_state == BLOCK_STATE_BLK || block_state == BLOCK_STATE_REP || block_state == BLOCK_STATE_REBUILD);
|
|
|
|
/* compute the hash of the block just read */
|
|
if (rehash) {
|
|
memhash_block(state->prevhash, state->prevhashseed, hash, buffer[j], read_size, state->block_size);
|
|
} else {
|
|
memhash_block(state->hash, state->hashseed, hash, buffer[j], read_size, state->block_size);
|
|
}
|
|
|
|
/* compare the hash */
|
|
if (memcmp(hash, block->hash, BLOCK_HASH_SIZE) != 0) {
|
|
unsigned diff = memdiff(hash, block->hash, BLOCK_HASH_SIZE);
|
|
|
|
/* save the failed block for the check/fix */
|
|
failed[failed_count].is_bad = 1; /* it's bad because the hash doesn't match */
|
|
failed[failed_count].recovery = RECOVERY_NONE;
|
|
failed[failed_count].index = j;
|
|
failed[failed_count].block = block;
|
|
failed[failed_count].disk = disk;
|
|
failed[failed_count].file = file;
|
|
failed[failed_count].file_pos = file_pos;
|
|
failed[failed_count].handle = &handle[j];
|
|
++failed_count;
|
|
|
|
log_tag("error_data:%" PRIu64 ":%s:%s: Data error at position %" PRIu64 ", diff hash bits %u/%zu\n", i, disk->name, esc_tag(file->sub), file_pos, diff, BLOCK_HASH_SIZE * 8);
|
|
++silent_error;
|
|
continue;
|
|
}
|
|
|
|
/*
|
|
* A readable REP has already passed its NEW/current hash check, so its current
|
|
* data is known good. It must nevertheless participate in repair because an
|
|
* interrupted sync may have left physical parity containing its OLD
|
|
* contribution.
|
|
*
|
|
* Unlike CHG, the REP stored hash cannot identify that OLD contribution
|
|
* because it describes the NEW data.
|
|
*/
|
|
if (block_state == BLOCK_STATE_REP) {
|
|
failed[failed_count].is_bad = 0; /* it's not bad */
|
|
failed[failed_count].recovery = RECOVERY_NONE;
|
|
failed[failed_count].index = j;
|
|
failed[failed_count].block = block;
|
|
failed[failed_count].disk = disk;
|
|
failed[failed_count].file = file;
|
|
failed[failed_count].file_pos = file_pos;
|
|
failed[failed_count].handle = &handle[j];
|
|
++failed_count;
|
|
continue;
|
|
}
|
|
}
|
|
|
|
/* now read and check the parity if requested */
|
|
if (!state->opt.auditonly) {
|
|
void* buffer_recov[LEV_MAX];
|
|
|
|
/* buffers for parity read and not computed */
|
|
for (l = 0; l < state->level; ++l)
|
|
buffer_recov[l] = buffer[diskmax + state->level + l];
|
|
for (; l < LEV_MAX; ++l)
|
|
buffer_recov[l] = 0;
|
|
|
|
/* read the parity */
|
|
for (l = 0; l < state->level; ++l) {
|
|
if (parity[l]) {
|
|
ret = parity_read(parity[l], i, buffer_recov[l], state->block_size);
|
|
if (ret == -1) {
|
|
log_tag("parity_%s:%" PRIu64 ":%s: Read error. %s.\n", es(errno), i, lev_config_name(l), strerror(errno));
|
|
|
|
/*
|
|
* NULL means that this parity level cannot be trusted as an input to recovery.
|
|
* The same marker is later used for a parity block that was read successfully
|
|
* but failed comparison with recomputed parity.
|
|
*
|
|
* During fix, an accessible non-excluded parity marked this way is also the
|
|
* one that needs to be rewritten from the recomputed value.
|
|
*/
|
|
buffer_recov[l] = 0;
|
|
|
|
if (is_hw(errno)) {
|
|
++io_error;
|
|
} else {
|
|
++soft_error;
|
|
}
|
|
}
|
|
} else {
|
|
buffer_recov[l] = 0;
|
|
}
|
|
}
|
|
|
|
/* try all the recovering strategies */
|
|
ret = repair(state, rehash, i, diskmax, failed, failed_map, failed_count, buffer, buffer_recov, buffer_scratch);
|
|
if (ret != 0) {
|
|
/* increment the number of errors */
|
|
if (ret > 0)
|
|
silent_error += ret;
|
|
++unrecoverable_error;
|
|
|
|
/* print a list of all unrecoverable errors in files */
|
|
for (j = 0; j < failed_count; ++j) {
|
|
if (failed[j].is_bad && failed[j].recovery != RECOVERY_CURRENT)
|
|
log_tag("unrecoverable:%" PRIu64 ":%s:%s: Unrecoverable error at position %" PRIu64 "\n", i, failed[j].disk->name, esc_tag(failed[j].file->sub), failed[j].file_pos);
|
|
}
|
|
|
|
/* update files that have blocks fixed by import/search */
|
|
if (file_recover(state, fix, partial, i, failed, failed_count, buffer, &recovered_error) != 0) {
|
|
++unrecoverable_error;
|
|
goto bail;
|
|
}
|
|
|
|
/* keep track of damaged files for blocks that could not be recovered */
|
|
for (j = 0; j < failed_count; ++j) {
|
|
if (!failed[j].is_bad || failed[j].recovery == RECOVERY_CURRENT)
|
|
continue;
|
|
|
|
file_flag_set(failed[j].file, FILE_IS_DAMAGED);
|
|
|
|
/*
|
|
* Persist the unrecoverable state immediately for a full fix.
|
|
* Quarantine is a file-level operation because it renames the whole file.
|
|
* A partial -S/-B fix operates only at block level, so keep FILE_IS_DAMAGED
|
|
* but leave the file under its normal name when partial is set.
|
|
*/
|
|
if (fix
|
|
&& !file_flag_has(failed[j].file, FILE_IS_EXCLUDED)
|
|
&& !(state->opt.syncedonly && file_flag_has(failed[j].file, FILE_IS_UNSYNCED))
|
|
&& !partial
|
|
) {
|
|
ret = handle_quarantine(failed[j].handle, failed[j].file, state->file_mode);
|
|
if (ret == -1) {
|
|
/* LCOV_EXCL_START */
|
|
log_tag("%s:%" PRIu64 ":%s:%s: Quarantine error. %s.\n", es(errno), i, failed[j].disk->name, esc_tag(failed[j].file->sub), strerror(errno));
|
|
log_fatal_errno(errno, failed[j].disk->name);
|
|
log_fatal(errno, "Stopping at block %" PRIu64 "\n", i);
|
|
++unrecoverable_error;
|
|
goto bail;
|
|
/* LCOV_EXCL_STOP */
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
/*
|
|
* repair() returning success means that a RAID solution was validated under
|
|
* one of the parity-history hypotheses. It does not necessarily mean that
|
|
* every bad unsynced block was recovered to its CURRENT version.
|
|
*
|
|
* A bad block marked RECOVERY_OUTOFDATE is therefore still an unrecoverable current
|
|
* data error even though the stripe-level RAID recovery succeeded.
|
|
*/
|
|
int partial_recover_error = 0;
|
|
|
|
/* print a list of all the errors in files */
|
|
for (j = 0; j < failed_count; ++j) {
|
|
if (failed[j].is_bad && failed[j].recovery == RECOVERY_OUTOFDATE) {
|
|
++partial_recover_error;
|
|
log_tag("unrecoverable:%" PRIu64 ":%s:%s: Unrecoverable unsynced error at position %" PRIu64 "\n", i, failed[j].disk->name, esc_tag(failed[j].file->sub), failed[j].file_pos);
|
|
}
|
|
}
|
|
if (partial_recover_error != 0) {
|
|
silent_error += partial_recover_error;
|
|
++unrecoverable_error;
|
|
}
|
|
|
|
/*
|
|
* Check parities, but only if all the blocks have it computed and it's used.
|
|
*
|
|
* If you check/fix after a partial sync, it's OK to have parity errors
|
|
* on the blocks with invalid parity and doesn't make sense to try to fix it.
|
|
*
|
|
* It's also OK to have data errors on unused parity, because sync doesn't
|
|
* update it.
|
|
*/
|
|
if (used_parity && valid_parity) {
|
|
/* check the parity */
|
|
for (l = 0; l < state->level; ++l) {
|
|
if (buffer_recov[l] != 0 && memcmp(buffer_recov[l], buffer[diskmax + l], state->block_size) != 0) {
|
|
unsigned diff = memdiff(buffer_recov[l], buffer[diskmax + l], state->block_size);
|
|
|
|
/* mark that the read parity is wrong, setting ptr to 0 */
|
|
buffer_recov[l] = 0;
|
|
|
|
log_tag("parity_error_data:%" PRIu64 ":%s: Data error, diff parity bits %u/%u\n", i, lev_config_name(l), diff, state->block_size * 8);
|
|
++silent_error;
|
|
}
|
|
}
|
|
}
|
|
|
|
/* update the fixed files */
|
|
if (file_recover(state, fix, partial, i, failed, failed_count, buffer, &recovered_error) != 0) {
|
|
++unrecoverable_error;
|
|
goto bail;
|
|
}
|
|
|
|
if (fix) {
|
|
/*
|
|
* Update parity only if all the blocks have it computed and it's used.
|
|
*
|
|
* If you check/fix after a partial sync, you do not want to fix parity
|
|
* for blocks that are going to have it computed in the sync completion.
|
|
*
|
|
* For unused parity there is no need to write or allocate it because no live
|
|
* file depends on that position and its physical contents do not matter.
|
|
*/
|
|
if (used_parity && valid_parity) {
|
|
/* update the parity */
|
|
for (l = 0; l < state->level; ++l) {
|
|
/* if the parity on disk is wrong */
|
|
if (buffer_recov[l] == 0
|
|
/* and we have access at the parity */
|
|
&& parity[l] != 0
|
|
/* and the parity is not excluded */
|
|
&& !state->parity[l].is_excluded_by_filter
|
|
) {
|
|
ret = parity_write(parity[l], i, buffer[diskmax + l], state->block_size, state->opt.skip_fallocate);
|
|
if (ret == -1) {
|
|
/* LCOV_EXCL_START */
|
|
log_tag("parity_%s:%" PRIu64 ":%s: Write error. %s.\n", es(errno), i, lev_config_name(l), strerror(errno));
|
|
log_fatal_errno(errno, lev_config_name(l));
|
|
log_fatal(errno, "Stopping at block %" PRIu64 "\n", i);
|
|
|
|
++unrecoverable_error;
|
|
goto bail;
|
|
/* LCOV_EXCL_STOP */
|
|
}
|
|
|
|
log_tag("parity_fixed:%" PRIu64 ":%s: Fixed data error\n", i, lev_config_name(l));
|
|
++recovered_error;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
/*
|
|
* If we are not checking, we just set the DAMAGED flag
|
|
* to report that the file is damaged, and we don't know if we can fix it
|
|
*/
|
|
for (j = 0; j < failed_count; ++j) {
|
|
if (failed[j].is_bad) {
|
|
file_flag_set(failed[j].file, FILE_IS_DAMAGED);
|
|
}
|
|
}
|
|
}
|
|
|
|
/* post process the files */
|
|
ret = file_post(state, fix, partial, i, handle, diskmax);
|
|
if (ret == -1) {
|
|
/* LCOV_EXCL_START */
|
|
log_fatal(errno, "Stopping at block %" PRIu64 "\n", i);
|
|
|
|
++unrecoverable_error;
|
|
goto bail;
|
|
/* LCOV_EXCL_STOP */
|
|
}
|
|
|
|
/* count the number of processed block */
|
|
++countpos;
|
|
|
|
/* progress */
|
|
if (state_progress(state, 0, i, countpos, countmax, countsize)) {
|
|
/* LCOV_EXCL_START */
|
|
goto end;
|
|
/* LCOV_EXCL_STOP */
|
|
}
|
|
|
|
/* thermal control */
|
|
if (state_thermal_alarm(state)) {
|
|
/* until now is misc */
|
|
state_usage_misc(state);
|
|
|
|
state_progress_stop(state);
|
|
|
|
state_thermal_cooldown(state);
|
|
|
|
state_progress_restart(state);
|
|
|
|
/* drop until now */
|
|
state_usage_waste(state);
|
|
|
|
if (os_signal_interrupt())
|
|
goto end;
|
|
}
|
|
}
|
|
|
|
/*
|
|
* These objects have no parity block position, so an explicit -S/-B block
|
|
* range cannot select them. Check/fix them separately after block processing,
|
|
* including during an otherwise partial block-range operation.
|
|
*/
|
|
/* for each disk, recover empty files, symlinks and empty dirs */
|
|
for (i = 0; i < diskmax; ++i) {
|
|
tommy_node* node;
|
|
struct snapraid_disk* disk;
|
|
|
|
if (!handle[i].disk)
|
|
continue;
|
|
|
|
/* for each empty file in the disk */
|
|
disk = handle[i].disk;
|
|
node = disk->filelist;
|
|
while (node) {
|
|
char path[PATH_MAX];
|
|
struct stat st;
|
|
struct snapraid_file* file;
|
|
int unsuccessful = 0;
|
|
int unrecoverable = 0;
|
|
|
|
file = node->data;
|
|
node = node->next; /* next node */
|
|
|
|
/* if not empty, it's already checked and continue to the next one */
|
|
if (file->size != 0) {
|
|
continue;
|
|
}
|
|
|
|
/* if excluded continue to the next one */
|
|
if (file_flag_has(file, FILE_IS_EXCLUDED)) {
|
|
continue;
|
|
}
|
|
|
|
/* lstat the file */
|
|
pathprint(path, sizeof(path), "%s%s", disk->dir, file->sub);
|
|
ret = lstat(path, &st);
|
|
if (ret == -1) {
|
|
unsuccessful = 1;
|
|
if (errno != ENOENT)
|
|
unrecoverable = 1;
|
|
|
|
log_error(errno, "Error stating empty file '%s'. %s.\n", path, strerror(errno));
|
|
log_tag("empty_%s:%s:%s: Empty file stat error\n", es(errno), disk->name, esc_tag(file->sub));
|
|
|
|
if (is_hw(errno)) {
|
|
++io_error;
|
|
} else {
|
|
++soft_error;
|
|
}
|
|
} else if (!S_ISREG(st.st_mode)) {
|
|
unsuccessful = 1;
|
|
unrecoverable = 1;
|
|
|
|
log_error(ESOFT, "Error stating empty file '%s' for not regular file.\n", path);
|
|
log_tag("empty_error:%s:%s: Empty file error for not regular file\n", disk->name, esc_tag(file->sub));
|
|
++soft_error;
|
|
#if HAVE_LSTAT_SYNC
|
|
/* NTFS hard-link directory entries may report a stale size. */
|
|
} else if (lstat_sync(path, &st, 0) != 0) {
|
|
unsuccessful = 1;
|
|
unrecoverable = 1;
|
|
|
|
log_error(errno, "Error stating empty file '%s'. %s.\n", path, strerror(errno));
|
|
log_tag("empty_%s:%s:%s: Empty file stat error\n", es(errno), disk->name, esc_tag(file->sub));
|
|
|
|
if (is_hw(errno)) {
|
|
++io_error;
|
|
} else {
|
|
++soft_error;
|
|
}
|
|
#endif
|
|
} else if (st.st_size != 0) {
|
|
unsuccessful = 1;
|
|
|
|
log_error(ESOFT, "Error stating empty file '%s' for not empty file.\n", path);
|
|
log_tag("empty_error:%s:%s: Empty file error for size '%" PRIu64 "'\n", disk->name, esc_tag(file->sub), (uint64_t)st.st_size);
|
|
++soft_error;
|
|
}
|
|
|
|
/*
|
|
* In check mode a type mismatch is still potentially recoverable if the
|
|
* conflicting object is removed manually. In fix mode, however, we don't
|
|
* remove objects of a different type automatically.
|
|
*/
|
|
if (fix && unrecoverable)
|
|
++unrecoverable_error;
|
|
|
|
if (fix && unsuccessful && !unrecoverable) {
|
|
int f;
|
|
|
|
/* create the ancestor directories */
|
|
ret = mkancestor(path);
|
|
if (ret != 0) {
|
|
/* LCOV_EXCL_START */
|
|
log_fatal(errno, "Error creating ancestor '%s%s'. %s.\n", disk->dir, file->sub, strerror(errno));
|
|
log_tag("empty_%s:%s:%s: Create ancestor error. %s.\n", es(errno), disk->name, esc_tag(file->sub), strerror(errno));
|
|
log_fatal_errno(errno, disk->name);
|
|
log_fatal(errno, "Stopping\n");
|
|
|
|
++unrecoverable_error;
|
|
goto bail;
|
|
/* LCOV_EXCL_STOP */
|
|
}
|
|
|
|
/*
|
|
* Create it
|
|
* O_NOFOLLOW: do not follow links to ensure to open the real file
|
|
*/
|
|
f = open(path, O_WRONLY | O_CREAT | O_TRUNC | O_BINARY | O_NOFOLLOW, 0600);
|
|
if (f == -1) {
|
|
/* LCOV_EXCL_START */
|
|
log_fatal(errno, "Error creating '%s%s'. %s.\n", disk->dir, file->sub, strerror(errno));
|
|
log_tag("empty_%s:%s:%s: Create error. %s.\n", es(errno), disk->name, esc_tag(file->sub), strerror(errno));
|
|
log_fatal_errno(errno, disk->name);
|
|
log_fatal(errno, "Stopping\n");
|
|
|
|
++unrecoverable_error;
|
|
goto bail;
|
|
/* LCOV_EXCL_STOP */
|
|
}
|
|
|
|
/* set the original modification time */
|
|
ret = fmtime(f, file->mtime_sec, file->mtime_nsec);
|
|
if (ret != 0) {
|
|
/* LCOV_EXCL_START */
|
|
log_fatal(errno, "Error timing '%s%s'. %s.\n", disk->dir, file->sub, strerror(errno));
|
|
log_tag("empty_%s:%s:%s: Time error. %s.\n", es(errno), disk->name, esc_tag(file->sub), strerror(errno));
|
|
log_fatal_errno(errno, disk->name);
|
|
log_fatal(errno, "Stopping\n");
|
|
|
|
close(f);
|
|
|
|
++unrecoverable_error;
|
|
goto bail;
|
|
/* LCOV_EXCL_STOP */
|
|
}
|
|
|
|
/* close it */
|
|
ret = close(f);
|
|
if (ret != 0) {
|
|
/* LCOV_EXCL_START */
|
|
log_fatal(errno, "Error closing '%s%s'. %s.\n", disk->dir, file->sub, strerror(errno));
|
|
log_tag("empty_%s:%s:%s: Close error. %s.\n", es(errno), disk->name, esc_tag(file->sub), strerror(errno));
|
|
log_fatal_errno(errno, disk->name);
|
|
log_fatal(errno, "Stopping\n");
|
|
|
|
++unrecoverable_error;
|
|
goto bail;
|
|
/* LCOV_EXCL_STOP */
|
|
}
|
|
|
|
log_tag("empty_fixed:%s:%s: Fixed empty file\n", disk->name, esc_tag(file->sub));
|
|
++recovered_error;
|
|
|
|
log_tag("status:recovered:%s:%s\n", disk->name, esc_tag(file->sub));
|
|
msg_info("recovered %s\n", fmt_term(disk, file->sub));
|
|
}
|
|
}
|
|
|
|
/* for each link in the disk */
|
|
disk = handle[i].disk;
|
|
node = disk->linklist;
|
|
while (node) {
|
|
char path[PATH_MAX];
|
|
char pathto[PATH_MAX];
|
|
char linkto[PATH_MAX];
|
|
struct stat st;
|
|
struct stat stto;
|
|
struct snapraid_link* slink;
|
|
int unsuccessful = 0;
|
|
int unrecoverable = 0;
|
|
|
|
slink = node->data;
|
|
node = node->next; /* next node */
|
|
|
|
/* if excluded continue to the next one */
|
|
if (link_flag_has(slink, FILE_IS_EXCLUDED)) {
|
|
continue;
|
|
}
|
|
|
|
if (link_flag_has(slink, FILE_IS_HARDLINK)) {
|
|
/* stat the hardlink itself without following symbolic links */
|
|
pathprint(path, sizeof(path), "%s%s", disk->dir, slink->sub);
|
|
ret = lstat(path, &st);
|
|
if (ret == -1) {
|
|
unsuccessful = 1;
|
|
|
|
log_error(errno, "Error stating hardlink '%s'. %s.\n", path, strerror(errno));
|
|
log_tag("hardlink_%s:%s:%s:%s: Hardlink stat error. %s.\n", es(errno), disk->name, esc_tag(slink->sub), esc_tag(slink->linkto), strerror(errno));
|
|
|
|
if (is_hw(errno)) {
|
|
++io_error;
|
|
} else {
|
|
++soft_error;
|
|
}
|
|
} else if (!S_ISREG(st.st_mode)) {
|
|
unsuccessful = 1;
|
|
|
|
log_error(ESOFT, "Error stating hardlink '%s' for not regular file.\n", path);
|
|
log_tag("hardlink_error:%s:%s:%s: Hardlink error for not regular file\n", disk->name, esc_tag(slink->sub), esc_tag(slink->linkto));
|
|
++soft_error;
|
|
#if HAVE_LSTAT_SYNC
|
|
/* Windows lstat doesn't report the file identity required to validate hardlinks. */
|
|
} else if (lstat_sync(path, &st, 0) != 0) {
|
|
unsuccessful = 1;
|
|
|
|
log_error(errno, "Error stating hardlink '%s'. %s.\n", path, strerror(errno));
|
|
log_tag("hardlink_%s:%s:%s:%s: Hardlink stat error. %s.\n", es(errno), disk->name, esc_tag(slink->sub), esc_tag(slink->linkto), strerror(errno));
|
|
|
|
if (is_hw(errno)) {
|
|
++io_error;
|
|
} else {
|
|
++soft_error;
|
|
}
|
|
#endif
|
|
}
|
|
|
|
/* stat the target itself without following symbolic links */
|
|
pathprint(pathto, sizeof(pathto), "%s%s", disk->dir, slink->linkto);
|
|
ret = lstat(pathto, &stto);
|
|
if (ret == -1) {
|
|
unsuccessful = 1;
|
|
|
|
if (errno == ENOENT) {
|
|
unrecoverable = 1;
|
|
if (fix) {
|
|
/*
|
|
* If the target doesn't exist, it's unrecoverable
|
|
* because we cannot create an hardlink of a file that
|
|
* doesn't exists
|
|
* but in check, we can assume that fixing will recover
|
|
* such missing file, so we assume a less drastic error
|
|
*/
|
|
++unrecoverable_error;
|
|
}
|
|
}
|
|
|
|
log_error(errno, "Error stating hardlink-to '%s'. %s.\n", pathto, strerror(errno));
|
|
log_tag("hardlink_%s:%s:%s:%s: Hardlink to stat error. %s.\n", es(errno), disk->name, esc_tag(slink->sub), esc_tag(slink->linkto), strerror(errno));
|
|
|
|
if (is_hw(errno)) {
|
|
++io_error;
|
|
} else {
|
|
++soft_error;
|
|
}
|
|
} else if (!S_ISREG(stto.st_mode)) {
|
|
unsuccessful = 1;
|
|
unrecoverable = 1;
|
|
|
|
log_error(ESOFT, "Error stating hardlink-to '%s' for not regular file.\n", pathto);
|
|
log_tag("hardlink_error:%s:%s:%s: Hardlink-to error for not regular file\n", disk->name, esc_tag(slink->sub), esc_tag(slink->linkto));
|
|
++soft_error;
|
|
if (fix)
|
|
++unrecoverable_error;
|
|
#if HAVE_LSTAT_SYNC
|
|
/* Windows lstat doesn't report the file identity required to validate hardlinks. */
|
|
} else if (lstat_sync(pathto, &stto, 0) != 0) {
|
|
unsuccessful = 1;
|
|
|
|
log_error(errno, "Error stating hardlink-to '%s'. %s.\n", pathto, strerror(errno));
|
|
log_tag("hardlink_%s:%s:%s:%s: Hardlink to stat error. %s.\n", es(errno), disk->name, esc_tag(slink->sub), esc_tag(slink->linkto), strerror(errno));
|
|
|
|
if (is_hw(errno)) {
|
|
++io_error;
|
|
} else {
|
|
++soft_error;
|
|
}
|
|
#endif
|
|
} else if (!unsuccessful && st.st_ino != INODE_INVALID && stto.st_ino != INODE_INVALID && (st.st_dev != stto.st_dev || st.st_ino != stto.st_ino)) {
|
|
unsuccessful = 1;
|
|
|
|
log_error(ESOFT, "Mismatch hardlink '%s' and '%s'. Different file identity.\n", path, pathto);
|
|
log_tag("hardlink_error:%s:%s:%s: Hardlink mismatch for different file identity\n", disk->name, esc_tag(slink->sub), esc_tag(slink->linkto));
|
|
++soft_error;
|
|
}
|
|
} else {
|
|
/* read the symlink */
|
|
pathprint(path, sizeof(path), "%s%s", disk->dir, slink->sub);
|
|
ret = readlink(path, linkto, sizeof(linkto));
|
|
if (ret < 0) {
|
|
unsuccessful = 1;
|
|
|
|
log_error(errno, "Error reading symlink '%s'. %s.\n", path, strerror(errno));
|
|
log_tag("symlink_%s:%s:%s: Symlink read error. %s.\n", es(errno), disk->name, esc_tag(slink->sub), strerror(errno));
|
|
|
|
if (is_hw(errno)) {
|
|
++io_error;
|
|
} else {
|
|
++soft_error;
|
|
}
|
|
} else if (ret >= PATH_MAX) {
|
|
unsuccessful = 1;
|
|
|
|
log_error(ESOFT, "Error reading symlink '%s'. Symlink too long.\n", path);
|
|
log_tag("symlink_error:%s:%s: Symlink too long\n", disk->name, esc_tag(slink->sub));
|
|
++soft_error;
|
|
} else {
|
|
linkto[ret] = 0;
|
|
|
|
if (strcmp(linkto, slink->linkto) != 0) {
|
|
unsuccessful = 1;
|
|
|
|
log_tag("symlink_error:%s:%s: Symlink data error '%s' instead of '%s'\n", disk->name, esc_tag(slink->sub), linkto, slink->linkto);
|
|
++soft_error;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (fix && unsuccessful && !unrecoverable) {
|
|
const char* link = link_flag_has(slink, FILE_IS_HARDLINK) ? "hard" : "sym";
|
|
|
|
/* create the ancestor directories */
|
|
ret = mkancestor(path);
|
|
if (ret != 0) {
|
|
/* LCOV_EXCL_START */
|
|
log_fatal(errno, "Error creating ancestor '%s%s'. %s.\n", disk->dir, slink->sub, strerror(errno));
|
|
log_tag("%slink_%s:%s:%s: Create ancestor error. %s.\n", link, es(errno), disk->name, esc_tag(slink->sub), strerror(errno));
|
|
log_fatal_errno(errno, disk->name);
|
|
log_fatal(errno, "Stopping\n");
|
|
|
|
++unrecoverable_error;
|
|
goto bail;
|
|
/* LCOV_EXCL_STOP */
|
|
}
|
|
|
|
/* if it exists, it must be deleted before recreating */
|
|
ret = remove(path);
|
|
if (ret != 0 && errno != ENOENT) {
|
|
/* LCOV_EXCL_START */
|
|
log_fatal(errno, "Error removing '%s%s'. %s.\n", disk->dir, slink->sub, strerror(errno));
|
|
log_tag("%slink_%s:%s:%s: Remove error. %s.\n", link, es(errno), disk->name, esc_tag(slink->sub), strerror(errno));
|
|
log_fatal_errno(errno, disk->name);
|
|
log_fatal(errno, "Stopping\n");
|
|
|
|
++unrecoverable_error;
|
|
goto bail;
|
|
/* LCOV_EXCL_STOP */
|
|
}
|
|
|
|
/* create it */
|
|
if (link_flag_has(slink, FILE_IS_HARDLINK)) {
|
|
ret = hardlink(pathto, path);
|
|
if (ret != 0) {
|
|
/* LCOV_EXCL_START */
|
|
log_fatal(errno, "Error writing hardlink '%s' to '%s'. %s.\n", path, pathto, strerror(errno));
|
|
log_tag("hardlink_%s:%s:%s: Hardlink error. %s.\n", es(errno), disk->name, esc_tag(slink->sub), strerror(errno));
|
|
log_fatal_errno(errno, disk->name);
|
|
log_fatal(errno, "Stopping\n");
|
|
|
|
++unrecoverable_error;
|
|
goto bail;
|
|
/* LCOV_EXCL_STOP */
|
|
}
|
|
|
|
log_tag("hardlink_fixed:%s:%s: Fixed hardlink error\n", disk->name, esc_tag(slink->sub));
|
|
++recovered_error;
|
|
} else {
|
|
ret = symlink(slink->linkto, path);
|
|
if (ret != 0) {
|
|
/* LCOV_EXCL_START */
|
|
log_fatal(errno, "Error writing symlink '%s' to '%s'. %s.\n", path, slink->linkto, strerror(errno));
|
|
log_tag("symlink_%s:%s:%s: Hardlink error. %s.\n", es(errno), disk->name, esc_tag(slink->sub), strerror(errno));
|
|
log_fatal_errno(errno, disk->name);
|
|
log_fatal(errno, "Stopping\n");
|
|
|
|
++unrecoverable_error;
|
|
goto bail;
|
|
/* LCOV_EXCL_STOP */
|
|
}
|
|
|
|
log_tag("symlink_fixed:%s:%s: Fixed symlink error\n", disk->name, esc_tag(slink->sub));
|
|
++recovered_error;
|
|
}
|
|
|
|
log_tag("status:recovered:%s:%s\n", disk->name, esc_tag(slink->sub));
|
|
msg_info("recovered %s\n", fmt_term(disk, slink->sub));
|
|
}
|
|
}
|
|
|
|
/* for each dir in the disk */
|
|
disk = handle[i].disk;
|
|
node = disk->dirlist;
|
|
while (node) {
|
|
char path[PATH_MAX];
|
|
struct stat st;
|
|
struct snapraid_dir* dir;
|
|
int unsuccessful = 0;
|
|
int unrecoverable = 0;
|
|
|
|
dir = node->data;
|
|
node = node->next; /* next node */
|
|
|
|
/* if excluded continue to the next one */
|
|
if (dir_flag_has(dir, FILE_IS_EXCLUDED)) {
|
|
continue;
|
|
}
|
|
|
|
/* lstat the dir */
|
|
pathprint(path, sizeof(path), "%s%s", disk->dir, dir->sub);
|
|
ret = lstat(path, &st);
|
|
if (ret == -1) {
|
|
unsuccessful = 1;
|
|
if (errno != ENOENT)
|
|
unrecoverable = 1;
|
|
|
|
log_error(errno, "Error stating dir '%s'. %s.\n", path, strerror(errno));
|
|
log_tag("dir_%s:%s:%s: Dir stat error. %s.\n", es(errno), disk->name, esc_tag(dir->sub), strerror(errno));
|
|
|
|
if (is_hw(errno)) {
|
|
++io_error;
|
|
} else {
|
|
++soft_error;
|
|
}
|
|
} else if (!S_ISDIR(st.st_mode)) {
|
|
unsuccessful = 1;
|
|
unrecoverable = 1;
|
|
|
|
log_tag("dir_error:%s:%s: Dir error for not directory\n", disk->name, esc_tag(dir->sub));
|
|
++soft_error;
|
|
}
|
|
|
|
/*
|
|
* In check mode a type mismatch is still potentially recoverable if the
|
|
* conflicting object is removed manually. In fix mode, however, we don't
|
|
* remove objects of a different type automatically.
|
|
*/
|
|
if (fix && unrecoverable)
|
|
++unrecoverable_error;
|
|
|
|
if (fix && unsuccessful && !unrecoverable) {
|
|
/* create the ancestor directories */
|
|
ret = mkancestor(path);
|
|
if (ret != 0) {
|
|
/* LCOV_EXCL_START */
|
|
log_fatal(errno, "Error creating ancestor '%s%s'. %s.\n", disk->dir, dir->sub, strerror(errno));
|
|
log_tag("dir_%s:%s:%s: Create ancestor error. %s.\n", es(errno), disk->name, esc_tag(dir->sub), strerror(errno));
|
|
log_fatal_errno(errno, disk->name);
|
|
log_fatal(errno, "Stopping\n");
|
|
|
|
++unrecoverable_error;
|
|
goto bail;
|
|
/* LCOV_EXCL_STOP */
|
|
}
|
|
|
|
/* create it */
|
|
ret = mkdir(path, S_IRWXU | S_IRGRP | S_IXGRP | S_IROTH | S_IXOTH);
|
|
if (ret != 0 && errno != EEXIST) {
|
|
/* LCOV_EXCL_START */
|
|
log_fatal(errno, "Error creating directory '%s%s'. %s.\n", disk->dir, dir->sub, strerror(errno));
|
|
log_tag("dir_%s:%s:%s: Create directory error. %s.\n", es(errno), disk->name, esc_tag(dir->sub), strerror(errno));
|
|
log_fatal_errno(errno, disk->name);
|
|
log_fatal(errno, "Stopping\n");
|
|
|
|
++unrecoverable_error;
|
|
goto bail;
|
|
/* LCOV_EXCL_STOP */
|
|
}
|
|
|
|
log_tag("dir_fixed:%s:%s: Fixed dir error\n", disk->name, esc_tag(dir->sub));
|
|
++recovered_error;
|
|
|
|
log_tag("status:recovered:%s:%s\n", disk->name, esc_tag(dir->sub));
|
|
msg_info("recovered %s\n", fmt_term(disk, dir->sub));
|
|
}
|
|
}
|
|
}
|
|
|
|
end:
|
|
state_progress_end(state, countpos, countmax, countsize, "Nothing to check.\n");
|
|
|
|
bail:
|
|
/* close all the files left open */
|
|
for (j = 0; j < diskmax; ++j) {
|
|
struct snapraid_file* file = handle[j].file;
|
|
struct snapraid_disk* disk = handle[j].disk;
|
|
ret = handle_close(&handle[j]);
|
|
if (ret == -1) {
|
|
/* LCOV_EXCL_START */
|
|
/*
|
|
* If handle_close fails, the handle was open (f != -1), which
|
|
* guarantees that both file and disk pointers are valid.
|
|
*/
|
|
log_tag("%s::%s:%s: Close error. %s.\n", es(errno), disk->name, esc_tag(file->sub), strerror(errno));
|
|
log_fatal_errno(errno, disk->name);
|
|
|
|
++unrecoverable_error;
|
|
/* continue, as we are already exiting */
|
|
/* LCOV_EXCL_STOP */
|
|
}
|
|
}
|
|
|
|
if (soft_error || io_error || silent_error || recovered_error || unrecoverable_error) {
|
|
msg_status("\n");
|
|
msg_status("%8u soft errors\n", soft_error);
|
|
msg_status("%8u io errors\n", io_error);
|
|
msg_status("%8u data errors\n", silent_error);
|
|
if (fix) {
|
|
msg_status("%8u recovered errors\n", recovered_error);
|
|
}
|
|
if (unrecoverable_error) {
|
|
msg_status("%8u UNRECOVERABLE errors\n", unrecoverable_error);
|
|
} else {
|
|
/* without checking, we don't know if they are really recoverable or not */
|
|
if (!state->opt.auditonly)
|
|
msg_status("%8u unrecoverable errors\n", unrecoverable_error);
|
|
}
|
|
}
|
|
|
|
if ((soft_error || io_error || silent_error) && !fix) {
|
|
if (soft_error)
|
|
log_fatal(ESOFT, "WARNING! There are soft errors!\n");
|
|
if (io_error)
|
|
log_fatal(EIO, "DANGER! Unexpected input/output errors!\n");
|
|
if (silent_error)
|
|
log_fatal(EDATA, "DANGER! Unexpected silent data errors!\n");
|
|
}
|
|
if (unrecoverable_error)
|
|
log_fatal(ESOFT, "DANGER! Unrecoverable errors detected!\n");
|
|
|
|
log_tag("summary:error_soft:%u\n", soft_error);
|
|
log_tag("summary:error_io:%u\n", io_error);
|
|
log_tag("summary:error_data:%u\n", silent_error);
|
|
if (fix)
|
|
log_tag("summary:error_recovered:%u\n", recovered_error);
|
|
if (!state->opt.auditonly)
|
|
log_tag("summary:error_unrecoverable:%u\n", unrecoverable_error);
|
|
if (fix) {
|
|
if (soft_error + io_error + silent_error + recovered_error + unrecoverable_error == 0)
|
|
log_tag("summary:exit:ok\n");
|
|
else if (unrecoverable_error == 0)
|
|
log_tag("summary:exit:recovered\n");
|
|
else
|
|
log_tag("summary:exit:unrecoverable\n");
|
|
} else if (!state->opt.auditonly) {
|
|
if (soft_error + io_error + silent_error + unrecoverable_error == 0)
|
|
log_tag("summary:exit:ok\n");
|
|
else if (unrecoverable_error == 0)
|
|
log_tag("summary:exit:recoverable\n");
|
|
else
|
|
log_tag("summary:exit:unrecoverable\n");
|
|
} else { /* audit only */
|
|
if (soft_error + silent_error + io_error == 0)
|
|
log_tag("summary:exit:ok\n");
|
|
else if (silent_error + io_error == 0)
|
|
log_tag("summary:exit:warning\n");
|
|
else
|
|
log_tag("summary:exit:error\n");
|
|
}
|
|
log_flush();
|
|
|
|
free(failed);
|
|
free(failed_map);
|
|
free(block_enabled);
|
|
free(handle);
|
|
free(buffer_alloc);
|
|
free(buffer);
|
|
|
|
bw_done(&bw);
|
|
|
|
/* fail if some error are present after the run */
|
|
if (fix) {
|
|
if (state->opt.expect_unrecoverable) {
|
|
if (unrecoverable_error == 0)
|
|
return -1;
|
|
} else {
|
|
if (unrecoverable_error != 0)
|
|
return -1;
|
|
}
|
|
} else {
|
|
if (state->opt.expect_unrecoverable) {
|
|
if (unrecoverable_error == 0)
|
|
return -1;
|
|
} else if (state->opt.expect_recoverable) {
|
|
if (unrecoverable_error != 0)
|
|
return -1;
|
|
if (soft_error + silent_error + io_error == 0)
|
|
return -1;
|
|
} else {
|
|
if (unrecoverable_error != 0)
|
|
return -1;
|
|
if (soft_error + silent_error + io_error != 0)
|
|
return -1;
|
|
}
|
|
}
|
|
|
|
if (alert < 0)
|
|
return -1;
|
|
|
|
return 0;
|
|
}
|
|
|
|
int state_check(struct snapraid_state* state, int fix, block_off_t blockstart, block_off_t blockcount)
|
|
{
|
|
block_off_t blockmax;
|
|
data_off_t size;
|
|
int ret;
|
|
int partial;
|
|
struct snapraid_parity_handle parity[LEV_MAX];
|
|
struct snapraid_parity_handle* parity_ptr[LEV_MAX];
|
|
unsigned process_error;
|
|
unsigned l;
|
|
|
|
msg_progress("Initializing...\n");
|
|
|
|
/*
|
|
* -S/--start and -B/--count allow check/fix to operate on an arbitrary
|
|
* parity range instead of processing from the beginning. In particular,
|
|
* recovery of a later section can be retried while an earlier section is
|
|
* missing or damaged.
|
|
*
|
|
* A partial fix therefore does not establish that preceding or following
|
|
* parity is valid; it covers only the requested range.
|
|
*
|
|
* Keep partial based on the explicit -S/-B request rather than the clipped
|
|
* range: a nonzero -B remains partial even if it reaches the end of parity.
|
|
*/
|
|
partial = blockstart != 0 || blockcount != 0;
|
|
|
|
/*
|
|
* Fix is best-effort on each stripe. If a stripe cannot be recovered, it
|
|
* is left unfixed and processing continues with the following stripes.
|
|
* Consequently, repaired parity is not guaranteed to be contiguous: an
|
|
* unrecoverable stripe may remain as a hole between successfully repaired
|
|
* stripes. This is intentional, as stopping would prevent recovery of all
|
|
* subsequent recoverable stripes.
|
|
*/
|
|
|
|
blockmax = parity_allocated_size(state);
|
|
size = blockmax * state->block_size;
|
|
|
|
if (blockstart > blockmax) {
|
|
/* LCOV_EXCL_START */
|
|
log_fatal(EUSER, "Error in the specified starting block %" PRIu64 ". It's larger than the parity size %" PRIu64 ".\n", blockstart, blockmax);
|
|
exit(EXIT_FAILURE);
|
|
/* LCOV_EXCL_STOP */
|
|
}
|
|
|
|
/* adjust the number of block to process */
|
|
if (blockcount != 0 && blockcount < blockmax - blockstart) {
|
|
blockmax = blockstart + blockcount;
|
|
}
|
|
|
|
if (fix) {
|
|
/*
|
|
* If fixing, create the file and open for writing
|
|
* if it fails, we cannot continue
|
|
*/
|
|
for (l = 0; l < state->level; ++l) {
|
|
/* skip parity disks that are not accessible */
|
|
if (state->parity[l].skip_access) {
|
|
parity_ptr[l] = 0;
|
|
continue;
|
|
}
|
|
|
|
parity_ptr[l] = &parity[l];
|
|
|
|
/* if the parity is excluded */
|
|
if (state->parity[l].is_excluded_by_filter) {
|
|
/*
|
|
* Excluded means "do not modify", not "do not use".
|
|
*
|
|
* An excluded parity can still provide a recovery equation or the independent
|
|
* parity check required when recovered data has no trustworthy hash, so keep
|
|
* it available read-only whenever possible.
|
|
*/
|
|
ret = parity_open(parity_ptr[l], &state->parity[l], l, state->file_mode, state->block_size, state->opt.parity_limit_size);
|
|
if (ret == -1) {
|
|
log_tag("parity_%s:%u:%s: Open error. %s.\n", es(errno), 0, lev_config_name(l), strerror(errno));
|
|
if (is_hw(errno)) {
|
|
log_fatal_errno(errno, lev_config_name(l));
|
|
exit(EXIT_FAILURE);
|
|
}
|
|
|
|
/* continue anyway */
|
|
parity_ptr[l] = 0;
|
|
}
|
|
} else {
|
|
/* open for writing */
|
|
ret = parity_create(parity_ptr[l], &state->parity[l], l, state->file_mode, state->block_size, state->opt.parity_limit_size);
|
|
if (ret == -1) {
|
|
/* LCOV_EXCL_START */
|
|
log_tag("parity_%s:%u:%s: Create error. %s.\n", es(errno), 0, lev_config_name(l), strerror(errno));
|
|
log_fatal_errno(errno, lev_config_name(l));
|
|
exit(EXIT_FAILURE);
|
|
/* LCOV_EXCL_STOP */
|
|
}
|
|
|
|
/*
|
|
* Fix restores the physical state described by the content file.
|
|
*
|
|
* For an existing parity level, preserve every persisted split boundary
|
|
* while limiting the physical layout to the parity extent actually required
|
|
* by the content. Preceding splits needed to reach an existing later split
|
|
* are restored immediately; the remaining tail grows lazily as repaired
|
|
* parity blocks are written.
|
|
*
|
|
* A new parity level has no persisted layout yet (PARITY_SIZE_INVALID), so
|
|
* its split layout must instead be allocated from scratch.
|
|
*/
|
|
if (state->parity[l].split_map[0].size == PARITY_SIZE_INVALID)
|
|
ret = parity_chsize(parity_ptr[l], &state->parity[l], 0, size, state->block_size, state->opt.skip_fallocate, state->opt.skip_space_holder);
|
|
else
|
|
ret = parity_restore(parity_ptr[l], size, state->block_size, state->opt.skip_fallocate);
|
|
if (ret == -1) {
|
|
/* LCOV_EXCL_START */
|
|
log_tag("parity_%s:%u:%s: Create error. %s.\n", es(errno), 0, lev_config_name(l), strerror(errno));
|
|
log_fatal_errno(errno, lev_config_name(l));
|
|
exit(EXIT_FAILURE);
|
|
/* LCOV_EXCL_STOP */
|
|
}
|
|
}
|
|
}
|
|
} else if (!state->opt.auditonly) {
|
|
/*
|
|
* If checking, open the file for reading
|
|
* it may fail if the file doesn't exist, in this case we continue to check the files
|
|
*/
|
|
for (l = 0; l < state->level; ++l) {
|
|
parity_ptr[l] = &parity[l];
|
|
ret = parity_open(parity_ptr[l], &state->parity[l], l, state->file_mode, state->block_size, state->opt.parity_limit_size);
|
|
if (ret == -1) {
|
|
log_tag("parity_%s:%u:%s: Open error. %s.\n", es(errno), 0, lev_config_name(l), strerror(errno));
|
|
if (is_hw(errno)) {
|
|
log_fatal_errno(errno, lev_config_name(l));
|
|
exit(EXIT_FAILURE);
|
|
}
|
|
|
|
msg_status("No accessible %s file, only files will be checked.\n", lev_name(l));
|
|
|
|
/* continue anyway */
|
|
parity_ptr[l] = 0;
|
|
}
|
|
}
|
|
} else {
|
|
/* otherwise don't use any parity */
|
|
for (l = 0; l < state->level; ++l)
|
|
parity_ptr[l] = 0;
|
|
}
|
|
|
|
process_error = 0;
|
|
|
|
/* skip degenerated cases of empty parity, or skipping all */
|
|
if (blockstart < blockmax) {
|
|
ret = state_check_process(state, fix, parity_ptr, blockstart, blockmax, partial);
|
|
if (ret == -1) {
|
|
/* LCOV_EXCL_START */
|
|
++process_error;
|
|
/* continue, as we are already exiting */
|
|
/* LCOV_EXCL_STOP */
|
|
}
|
|
}
|
|
|
|
/* try to close only if opened */
|
|
for (l = 0; l < state->level; ++l) {
|
|
if (parity_ptr[l]) {
|
|
ret = parity_close(parity_ptr[l]);
|
|
if (ret == -1) {
|
|
/* LCOV_EXCL_START */
|
|
log_tag("parity_%s::%s: Close error. %s.\n", es(errno), lev_config_name(l), strerror(errno));
|
|
log_fatal_errno(errno, lev_config_name(l));
|
|
|
|
++process_error;
|
|
/* continue, as we are already exiting */
|
|
/* LCOV_EXCL_STOP */
|
|
}
|
|
}
|
|
}
|
|
|
|
if (process_error != 0)
|
|
return -1;
|
|
|
|
if (log_hardware_errors() != 0) {
|
|
msg_status("Everything done, but hardware errors were encountered in the process\n");
|
|
return -1;
|
|
}
|
|
|
|
msg_status("Everything OK\n");
|
|
|
|
return 0;
|
|
}
|
|
|