/** * A tool for ext2/ext3/ext4 filesystems that allows to change inode count * without recreating the FS. * * Copyright (c) Vitaliy Filippov 2013+ * License: GNU GPLv2 or later * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ /** * TODO try to not fragment the journal while moving * * The theory isn't that hard: * 1) Determine where we want to move the inode tables: * 1.1) Create a map of non-movable blocks - these are: superblock & group descriptors, * block bitmaps, inode bitmaps, bad blocks, and blocks in the resize inode. * Otherwise speaking, we can move any block that either belongs to inode table * or belongs to any inode other than the resize or bad blocks inode. * 1.2) Using the created map, find place for each group inode table closest to the beginning * of its flex_bg. Save these locations in memory. * 1.3) Free the map. * 2) Only if shrinking: move inodes away from the end of each block group inode table * 2.1) move each inode to the new place, mark new place as occupied, unmark old one * 2.2) remember the old->new inode number mapping * 3) Move data away from extra blocks needed by growing/moved inode tables: * 3.2) Create a map of blocks that we want to free. * 3.3) Iterate through all inodes and move blocks. It may involve overwriting * the whole file extent tree or block mapping. * 4) 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) * 5) Move inode tables. * 6) Unmark old inode table blocks, mark new ones. * 7) Change block group descriptors: bg_inode_table, bg_free_inodes_count, * bg_free_blocks_count, bg_inode_bitmap_csum, bg_itable_unused * 8) 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. */ /** * Need tests for: * - both ext2 and ext4 filesystem images * - inode moving: FS image with many files * - block moving, including extent blocks: big sparse file with many extents * - block moving between different groups * - reallocation (both shrink and grow) in case of big flex_bg when inode * tables don't fit into single block group * - non-optimal reallocation, i.e. when inode block count doesn't change * - reallocation on a bigalloc filesystem */ #define _LARGEFILE_SOURCE #define _LARGEFILE64_SOURCE #include #include #include #include #include #include #include #include #include #include #include #include "bmove.h" #include "check_uninit.h" #include "patch_io.h" #define _(a) (a) #define min(a,b) ((a)<(b)?(a):(b)) // Check our own feature sets for the case of linking with newer libext2fs #define REALLOC_FEATURE_COMPAT_SUPP \ (EXT2_FEATURE_COMPAT_DIR_PREALLOC|\ EXT2_FEATURE_COMPAT_IMAGIC_INODES|\ EXT3_FEATURE_COMPAT_HAS_JOURNAL|\ EXT2_FEATURE_COMPAT_RESIZE_INODE|\ EXT2_FEATURE_COMPAT_DIR_INDEX|\ EXT2_FEATURE_COMPAT_EXT_ATTR) // 'journal_dev' and 'recover' incompat features are unsupported #define REALLOC_FEATURE_INCOMPAT_SUPP \ (EXT2_FEATURE_INCOMPAT_FILETYPE|\ EXT2_FEATURE_INCOMPAT_COMPRESSION|\ EXT2_FEATURE_INCOMPAT_META_BG|\ EXT3_FEATURE_INCOMPAT_EXTENTS|\ EXT4_FEATURE_INCOMPAT_FLEX_BG|\ EXT4_FEATURE_INCOMPAT_MMP|\ EXT4_FEATURE_INCOMPAT_64BIT) #define REALLOC_FEATURE_RO_COMPAT_SUPP \ (EXT2_FEATURE_RO_COMPAT_SPARSE_SUPER|\ EXT4_FEATURE_RO_COMPAT_HUGE_FILE|\ EXT2_FEATURE_RO_COMPAT_LARGE_FILE|\ EXT4_FEATURE_RO_COMPAT_DIR_NLINK|\ EXT4_FEATURE_RO_COMPAT_EXTRA_ISIZE|\ EXT4_FEATURE_RO_COMPAT_METADATA_CSUM|\ EXT4_FEATURE_RO_COMPAT_GDT_CSUM|\ EXT4_FEATURE_RO_COMPAT_BIGALLOC|\ EXT4_FEATURE_RO_COMPAT_QUOTA) // "local data" for the inode reallocation process typedef struct { ext2_filsys fs; int fs_fd; dgrp_t flexbg_size, flex_count; 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; blk64_t *new_itable_loc; // (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; } /* Rewrite extents */ static errcode_t rewrite_extents(ext2_filsys fs, ext2_ino_t ino) { ext2_extent_handle_t handle; struct ext2fs_extent extent; errcode_t errcode; struct ext2_extent_info info; errcode = ext2fs_extent_open(fs, ino, &handle); if (errcode) return errcode; errcode = ext2fs_extent_get(handle, EXT2_EXTENT_ROOT, &extent); if (errcode) goto out; do { errcode = ext2fs_extent_get_info(handle, &info); if (errcode) break; /* * If this is the first extent in an extent block that we * haven't visited, rewrite the extent to force the ETB * checksum to be rewritten. */ if (info.curr_entry == 1 && info.curr_level != 0 && !(extent.e_flags & EXT2_EXTENT_FLAGS_SECOND_VISIT)) { errcode = ext2fs_extent_replace(handle, 0, &extent); if (errcode) break; } /* Skip to the end of a block of leaf nodes */ if (extent.e_flags & EXT2_EXTENT_FLAGS_LEAF) { errcode = ext2fs_extent_get(handle, EXT2_EXTENT_LAST_SIB, &extent); if (errcode) break; } errcode = ext2fs_extent_get(handle, EXT2_EXTENT_NEXT, &extent); } while (errcode == 0); out: /* Ok if we run off the end */ if (errcode == EXT2_ET_EXTENT_NO_NEXT) errcode = 0; ext2fs_extent_free(handle); return errcode; } /** * Move inodes from the end of each block group inode table * so the tables can be shrinked */ errcode_t shrink_move_inodes(realloc_data *rd) { errcode_t retval = 0; int 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; 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); /* Rewrite extent block checksums with new inode number */ if (ext2fs_has_feature_metadata_csum(rd->fs->super) && (inode->i_flags & EXT4_EXTENTS_FL)) { rd->fs->flags |= EXT2_FLAG_IGNORE_CSUM_ERRORS; retval = rewrite_extents(rd->fs, new_ino); rd->fs->flags &= ~EXT2_FLAG_IGNORE_CSUM_ERRORS; if (retval) goto out; } // 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 the new place for each block group inode table * so the tables can be grown or moved. */ errcode_t extend_move_blocks(realloc_data *rd) { ext2fs_block_bitmap reserve_map; dgrp_t grp; errcode_t retval; if (rd->ibg_new == rd->ibg_old) { return 0; } retval = ext2fs_allocate_block_bitmap(rd->fs, "reserved block map", &reserve_map); if (retval) { return retval; } // Mark blocks we want to free as "reserved" // Don't care about which blocks are already used by inode tables, // because ext2fs_move_blocks only moves blocks that belong to inodes. for (grp = 0; grp < rd->fs->group_desc_count; grp++) { ext2fs_mark_block_bitmap_range2(reserve_map, rd->new_itable_loc[grp], rd->ibg_new); } retval = ext2fs_move_blocks(rd->fs, reserve_map, rd->fs->block_map, 0); ext2fs_mark_bb_dirty(rd->fs); ext2fs_flush(rd->fs); 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; } static int fix_dirent_csum_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; ext2fs_dir_block_csum_set(rd->fs, dir, dirent); return DIRENT_CHANGED; } /** * Change inode numbers in all directory entries */ errcode_t 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++) { if (ext2fs_test_inode_bitmap2(rd->fs->inode_map, ino)) { ext2fs_dir_iterate2(rd->fs, ino, 0, 0, change_inode_numbers_callback, rd); } } return 0; } errcode_t fix_dirent_csums(realloc_data *rd) { ext2_ino_t ino; realloc_sort_inode_map(rd); for (ino = 1; ino <= rd->fs->super->s_inodes_count; ino++) { if (ext2fs_test_inode_bitmap2(rd->fs->inode_map, ino)) { ext2fs_dir_iterate2(rd->fs, ino, 0, 0, fix_dirent_csum_callback, rd); } } return 0; } /** * 1) Move inode tables * 2) Mark/unmark new/old inode table blocks * 3) Adjust superblock and block group descriptors */ errcode_t change_super_and_bgd(realloc_data *rd) { blk64_t blk, end; dgrp_t grp, n_grp, flex_grp; __u32 used_ig, used_ibg, i, unus; ext2_ino_t inum; errcode_t retval = 0; int has_gdt_csum = EXT2_HAS_RO_COMPAT_FEATURE(rd->fs->super, EXT4_FEATURE_RO_COMPAT_GDT_CSUM|EXT4_FEATURE_RO_COMPAT_METADATA_CSUM); int cl = EXT2FS_CLUSTER_RATIO(rd->fs); void *buf = NULL, *inode_buf = NULL; ext2fs_flush(rd->fs); retval = ext2fs_get_mem(EXT2_BLOCK_SIZE(rd->fs->super) * rd->ibg_new * rd->flexbg_size, &buf); if (retval) { goto out; } // First mark/unmark inode table blocks (mark and unmark in separate passes) for (grp = 0; grp < rd->fs->group_desc_count; grp++) { // In case of bigalloc, last cluster of previous group inode table // may often overlap with the first cluster of next group one blk = ext2fs_inode_table_loc(rd->fs, grp) & ~(cl-1); if (!ext2fs_test_block_bitmap2(rd->fs->block_map, blk)) { blk += cl; } end = (ext2fs_inode_table_loc(rd->fs, grp) + rd->ibg_old + (cl-1)) & ~(cl-1); for (; blk < end; blk += cl) { ext2fs_block_alloc_stats2(rd->fs, blk, -1); } } for (grp = 0; grp < rd->fs->group_desc_count; grp++) { blk = rd->new_itable_loc[grp] & ~(cl-1); if (grp > 0 && blk == end-cl) { blk += cl; } end = (rd->new_itable_loc[grp] + rd->ibg_new + (cl-1)) & ~(cl-1); for (; blk < end; blk += cl) { if (ext2fs_test_block_bitmap2(rd->fs->block_map, blk)) { printf("BUG: Block %lld (suited for group %u inode table) is still allocated\n", blk, grp); retval = ENOSPC; goto out; } ext2fs_block_alloc_stats2(rd->fs, blk, +1); } } for (flex_grp = 0; flex_grp < rd->flex_count; flex_grp++) { // Read inode tables for a flex_bg grp = flex_grp*rd->flexbg_size; n_grp = min(rd->flexbg_size, rd->fs->group_desc_count-grp); for (i = 0; i < n_grp; i++, grp++) { // Skip unitialized inode table parts used_ibg = rd->ibg_old; used_ig = rd->ig_old; if (has_gdt_csum) { if (ext2fs_bg_flags_test(rd->fs, grp, EXT2_BG_INODE_UNINIT)) { used_ig = used_ibg = 0; } else { used_ig = (rd->ig_old - ext2fs_bg_itable_unused(rd->fs, grp)); used_ibg = (used_ig * EXT2_INODE_SIZE(rd->fs->super)+EXT2_BLOCK_SIZE(rd->fs->super)-1)/EXT2_BLOCK_SIZE(rd->fs->super); } } blk = ext2fs_inode_table_loc(rd->fs, grp); if (used_ibg > 0) { inode_buf = buf + i*rd->ibg_new*EXT2_BLOCK_SIZE(rd->fs->super); retval = io_channel_read_blk64(rd->fs->io, blk, min(used_ibg, rd->ibg_new), inode_buf); if (retval) goto out; if (used_ig < rd->ig_new) { memset(inode_buf + EXT2_INODE_SIZE(rd->fs->super) * used_ig, 0, EXT2_INODE_SIZE(rd->fs->super) * (rd->ig_new - used_ig)); } if (ext2fs_has_feature_metadata_csum(rd->fs->super)) { for (inum = 0; inum < used_ig || inum < rd->ig_new; inum++) { retval = ext2fs_inode_csum_set( rd->fs, 1 + rd->ig_new*grp + inum, inode_buf + EXT2_INODE_SIZE(rd->fs->super) * inum ); if (retval) goto out; } } } } // Write inode tables grp = flex_grp*rd->flexbg_size; for (i = 0; i < n_grp; i++, grp++) { retval = io_channel_write_blk64(rd->fs->io, rd->new_itable_loc[grp], rd->ibg_new, buf + i*rd->ibg_new*EXT2_BLOCK_SIZE(rd->fs->super)); if (retval) { printf("Error moving inode table for block group %u\n", grp); goto out; } // Set inode table location and free inode count ext2fs_inode_table_loc_set(rd->fs, grp, rd->new_itable_loc[grp]); 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_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; 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; } int nonmovable_callback(ext2_filsys fs, blk64_t *blocknr, e2_blkcnt_t blockcnt, blk64_t ref_blk, int ref_offset, void *priv_data) { if (blockcnt >= 0) { ext2fs_mark_block_bitmap2((ext2fs_block_bitmap)priv_data, *blocknr); } return 0; } /** * Allocate new place for all groups' inode tables and remember it. * This is more correct, because allows us to correctly handle situations * when flex_bg is so big that inode tables for all groups in flex_bg * do not fit into its first group, and also allows us to honor bad blocks. */ errcode_t alloc_itables(realloc_data *rd) { errcode_t retval = 0; ext2fs_block_bitmap nonmovable = NULL; dgrp_t grp, flex_grp; int n_grp, i, mod; blk64_t blk, start, end; retval = ext2fs_get_mem(sizeof(blk64_t) * rd->fs->group_desc_count, &rd->new_itable_loc); if (retval) goto out; // Create a map of blocks we can't move retval = ext2fs_allocate_block_bitmap(rd->fs, "non-movable block bitmap", &nonmovable); if (retval < 0) goto out; retval = ext2fs_block_iterate3(rd->fs, EXT2_BAD_INO, 0, NULL, nonmovable_callback, nonmovable); if (retval < 0) goto out; retval = ext2fs_block_iterate3(rd->fs, EXT2_RESIZE_INO, 0, NULL, nonmovable_callback, nonmovable); if (retval < 0) goto out; for (grp = 0; grp < rd->fs->group_desc_count; grp++) { ext2fs_reserve_super_and_bgd(rd->fs, grp, nonmovable); ext2fs_mark_block_bitmap2(nonmovable, ext2fs_block_bitmap_loc(rd->fs, grp)); ext2fs_mark_block_bitmap2(nonmovable, ext2fs_inode_bitmap_loc(rd->fs, grp)); } // flex_bg parameters if (EXT2_HAS_INCOMPAT_FEATURE(rd->fs->super, EXT4_FEATURE_INCOMPAT_FLEX_BG) && rd->fs->super->s_log_groups_per_flex) { rd->flexbg_size = 1 << rd->fs->super->s_log_groups_per_flex; } else { rd->flexbg_size = 1; } rd->flex_count = (rd->fs->group_desc_count + rd->flexbg_size - 1) / rd->flexbg_size; // Allocate inode tables for (flex_grp = 0; flex_grp < rd->flex_count; flex_grp++) { grp = flex_grp*rd->flexbg_size; n_grp = min(rd->flexbg_size, rd->fs->group_desc_count-grp); // TODO We could use a better algorithm that would always try to find // the biggest free sequence of blocks if it can't allocate all inode // tables in sequence start = ext2fs_group_first_block2(rd->fs, grp); end = ext2fs_group_last_block2(rd->fs, grp+n_grp-1); mod = 0; for (i = 0; i < n_grp; i++, grp++) { retval = ext2fs_get_free_blocks2(rd->fs, start, end, rd->ibg_new, nonmovable, &blk); if (retval) goto out; if (start == blk) blk += mod; rd->new_itable_loc[grp] = blk; start = blk + rd->ibg_new; // Allow adjacent inode tables to share one cluster // ext2fs_get_free_blocks2 operates on cluster boundaries, // so we handle the division residue separately mod = start & EXT2FS_CLUSTER_MASK(rd->fs); start = start - mod; } } out: if (nonmovable) ext2fs_free_block_bitmap(nonmovable); return retval; } /** * Main function: change inode number of a filesystem! */ errcode_t do_realloc(realloc_data *rd) { __u32 ig_round; errcode_t retval; int phase = 1; 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); retval = EINVAL; goto out; } 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); } ext2fs_read_bitmaps(rd->fs); // Find where to put the new inode tables retval = alloc_itables(rd); if (retval) { goto out; } 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); retval = ENOSPC; goto out; } printf("Phase %d: Moving inodes out of the way\n", phase++); retval = shrink_move_inodes(rd); if (retval) { goto out; } } 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)); retval = ENOSPC; goto out; } } else if (rd->ibg_new == rd->ibg_old) { printf("The requested number of inodes is equal to current\n"); goto out; } printf("Phase %d: Moving data blocks out of the way\n", phase++); retval = extend_move_blocks(rd); if (retval) { goto out; } printf("Phase %d: Changing all inode numbers\n", phase++); retval = change_inode_numbers(rd); if (retval) { goto out; } printf("Phase %d: Adjusting superblock and block group descriptors\n", phase++); retval = change_super_and_bgd(rd); if (retval) { goto out; } /* printf("Phase %d: Fixing all directory checksums\n", phase++); retval = fix_dirent_csums(rd); if (retval) { goto out; }*/ out: if (rd->new_itable_loc) { ext2fs_free_mem(&rd->new_itable_loc); } if (rd->inode_map) { free(rd->inode_map); rd->inode_map = NULL; } return retval; } __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 make backup before applying changes run:\n" " e2patch backup %s %s %s.backup\n" "Then, to apply the inode change operation to the real filesystem run:\n" " e2patch apply %s %s\n" , name, patch_file, patch_file, 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 stat64 st_buf; for (optind = 1; optind < narg; optind++) { if (!strcmp(args[optind], "--patch")) rd.patch_file = args[++optind]; else if (!strcmp(args[optind], "--force")) force = 1; 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 ] \n\n" "If 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" " 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); } // FIXME Don't know why, but it segfaults if I use ext2fs_fstat here! (of course with ext2fs_struct_stat) retval = fstat64(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) { if ((rd.fs->super->s_state & EXT2_ERROR_FS) || !(rd.fs->super->s_state & EXT2_VALID_FS) || (rd.fs->super->s_feature_incompat & EXT3_FEATURE_INCOMPAT_RECOVER)) { fprintf(stderr, _("Please run 'e2fsck -f %s' first.\n\n"), rd.device_name); goto close_fs; } if (rd.fs->super->s_feature_incompat & ~REALLOC_FEATURE_INCOMPAT_SUPP || rd.fs->super->s_feature_compat & ~REALLOC_FEATURE_COMPAT_SUPP || rd.fs->super->s_feature_ro_compat & ~REALLOC_FEATURE_RO_COMPAT_SUPP) { retval = EXT2_ET_UNSUPP_FEATURE; com_err(program_name, retval, _("while trying to open %s"), 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; }