AOSP: ANDROID: e2fsck: Handle casefolded encryption

Adds support for EXT2_HASH_SIPHASH, and reading the hash from disk in
that case. We cannot compute the siphash without the key, so we must
not modify the names of any encrypted and casefolded directories,
which limits some recovery options, and we must assume the hashes
stored in dirents are correct.
This is in preparation for upcoming kernel support for encryption and
casefolding at the same time.

Google-Bug-Id: 138322712
Test: Create fs with casefold and encryption enabled via mke2fs and
      tune2fs, run fsck after creating casefolded + encrypted folder

Change-Id: Icca32d7d9dd3c7f52da03d60e4d89273cbec0a7d
From AOSP commit: 67eae926bdac1a54dbb8335731c5e1581f93e4bb
This commit is contained in:
Daniel Rosenberg 2020-03-13 16:08:52 -07:00 committed by Theodore Ts'o
parent b22c36e60c
commit 61421ee588
8 changed files with 181 additions and 30 deletions

View File

@ -158,6 +158,10 @@ errcode_t e2fsck_reset_context(e2fsck_t ctx)
ext2fs_u32_list_free(ctx->encrypted_dirs);
ctx->encrypted_dirs = 0;
}
if (ctx->casefolded_dirs) {
ext2fs_u32_list_free(ctx->casefolded_dirs);
ctx->casefolded_dirs = 0;
}
if (ctx->inode_count) {
ext2fs_free_icount(ctx->inode_count);
ctx->inode_count = 0;

View File

@ -390,6 +390,7 @@ struct e2fsck_struct {
profile_t profile;
int blocks_per_page;
ext2_u32_list encrypted_dirs;
ext2_u32_list casefolded_dirs;
/* Reserve blocks for root and l+f re-creation */
blk64_t root_repair_block, lnf_repair_block;

View File

@ -79,6 +79,7 @@ static void alloc_bb_map(e2fsck_t ctx);
static void alloc_imagic_map(e2fsck_t ctx);
static void mark_inode_bad(e2fsck_t ctx, ino_t ino);
static void add_encrypted_dir(e2fsck_t ctx, ino_t ino);
static void add_casefolded_dir(e2fsck_t ctx, ino_t ino);
static void handle_fs_bad_blocks(e2fsck_t ctx);
static void process_inodes(e2fsck_t ctx, char *block_buf);
static EXT2_QSORT_TYPE process_inode_cmp(const void *a, const void *b);
@ -1890,6 +1891,8 @@ void e2fsck_pass1(e2fsck_t ctx)
ctx->fs_directory_count++;
if (inode->i_flags & EXT4_ENCRYPT_FL)
add_encrypted_dir(ctx, ino);
if (inode->i_flags & EXT4_CASEFOLD_FL)
add_casefolded_dir(ctx, ino);
} else if (LINUX_S_ISREG (inode->i_mode)) {
ext2fs_mark_inode_bitmap2(ctx->inode_reg_map, ino);
ctx->fs_regular_count++;
@ -2220,6 +2223,24 @@ error:
ctx->flags |= E2F_FLAG_ABORT;
}
static void add_casefolded_dir(e2fsck_t ctx, ino_t ino)
{
struct problem_context pctx;
if (!ctx->casefolded_dirs) {
pctx.errcode = ext2fs_u32_list_create(&ctx->casefolded_dirs, 0);
if (pctx.errcode)
goto error;
}
pctx.errcode = ext2fs_u32_list_add(ctx->casefolded_dirs, ino);
if (pctx.errcode == 0)
return;
error:
fix_problem(ctx, PR_1_ALLOCATE_CASEFOLDED_DIRLIST, &pctx);
/* Should never get here */
ctx->flags |= E2F_FLAG_ABORT;
}
/*
* This procedure will allocate the inode "bb" (badblock) map table
*/
@ -2677,9 +2698,20 @@ static int handle_htree(e2fsck_t ctx, struct problem_context *pctx,
if ((root->hash_version != EXT2_HASH_LEGACY) &&
(root->hash_version != EXT2_HASH_HALF_MD4) &&
(root->hash_version != EXT2_HASH_TEA) &&
(root->hash_version != EXT2_HASH_SIPHASH) &&
fix_problem(ctx, PR_1_HTREE_HASHV, pctx))
return 1;
if (ext4_hash_in_dirent(inode)) {
if (root->hash_version != EXT2_HASH_SIPHASH &&
fix_problem(ctx, PR_1_HTREE_NEEDS_SIPHASH, pctx))
return 1;
} else {
if (root->hash_version == EXT2_HASH_SIPHASH &&
fix_problem(ctx, PR_1_HTREE_CANNOT_SIPHASH, pctx))
return 1;
}
if ((root->unused_flags & EXT2_HASH_FLAG_INCOMPAT) &&
fix_problem(ctx, PR_1_HTREE_INCOMPAT, pctx))
return 1;

View File

@ -289,6 +289,10 @@ void e2fsck_pass2(e2fsck_t ctx)
ext2fs_u32_list_free(ctx->encrypted_dirs);
ctx->encrypted_dirs = 0;
}
if (ctx->casefolded_dirs) {
ext2fs_u32_list_free(ctx->casefolded_dirs);
ctx->casefolded_dirs = 0;
}
clear_problem_context(&pctx);
if (ctx->large_files) {
@ -706,7 +710,8 @@ static void salvage_directory(ext2_filsys fs,
struct ext2_dir_entry *dirent,
struct ext2_dir_entry *prev,
unsigned int *offset,
unsigned int block_len)
unsigned int block_len,
int hash_in_dirent)
{
char *cp = (char *) dirent;
int left;
@ -731,7 +736,8 @@ static void salvage_directory(ext2_filsys fs,
* Special case of directory entry of size 8: copy what's left
* of the directory block up to cover up the invalid hole.
*/
if ((left >= 12) && (rec_len == EXT2_DIR_ENTRY_HEADER_LEN)) {
if ((left >= ext2fs_dir_rec_len(1, hash_in_dirent)) &&
(rec_len == EXT2_DIR_ENTRY_HEADER_LEN)) {
memmove(cp, cp+EXT2_DIR_ENTRY_HEADER_LEN, left);
memset(cp + left, 0, EXT2_DIR_ENTRY_HEADER_LEN);
return;
@ -743,7 +749,7 @@ static void salvage_directory(ext2_filsys fs,
*/
if ((left < 0) &&
((int) rec_len + left > EXT2_DIR_ENTRY_HEADER_LEN) &&
((int) name_len + EXT2_DIR_ENTRY_HEADER_LEN <= (int) rec_len + left) &&
((int) ext2fs_dir_rec_len(name_len, hash_in_dirent) <= (int) rec_len + left) &&
dirent->inode <= fs->super->s_inodes_count &&
strnlen(dirent->name, name_len) == name_len) {
(void) ext2fs_set_rec_len(fs, (int) rec_len + left, dirent);
@ -933,6 +939,8 @@ static int check_dir_block(ext2_filsys fs,
size_t inline_data_size = 0;
int filetype = 0;
int encrypted = 0;
int hash_in_dirent = 0;
int casefolded = 0;
size_t max_block_size;
int hash_flags = 0;
static char *eop_read_dirblock = NULL;
@ -1156,6 +1164,9 @@ skip_checksum:
if (ctx->encrypted_dirs)
encrypted = ext2fs_u32_list_test(ctx->encrypted_dirs, ino);
if (ctx->casefolded_dirs)
casefolded = ext2fs_u32_list_test(ctx->casefolded_dirs, ino);
hash_in_dirent = encrypted && casefolded;
dict_init(&de_dict, DICTCOUNT_T_MAX, dict_de_cmp);
prev = 0;
@ -1163,6 +1174,9 @@ skip_checksum:
dgrp_t group;
ext2_ino_t first_unused_inode;
unsigned int name_len;
/* csum entry is not checked here, so don't worry about it */
int extended = (dot_state > 1) && hash_in_dirent;
int min_dir_len = ext2fs_dir_rec_len(1, extended);
problem = 0;
if (!inline_data_size || dot_state > 1) {
@ -1172,15 +1186,16 @@ skip_checksum:
* force salvaging this dir.
*/
if (max_block_size - offset < EXT2_DIR_ENTRY_HEADER_LEN)
rec_len = EXT2_DIR_REC_LEN(1);
rec_len = ext2fs_dir_rec_len(1, extended);
else
(void) ext2fs_get_rec_len(fs, dirent, &rec_len);
cd->pctx.dirent = dirent;
cd->pctx.num = offset;
if ((offset + rec_len > max_block_size) ||
(rec_len < 12) ||
(rec_len < min_dir_len) ||
((rec_len % 4) != 0) ||
(((unsigned) ext2fs_dirent_name_len(dirent) + EXT2_DIR_ENTRY_HEADER_LEN) > rec_len)) {
((ext2fs_dir_rec_len(ext2fs_dirent_name_len(dirent),
extended)) > rec_len)) {
if (fix_problem(ctx, PR_2_DIR_CORRUPTED,
&cd->pctx)) {
#ifdef WORDS_BIGENDIAN
@ -1213,7 +1228,8 @@ skip_checksum:
#endif
salvage_directory(fs, dirent, prev,
&offset,
max_block_size);
max_block_size,
hash_in_dirent);
#ifdef WORDS_BIGENDIAN
if (need_reswab) {
(void) ext2fs_get_rec_len(fs,
@ -1435,10 +1451,17 @@ skip_checksum:
if (dx_dir->casefolded_hash)
hash_flags = EXT4_CASEFOLD_FL;
ext2fs_dirhash2(dx_dir->hashversion, dirent->name,
ext2fs_dirent_name_len(dirent),
fs->encoding, hash_flags,
fs->super->s_hash_seed, &hash, 0);
if (dx_dir->hashversion == EXT2_HASH_SIPHASH) {
if (dot_state > 1)
hash = EXT2_DIRENT_HASH(dirent);
} else {
ext2fs_dirhash2(dx_dir->hashversion,
dirent->name,
ext2fs_dirent_name_len(dirent),
fs->encoding, hash_flags,
fs->super->s_hash_seed,
&hash, 0);
}
if (hash < dx_db->min_hash)
dx_db->min_hash = hash;
if (hash > dx_db->max_hash)

View File

@ -1253,6 +1253,16 @@ static struct e2fsck_problem problem_table[] = {
N_("@d %p has the casefold flag, but the\ncasefold feature is not enabled. "),
PROMPT_CLEAR_FLAG, 0, 0, 0, 0 },
/* Htree directory should use SipHash but does not */
{ PR_1_HTREE_NEEDS_SIPHASH,
N_("@h %i uses hash version (%N), but should use SipHash (6) \n"),
PROMPT_CLEAR_HTREE, PR_PREEN_OK, 0, 0, 0 },
/* Htree directory uses SipHash but should not */
{ PR_1_HTREE_CANNOT_SIPHASH,
N_("@h %i uses SipHash, but should not. "),
PROMPT_CLEAR_HTREE, PR_PREEN_OK, 0, 0, 0 },
/* Pass 1b errors */
/* Pass 1B: Rescan for duplicate/bad blocks */

View File

@ -701,6 +701,15 @@ struct problem_context {
/* Casefold flag set, but file system is missing the casefold feature */
#define PR_1_CASEFOLD_FEATURE 0x010089
/* Error allocating memory for casefolded directory list */
#define PR_1_ALLOCATE_CASEFOLDED_DIRLIST 0x01008C
/* Htree directory should use SipHash but does not */
#define PR_1_HTREE_NEEDS_SIPHASH 0x01008D
/* Htree directory uses SipHash but should not */
#define PR_1_HTREE_CANNOT_SIPHASH 0x01008E
/*
* Pass 1b errors

View File

@ -101,6 +101,21 @@ struct out_dir {
ext2_dirhash_t *hashes;
};
#define DOTDOT_OFFSET 12
static int is_fake_entry(ext2_filsys fs, int lblk, unsigned int offset)
{
/* Entries in the first block before this value refer to . or .. */
if (lblk == 0 && offset <= DOTDOT_OFFSET)
return 1;
/* Check if this is likely the csum entry */
if (ext2fs_has_feature_metadata_csum(fs->super) &&
(offset & (fs->blocksize - 1)) ==
fs->blocksize - sizeof(struct ext2_dir_entry_tail))
return 1;
return 0;
}
static int fill_dir_block(ext2_filsys fs,
blk64_t *block_nr,
e2_blkcnt_t blockcnt,
@ -113,7 +128,7 @@ static int fill_dir_block(ext2_filsys fs,
struct ext2_dir_entry *dirent;
char *dir;
unsigned int offset, dir_offset, rec_len, name_len;
int hash_alg, hash_flags;
int hash_alg, hash_flags, hash_in_entry;
if (blockcnt < 0)
return 0;
@ -140,6 +155,7 @@ static int fill_dir_block(ext2_filsys fs,
return BLOCK_ABORT;
}
hash_flags = fd->inode->i_flags & EXT4_CASEFOLD_FL;
hash_in_entry = ext4_hash_in_dirent(fd->inode);
hash_alg = fs->super->s_def_hash_version;
if ((hash_alg <= EXT2_HASH_TEA) &&
(fs->super->s_flags & EXT2_FLAGS_UNSIGNED_HASH))
@ -147,13 +163,18 @@ static int fill_dir_block(ext2_filsys fs,
/* While the directory block is "hot", index it. */
dir_offset = 0;
while (dir_offset < fs->blocksize) {
int min_rec = EXT2_DIR_ENTRY_HEADER_LEN;
int extended = hash_in_entry && !is_fake_entry(fs, blockcnt, dir_offset);
if (extended)
min_rec += EXT2_DIR_ENTRY_HASH_LEN;
dirent = (struct ext2_dir_entry *) (dir + dir_offset);
(void) ext2fs_get_rec_len(fs, dirent, &rec_len);
name_len = ext2fs_dirent_name_len(dirent);
if (((dir_offset + rec_len) > fs->blocksize) ||
(rec_len < 8) ||
(rec_len < min_rec) ||
((rec_len % 4) != 0) ||
(name_len + 8 > rec_len)) {
(name_len + min_rec > rec_len)) {
fd->err = EXT2_ET_DIR_CORRUPTED;
return BLOCK_ABORT;
}
@ -187,11 +208,14 @@ static int fill_dir_block(ext2_filsys fs,
}
ent = fd->harray + fd->num_array++;
ent->dir = dirent;
fd->dir_size += EXT2_DIR_REC_LEN(name_len);
fd->dir_size += ext2fs_dir_rec_len(name_len, extended);
ent->ino = dirent->inode;
if (fd->compress)
if (extended) {
ent->hash = EXT2_DIRENT_HASH(dirent);
ent->minor_hash = EXT2_DIRENT_MINOR_HASH(dirent);
} else if (fd->compress) {
ent->hash = ent->minor_hash = 0;
else {
} else {
fd->err = ext2fs_dirhash2(hash_alg,
dirent->name, name_len,
fs->encoding, hash_flags,
@ -473,6 +497,8 @@ static errcode_t copy_dir_entries(e2fsck_t ctx,
ext2_dirhash_t prev_hash;
int csum_size = 0;
struct ext2_dir_entry_tail *t;
int hash_in_entry = ext4_hash_in_dirent(fd->inode);
int min_rec_len = ext2fs_dir_rec_len(1, hash_in_entry);
if (ctx->htree_slack_percentage == 255) {
profile_get_uint(ctx->profile, "options",
@ -501,15 +527,16 @@ static errcode_t copy_dir_entries(e2fsck_t ctx,
prev_rec_len = 0;
rec_len = 0;
left = fs->blocksize - csum_size;
slack = fd->compress ? 12 :
slack = fd->compress ? min_rec_len :
((fs->blocksize - csum_size) * ctx->htree_slack_percentage)/100;
if (slack < 12)
slack = 12;
if (slack < min_rec_len)
slack = min_rec_len;
for (i = 0; i < fd->num_array; i++) {
ent = fd->harray + i;
if (ent->dir->inode == 0)
continue;
rec_len = EXT2_DIR_REC_LEN(ext2fs_dirent_name_len(ent->dir));
rec_len = ext2fs_dir_rec_len(ext2fs_dirent_name_len(ent->dir),
hash_in_entry);
if (rec_len > left) {
if (left) {
left += prev_rec_len;
@ -546,6 +573,11 @@ static errcode_t copy_dir_entries(e2fsck_t ctx,
prev_rec_len = rec_len;
memcpy(dirent->name, ent->dir->name,
ext2fs_dirent_name_len(dirent));
if (hash_in_entry) {
EXT2_DIRENT_HASHES(dirent)->hash = ext2fs_cpu_to_le32(ent->hash);
EXT2_DIRENT_HASHES(dirent)->minor_hash =
ext2fs_cpu_to_le32(ent->minor_hash);
}
offset += rec_len;
left -= rec_len;
if (left < slack) {
@ -570,7 +602,8 @@ static errcode_t copy_dir_entries(e2fsck_t ctx,
static struct ext2_dx_root_info *set_root_node(ext2_filsys fs, char *buf,
ext2_ino_t ino, ext2_ino_t parent)
ext2_ino_t ino, ext2_ino_t parent,
struct ext2_inode *inode)
{
struct ext2_dir_entry *dir;
struct ext2_dx_root_info *root;
@ -598,7 +631,10 @@ static struct ext2_dx_root_info *set_root_node(ext2_filsys fs, char *buf,
root = (struct ext2_dx_root_info *) (buf+24);
root->reserved_zero = 0;
root->hash_version = fs->super->s_def_hash_version;
if (ext4_hash_in_dirent(inode))
root->hash_version = EXT2_HASH_SIPHASH;
else
root->hash_version = fs->super->s_def_hash_version;
root->info_length = 8;
root->indirect_levels = 0;
root->unused_flags = 0;
@ -684,7 +720,8 @@ static int alloc_blocks(ext2_filsys fs,
static errcode_t calculate_tree(ext2_filsys fs,
struct out_dir *outdir,
ext2_ino_t ino,
ext2_ino_t parent)
ext2_ino_t parent,
struct ext2_inode *inode)
{
struct ext2_dx_root_info *root_info;
struct ext2_dx_entry *root, *int_ent, *dx_ent = 0;
@ -693,7 +730,7 @@ static errcode_t calculate_tree(ext2_filsys fs,
int i, c1, c2, c3, nblks;
int limit_offset, int_offset, root_offset;
root_info = set_root_node(fs, outdir->buf, ino, parent);
root_info = set_root_node(fs, outdir->buf, ino, parent, inode);
root_offset = limit_offset = ((char *) root_info - outdir->buf) +
root_info->info_length;
root_limit = (struct ext2_dx_countlimit *) (outdir->buf + limit_offset);
@ -992,7 +1029,7 @@ resort:
if (!fd.compress) {
/* Calculate the interior nodes */
retval = calculate_tree(fs, &outdir, ino, fd.parent);
retval = calculate_tree(fs, &outdir, ino, fd.parent, fd.inode);
if (retval)
goto errout;
}

View File

@ -238,6 +238,7 @@ struct ext2_dx_root_info {
#define EXT2_HASH_LEGACY_UNSIGNED 3 /* reserved for userspace lib */
#define EXT2_HASH_HALF_MD4_UNSIGNED 4 /* reserved for userspace lib */
#define EXT2_HASH_TEA_UNSIGNED 5 /* reserved for userspace lib */
#define EXT2_HASH_SIPHASH 6
#define EXT2_HASH_FLAG_INCOMPAT 0x1
@ -983,6 +984,12 @@ EXT4_FEATURE_INCOMPAT_FUNCS(casefold, 4, CASEFOLD)
#define EXT4_DEFM_DISCARD 0x0400
#define EXT4_DEFM_NODELALLOC 0x0800
static inline int ext4_hash_in_dirent(const struct ext2_inode *inode)
{
return (inode->i_flags & EXT4_ENCRYPT_FL) &&
(inode->i_flags & EXT4_CASEFOLD_FL);
}
/*
* Structure of a directory entry
*/
@ -1017,6 +1024,25 @@ struct ext2_dir_entry_2 {
char name[EXT2_NAME_LEN]; /* File name */
};
/*
* Hashes for ext4_dir_entry for casefolded and ecrypted directories.
* This is located at the first 4 bit aligned location after the name.
*/
struct ext2_dir_entry_hash {
__le32 hash;
__le32 minor_hash;
};
#define EXT2_DIRENT_HASHES(entry) \
((struct ext2_dir_entry_hash *) &entry->name[\
(ext2fs_dirent_name_len(entry) + \
EXT2_DIR_ROUND) & ~EXT2_DIR_ROUND])
#define EXT2_DIRENT_HASH(entry) \
ext2fs_le32_to_cpu(EXT2_DIRENT_HASHES(entry)->hash)
#define EXT2_DIRENT_MINOR_HASH(entry) \
ext2fs_le32_to_cpu(EXT2_DIRENT_HASHES(entry)->minor_hash)
/*
* This is a bogus directory entry at the end of each leaf block that
* records checksums.
@ -1057,12 +1083,21 @@ struct ext2_dir_entry_tail {
* NOTE: It must be a multiple of 4
*/
#define EXT2_DIR_ENTRY_HEADER_LEN 8
#define EXT2_DIR_ENTRY_HASH_LEN 8
#define EXT2_DIR_PAD 4
#define EXT2_DIR_ROUND (EXT2_DIR_PAD - 1)
#define EXT2_DIR_REC_LEN(name_len) (((name_len) + \
EXT2_DIR_ENTRY_HEADER_LEN + \
EXT2_DIR_ROUND) & \
~EXT2_DIR_ROUND)
#define EXT2_DIR_REC_LEN(name_len) ext2fs_dir_rec_len(name_len, 0)
static inline unsigned int ext2fs_dir_rec_len(__u8 name_len,
int extended)
{
int rec_len = (name_len + EXT2_DIR_ENTRY_HEADER_LEN + EXT2_DIR_ROUND);
rec_len &= ~EXT2_DIR_ROUND;
if (extended)
rec_len += EXT2_DIR_ENTRY_HASH_LEN;
return rec_len;
}
/*
* Constants for ext4's extended time encoding