ext4-realloc-inodes/realloc-inodes.c

678 lines
22 KiB
C

/**
* A tool for ext2/ext3/ext4 filesystems that allows to change inode count
* without recreating it.
*
* TODO bigalloc compatibility
* TODO write some tests: for inode moving (image with many files),
* for block moving, including extent blocks (one sparse file with many extents),
* for block moving between different groups
*
* The theory isn't that hard:
* 1) If shrinking - move inodes away from the end of each block group inode table
* 1.1) move each inode to the new place, mark new place as occupied, unmark old one
* 1.2) remember the old->new inode number mapping
* 2) If growing - move data away from extra blocks needed by growing inode tables:
* 2.1) Create a map of blocks that we want to free
* 2.2) Iterate through all inodes and move remembered blocks.
* It involves overwriting the whole file extent tree or block mapping...
* If some of these blocks are in the bad block inode, we should either
* abort the reallocation process, or move inode tables to another location
* in a block group, possibly first defragmenting it... :-(
* 3) Change all inode numbers in directory entries according to mappings from (1.2),
* and then using a formula: new_num = 1 + ((old_num-1)/old_i_per_g)*new_i_per_g + ((old_num-1) % old_i_per_g)
* 4) Move parts of inode tables so they are consecutive again if flex_bg feature is active
* 5) Mark/unmark extra blocks for inode tables
* 6) Change block group descriptors: bg_inode_table, bg_free_inodes_count,
* bg_free_blocks_count, bg_inode_bitmap_csum, bg_itable_unused
* 7) Change superblock: s_inodes_count, s_free_blocks_count,
* s_free_inodes_count, s_inodes_per_group
*
* This is a highly destructive process and WILL leave a corrupted FS if interrupted.
* So we should provide a rollback method. undo_io_manager is very slow, so instead of it
* we use the "patch" i/o manager that first does not modify the filesystem directly, but
* only writes changed blocks to a separate sparse "patch" file.
*
* The modifications can then be applied to the real filesystem using e2patch utility,
* and if that process gets interrupted you can safely restart it and still get a consistent
* filesystem state.
*
* Moreover, you can create an "undo" ("backup") patch before applying modifications,
* also using e2patch utility.
*/
#include <stdio.h>
#include <fcntl.h>
#include <string.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <sys/time.h>
#include <errno.h>
#include <ext2fs/ext2_fs.h>
#include <ext2fs/ext2fs.h>
#include "bmove.h"
#include "check_uninit.h"
#include "patch_io.h"
#define _(a) (a)
#define min(a,b) ((a)<(b)?(a):(b))
// "local data" for the inode reallocation process
typedef struct
{
ext2_filsys fs;
int fs_fd;
char *device_name, *io_options, *patch_file;
__u32 ig_old, ig_new; // old and new inodes-per-group count
__u32 ibg_old, ibg_new; // old and new inode_blocks-per-group count
__u32 new_inode_count;
// (old->new) inode number map
ext2_ino_t *inode_map;
__u32 inode_map_size, inode_map_alloc;
} realloc_data;
// Utility functions for (old -> new) inode number map
void realloc_add_inode_map(realloc_data *rd, ext2_ino_t old, ext2_ino_t new)
{
if (!old || !new)
{
return;
}
if (2*rd->inode_map_size >= rd->inode_map_alloc)
{
rd->inode_map_alloc += 1024;
rd->inode_map = realloc(rd->inode_map, sizeof(ext2_ino_t) * rd->inode_map_alloc);
}
rd->inode_map[rd->inode_map_size*2] = old;
rd->inode_map[rd->inode_map_size*2+1] = new;
rd->inode_map_size++;
}
int realloc_compare_inode_map_callback(const void *a, const void *b)
{
return *((ext2_ino_t*)a) - *((ext2_ino_t*)b);
}
void realloc_sort_inode_map(realloc_data *rd)
{
if (!rd->inode_map)
{
return;
}
qsort(rd->inode_map, rd->inode_map_size, sizeof(ext2_ino_t)*2, realloc_compare_inode_map_callback);
}
ext2_ino_t realloc_search_inode_map(realloc_data *rd, ext2_ino_t old)
{
__u32 start = 0, end = rd->inode_map_size, cur;
ext2_ino_t cur_ino;
if (!rd->inode_map)
{
return 0;
}
while (end-start > 1)
{
cur = (start+end)>>1;
cur_ino = rd->inode_map[cur<<1];
if (cur_ino < old)
{
start = cur+1;
}
else if (cur_ino > old)
{
end = cur;
}
else
{
return rd->inode_map[(cur<<1)+1];
}
}
if (rd->inode_map[start<<1] == old)
{
return rd->inode_map[(start<<1)+1];
}
return 0;
}
/**
* Move inodes from the end of each block group inode table
* so the tables can be shrinked
*/
int shrink_move_inodes(realloc_data *rd)
{
int retval = 0, inode_size = EXT2_INODE_SIZE(rd->fs->super);
__u32 group, i;
__u32 new_group;
ext2_ino_t ino, new_ino;
struct ext2_inode *inode = NULL;
ext2fs_read_inode_bitmap(rd->fs);
if (retval)
{
return retval;
}
retval = ext2fs_get_mem(inode_size, &inode);
if (retval)
{
return retval;
}
for (group = 0; group < rd->fs->group_desc_count; group++)
{
for (i = rd->ig_new; i < rd->ig_old; i++)
{
ino = 1 + group*rd->ig_old + i;
if (ext2fs_test_inode_bitmap2(rd->fs->inode_map, ino))
{
// Inode is occupied and should be moved
new_group = group;
do
{
retval = ext2fs_find_first_zero_inode_bitmap2(rd->fs->inode_map,
1 + new_group*rd->ig_old,
new_group*rd->ig_old+rd->ig_new, &new_ino);
if (!retval)
{
break;
}
new_group = (new_group+1) % rd->fs->group_desc_count;
} while (new_group != group);
if (retval)
{
// No space to move this inode
goto out;
}
// Copy inode to the new place
check_inode_uninit(rd->fs, rd->fs->inode_map, new_group);
retval = ext2fs_read_inode_full(rd->fs, ino, inode, inode_size);
if (retval)
{
goto out;
}
retval = ext2fs_write_inode_full(rd->fs, new_ino, inode, inode_size);
if (retval)
{
goto out;
}
ext2fs_inode_alloc_stats2(rd->fs, new_ino, 1, inode->i_mode & S_IFDIR);
ext2fs_inode_alloc_stats2(rd->fs, ino, -1, inode->i_mode & S_IFDIR);
// Remember mapping
realloc_add_inode_map(rd, ino, new_ino);
}
}
}
if (rd->inode_map_size)
{
ext2fs_mark_ib_dirty(rd->fs);
}
out:
if (inode)
{
ext2fs_free_mem(&inode);
}
return retval;
}
/**
* Move data blocks from after the end of each block group inode table
* so the tables can be grown
*/
int extend_move_blocks(realloc_data *rd)
{
ext2fs_block_bitmap reserve_map;
blk64_t it_start, blk_diff, b_per_g;
dgrp_t flex_grp, n_grp, flex_count;
int retval, flexbg_size;
if (rd->ibg_new == rd->ibg_old)
{
return 0;
}
blk_diff = rd->ibg_new-rd->ibg_old;
b_per_g = EXT2_BLOCKS_PER_GROUP(rd->fs->super);
retval = ext2fs_allocate_block_bitmap(rd->fs, "reserved block map", &reserve_map);
if (retval)
{
return retval;
}
if (!rd->fs->block_map)
{
ext2fs_read_block_bitmap(rd->fs);
}
// Mark reserved blocks (those we want to free)
if (EXT2_HAS_INCOMPAT_FEATURE(rd->fs->super, EXT4_FEATURE_INCOMPAT_FLEX_BG)
&& rd->fs->super->s_log_groups_per_flex)
{
flexbg_size = 1 << rd->fs->super->s_log_groups_per_flex;
}
else
{
flexbg_size = 1;
}
flex_count = (rd->fs->group_desc_count + flexbg_size - 1) / flexbg_size;
for (flex_grp = 0; flex_grp < flex_count; flex_grp++)
{
n_grp = flexbg_size;
if (flex_grp*flexbg_size+n_grp > rd->fs->group_desc_count)
{
n_grp = rd->fs->group_desc_count-flex_grp*flexbg_size;
}
it_start = ext2fs_inode_table_loc(rd->fs, flex_grp*flexbg_size);
// Check group boundaries (the first group in flex_bg must contain all inode tables)
if ((it_start + rd->ibg_new*n_grp - 1) / b_per_g
!= (it_start + rd->ibg_old*n_grp - 1) / b_per_g)
{
retval = ENOSPC;
goto out;
}
it_start += rd->ibg_old*n_grp;
ext2fs_mark_block_bitmap_range2(reserve_map, it_start, blk_diff*n_grp);
}
retval = ext2fs_move_blocks(rd->fs, reserve_map, rd->fs->block_map, 0);
ext2fs_mark_bb_dirty(rd->fs);
ext2fs_flush(rd->fs);
out:
ext2fs_free_block_bitmap(reserve_map);
return retval;
}
static int change_inode_numbers_callback(ext2_ino_t dir, int entry,
struct ext2_dir_entry *dirent, int offset,
int blocksize, char *buf, void *priv_data)
{
realloc_data *rd = priv_data;
ext2_ino_t new_ino = realloc_search_inode_map(rd, dirent->inode);
if (!new_ino)
{
new_ino = dirent->inode;
}
new_ino = 1 + (new_ino-1)/rd->ig_old*rd->ig_new + (new_ino-1)%rd->ig_old;
if (new_ino != dirent->inode)
{
dirent->inode = new_ino;
return DIRENT_CHANGED;
}
return 0;
}
/**
* Change inode numbers in all directory entries
*/
int change_inode_numbers(realloc_data *rd)
{
ext2_ino_t ino;
realloc_sort_inode_map(rd);
for (ino = 1; ino <= rd->fs->super->s_inodes_count; ino++)
{
ext2fs_dir_iterate2(rd->fs, ino, 0, 0, change_inode_numbers_callback, rd);
}
return 0;
}
/**
* 1) Move inode tables so they are consecutive again if flex_bg is enabled
* 2) Mark/unmark extra inode table blocks
* 3) Adjust superblock and block group descriptors
*/
int change_super_and_bgd(realloc_data *rd)
{
blk64_t it_start, blk;
dgrp_t grp, flex_grp, flex_count;
__u32 unus, used_ibg;
int flexbg_size, n_grp, i, retval = 0;
int has_gdt_csum = EXT2_HAS_RO_COMPAT_FEATURE(rd->fs->super, EXT4_FEATURE_RO_COMPAT_GDT_CSUM);
void *buf = NULL;
ext2fs_flush(rd->fs);
if (!rd->fs->block_map)
{
ext2fs_read_block_bitmap(rd->fs);
}
if (EXT2_HAS_INCOMPAT_FEATURE(rd->fs->super, EXT4_FEATURE_INCOMPAT_FLEX_BG)
&& rd->fs->super->s_log_groups_per_flex)
{
flexbg_size = 1 << rd->fs->super->s_log_groups_per_flex;
}
else
{
flexbg_size = 1;
}
flex_count = (rd->fs->group_desc_count + flexbg_size - 1) / flexbg_size;
retval = ext2fs_get_mem(EXT2_BLOCK_SIZE(rd->fs->super) * rd->ibg_new * flexbg_size, &buf);
if (retval)
{
goto out;
}
for (flex_grp = 0; flex_grp < flex_count; flex_grp++)
{
n_grp = flexbg_size;
if (flex_grp*flexbg_size+n_grp > rd->fs->group_desc_count)
{
n_grp = rd->fs->group_desc_count-flex_grp*flexbg_size;
}
it_start = ext2fs_inode_table_loc(rd->fs, flex_grp*flexbg_size);
if (rd->ibg_new != rd->ibg_old)
{
memset(buf, 0, EXT2_BLOCK_SIZE(rd->fs->super) * rd->ibg_new * n_grp);
// Read inode table(s) while skipping unitialized inode table parts
for (grp = flex_grp*flexbg_size, i = 0; i < n_grp; grp++, i++)
{
used_ibg = rd->ibg_old;
if (has_gdt_csum)
{
if (ext2fs_bg_flags_test(rd->fs, grp, EXT2_BG_INODE_UNINIT))
{
used_ibg = 0;
}
else
{
used_ibg = (rd->ig_old - ext2fs_bg_itable_unused(rd->fs, grp));
used_ibg = (used_ibg * EXT2_INODE_SIZE(rd->fs->super)+EXT2_BLOCK_SIZE(rd->fs->super)-1)/EXT2_BLOCK_SIZE(rd->fs->super);
}
}
if (used_ibg > 0)
{
blk = ext2fs_inode_table_loc(rd->fs, grp);
retval = io_channel_read_blk64(rd->fs->io, blk,
min(used_ibg, rd->ibg_new),
buf + i*rd->ibg_new*EXT2_BLOCK_SIZE(rd->fs->super));
if (retval)
{
goto out;
}
}
}
// Write inode table(s) to the new place
retval = io_channel_write_blk64(rd->fs->io, it_start, rd->ibg_new * n_grp, buf);
if (retval)
{
// Exiting with badly corrupted filesystem :-(
printf("Error moving inode tables for %u groups, starting from %u\n", n_grp, flex_grp*flexbg_size);
goto out;
}
// Mark/unmark extra inode table blocks
if (rd->ibg_new < rd->ibg_old)
{
ext2fs_unmark_block_bitmap_range2(rd->fs->block_map, it_start + rd->ibg_new*n_grp,
(rd->ibg_old-rd->ibg_new)*n_grp);
}
else
{
ext2fs_mark_block_bitmap_range2(rd->fs->block_map, it_start + rd->ibg_old*n_grp,
(rd->ibg_new-rd->ibg_old)*n_grp);
}
}
ext2fs_bg_free_blocks_count_set(rd->fs, flex_grp*flexbg_size,
ext2fs_bg_free_blocks_count(rd->fs, flex_grp*flexbg_size) -
(rd->ibg_new - rd->ibg_old)*n_grp);
// Change inode table locations and free inode counts
for (grp = flex_grp*flexbg_size, i = 0; i < n_grp; grp++, i++)
{
blk = it_start + rd->ibg_new*i;
ext2fs_inode_table_loc_set(rd->fs, grp, blk);
ext2fs_bg_free_inodes_count_set(rd->fs, grp,
ext2fs_bg_free_inodes_count(rd->fs, grp) + rd->ig_new - rd->ig_old);
if (has_gdt_csum)
{
unus = ext2fs_bg_itable_unused(rd->fs, grp);
if (rd->ig_new > rd->ig_old || unus >= rd->ig_old - rd->ig_new)
{
unus += rd->ig_new - rd->ig_old;
}
else
{
unus = 0;
}
ext2fs_bg_itable_unused_set(rd->fs, grp, unus);
ext2fs_bg_flags_clear(rd->fs, grp, EXT2_BG_BLOCK_UNINIT);
ext2fs_group_desc_csum_set(rd->fs, grp);
}
}
}
// Bitmaps never need to be moved because a single bitmap is always a single FS block
ext2fs_mark_bb_dirty(rd->fs);
retval = rd->fs->write_bitmaps(rd->fs);
if (retval)
{
goto out;
}
rd->fs->write_bitmaps = NULL;
// Explicitly set 'overwrite backup superblocks' flag
rd->fs->flags &= ~EXT2_FLAG_MASTER_SB_ONLY;
ext2fs_free_blocks_count_add(rd->fs->super, rd->fs->group_desc_count * (rd->ibg_old - rd->ibg_new));
rd->fs->super->s_free_inodes_count += rd->fs->group_desc_count * (rd->ig_new - rd->ig_old);
rd->fs->super->s_inodes_per_group = rd->ig_new;
rd->fs->super->s_inodes_count = rd->fs->group_desc_count * rd->ig_new;
ext2fs_mark_super_dirty(rd->fs);
if (rd->ig_new > rd->ig_old)
{
// Mark newly allocated inodes as free in the bitmap
__u32 ino;
ext2fs_read_inode_bitmap(rd->fs);
for (grp = 0; grp < rd->fs->group_desc_count; grp++)
{
for (ino = rd->ig_old; ino < rd->ig_new; ino++)
{
ext2fs_unmark_inode_bitmap2(rd->fs->inode_map, 1 + ino + grp*rd->ig_new);
}
}
ext2fs_mark_ib_dirty(rd->fs);
}
out:
if (buf)
{
ext2fs_free_mem(&buf);
}
return retval;
}
/**
* Main function: change inode number of a filesystem!
*/
int do_realloc(realloc_data *rd)
{
__u32 ig_round;
int retval;
rd->ig_old = EXT2_INODES_PER_GROUP(rd->fs->super);
rd->ig_new = rd->new_inode_count / rd->fs->group_desc_count;
// inodes-per-group must be a multiple of 8 so each byte of inode bitmap is filled
rd->ig_new &= ~7;
if (rd->ig_new < 16)
{
printf("Too small number of inodes requested (%u), min inodes per group = 16\n", rd->ig_new);
return ENOENT;
}
rd->ibg_old = rd->fs->inode_blocks_per_group;
rd->ibg_new = (rd->ig_new * EXT2_INODE_SIZE(rd->fs->super) +
EXT2_BLOCK_SIZE(rd->fs->super) - 1) / EXT2_BLOCK_SIZE(rd->fs->super);
if (rd->new_inode_count != rd->ig_new * rd->fs->group_desc_count)
{
printf("Inode count %u rounded down to %u = (%u inodes per group) * (%u block groups)\n",
rd->new_inode_count, rd->ig_new * rd->fs->group_desc_count, rd->ig_new, rd->fs->group_desc_count);
}
rd->new_inode_count = rd->ig_new * rd->fs->group_desc_count;
ig_round = rd->ibg_new * EXT2_BLOCK_SIZE(rd->fs->super) / EXT2_INODE_SIZE(rd->fs->super);
if (rd->ig_new != ig_round)
{
printf("Inode count %u is not optimal because %u inodes per group is not a multiple of %u"
" - there will be wasted space in inode tables. Optimal inode count would be %u.\n",
rd->new_inode_count, rd->ig_new, EXT2_BLOCK_SIZE(rd->fs->super) / EXT2_INODE_SIZE(rd->fs->super), ig_round);
}
if (rd->ig_new < rd->ig_old)
{
if (rd->new_inode_count < rd->fs->super->s_inodes_count - rd->fs->super->s_free_inodes_count)
{
printf("Too small number of inodes requested, existing inodes (%u) won't fit\n",
rd->fs->super->s_inodes_count - rd->fs->super->s_free_inodes_count);
return ENOENT;
}
printf("Phase 1: Moving inodes out of the way\n");
retval = shrink_move_inodes(rd);
if (retval)
{
return retval;
}
}
else if (rd->ig_new > rd->ig_old)
{
blk64_t required_blocks = (rd->ibg_new - rd->ibg_old) * rd->fs->group_desc_count;
if (required_blocks > ext2fs_free_blocks_count(rd->fs->super))
{
printf("Requested number of inodes is too big, it requires at least %llu free blocks, "
"and there are only %llu free blocks available\n",
required_blocks, ext2fs_free_blocks_count(rd->fs->super));
return ENOENT;
}
printf("Phase 1: Moving data blocks out of the way\n");
retval = extend_move_blocks(rd);
if (retval)
{
return retval;
}
}
else
{
printf("The requested number of inodes is equal to current\n");
return 0;
}
printf("Phase 2: Changing all inode numbers\n");
retval = change_inode_numbers(rd);
if (retval)
{
return retval;
}
printf("Phase 3: Adjusting superblock and block group descriptors\n");
retval = change_super_and_bgd(rd);
if (retval)
{
return retval;
}
return 0;
}
__u32 atou(char *s)
{
__u32 x = 0;
if (s[0] == '0')
{
if (s[1] == 'x' || s[1] == 'X')
{
sscanf(s+2, "%x", &x);
}
else
{
sscanf(s+1, "%o", &x);
}
}
else
{
sscanf(s, "%u", &x);
}
return x;
}
const char *program_name = "realloc-inodes";
static int setup_patch_io(char *name, char *patch_file, io_manager *io_ptr)
{
set_patch_io_backing_manager(*io_ptr);
set_patch_io_patch_file(patch_file);
*io_ptr = patch_io_manager;
printf(_(
"To apply the inode change operation to the real filesystem"
" please run the command\n e2patch apply %s %s\n"
), name, patch_file);
return 0;
}
int main(int narg, char **args)
{
realloc_data rd = { 0 };
int optind, retval, io_flags = 0, force = 0;
io_manager io_ptr = unix_io_manager;
struct stat st_buf;
for (optind = 1; optind < narg; optind++)
{
if (!strcmp(args[optind], "--patch"))
rd.patch_file = args[++optind];
else if (args[optind][0] == '-' && args[optind][1] == '-')
{
if (strcmp(args[optind], "--help") != 0)
printf("Unknown option: %s\n", args[optind]);
break;
}
else if (!rd.device_name)
rd.device_name = args[optind];
else
rd.new_inode_count = atou(args[optind++]);
}
if (!rd.device_name || !rd.new_inode_count)
{
printf(
"Change inode count of an ext2/ext3/ext4 filesystem\n"
"License: GNU GPLv2 or later\nCopyright (c) Vitaliy Filippov, 2013+\n\n"
"USAGE: ./realloc-inodes [--patch <patch_file>] <device> <new_inode_count>\n\n"
"If <patch_file> is specified, all modifications are written to it\n"
"instead of directly modifying the filesystem. These modifications\n"
"can then be applied and unapplied to the real filesystem in a safe way.\n"
"<patch_file> should be on a filesystem supporting sparse files.\n"
);
return 0;
}
add_error_table(&et_ext2_error_table);
// Open FS
rd.fs_fd = ext2fs_open_file(rd.device_name, O_RDWR, 0);
if (rd.fs_fd < 0)
{
com_err(program_name, errno, _("while opening %s"), rd.device_name);
exit(1);
}
retval = fstat(rd.fs_fd, &st_buf);
if (retval < 0)
{
com_err(program_name, errno, _("while getting stat information for %s"), rd.device_name);
exit(1);
}
if (!S_ISREG(st_buf.st_mode))
{
close(rd.fs_fd);
rd.fs_fd = -1;
}
rd.io_options = strchr(rd.device_name, '?');
if (rd.io_options)
{
*rd.io_options++ = 0;
}
if (rd.patch_file)
{
setup_patch_io(rd.device_name, rd.patch_file, &io_ptr);
}
io_flags = EXT2_FLAG_64BITS | EXT2_FLAG_RW | EXT2_FLAG_EXCLUSIVE;
retval = ext2fs_open2(rd.device_name, rd.io_options, io_flags, 0, 0, io_ptr, &rd.fs);
if (retval)
{
com_err(program_name, retval, _("while trying to open %s"), rd.device_name);
printf(_("Couldn't find valid filesystem superblock.\n"));
goto close_fd;
}
if (!force && ((rd.fs->super->s_state & EXT2_ERROR_FS) || ((rd.fs->super->s_state & EXT2_VALID_FS) == 0)))
{
fprintf(stderr, _("Please run 'e2fsck -f %s' first.\n\n"), rd.device_name);
goto close_fs;
}
// Call main realloc function
retval = do_realloc(&rd);
if (retval)
{
com_err(program_name, retval, _("while resizing inode count"));
goto close_fs;
}
close_fs:
ext2fs_close(rd.fs);
close_fd:
if (rd.fs_fd > 0)
{
close(rd.fs_fd);
}
return retval;
}