From: Daeho Jeong Roll-forward recovery finds fsynced node blocks by following the chain in the warm node log. It starts from the next free block of the log recorded in the last checkpoint, and each node block has the address of the next one in its footer. f2fs_allocate_data_block() fills the footer after it moves the log to a new segment, so the chain is kept when a segment is full. f2fs_allocate_segment_for_resize() moves the log out of the range to be removed in the middle of a segment. No node block is allocated there, so the last node block in the old segment still points to the next block of the old segment, and nothing points to the new one. The node blocks written after that cannot be found by recovery until the next checkpoint. For example, with F2FS_IOC_RESIZE_FS: - fsync(A) writes a node block to segment X, which is in the range. - resize moves the warm node log from segment X to segment Y. - fsync(B) writes a node block to segment Y and returns 0. - after a sudden power-off, recovery follows the chain in segment X, recovers A and stops there. B is lost. Checkpoint is blocked during the first GC pass of resize, so this lasts until the pass is done. f2fs_ioc_reserve_dev_alias() moves the logs in the same way. Set SBI_NEED_CP when the warm node log is moved, so that fsync writes a checkpoint instead of relying on the chain. The next checkpoint clears the flag and starts a new chain from the new segment. Such an fsync waits for the first GC pass of resize, since checkpoint is blocked until then. An fsync which has already passed need_do_checkpoint() can still write its node blocks after the log is moved. So check the flag again after the node blocks are written, and write a checkpoint in that case, too. Test: four tasks create and fsync files while F2FS_IOC_RESIZE_FS shrinks the filesystem, and the machine is rebooted by sysrq-b 1.5 seconds after the log is moved. Without this patch, all the files whose fsync returned after the move were lost (454 and 437 files in two runs). With this patch, no fsync returned in that window and no file with a returned fsync was lost. The second check was tested with a debug patch which delays an fsync between need_do_checkpoint() and its node writes. Fixes: b4b10061ef98 ("f2fs: refactor resize_fs to avoid meta updates in progress") Cc: stable@vger.kernel.org Signed-off-by: Daeho Jeong --- fs/f2fs/file.c | 8 ++++++++ fs/f2fs/segment.c | 10 +++++++++- 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/fs/f2fs/file.c b/fs/f2fs/file.c index 05061254c3b2..61ed0ad5d29d 100644 --- a/fs/f2fs/file.c +++ b/fs/f2fs/file.c @@ -466,6 +466,14 @@ static int f2fs_do_sync_file(struct file *file, loff_t start, loff_t end, goto sync_nodes; } + /* + * The warm node log can be moved after need_do_checkpoint() above. + * Then the node blocks written here are out of the chain, so write + * a checkpoint. + */ + if (unlikely(is_sbi_flag_set(sbi, SBI_NEED_CP))) + goto go_write; + /* * If it's atomic_write, it's just fine to keep write ordering. So * here we don't need to wait for node write completion, since we use diff --git a/fs/f2fs/segment.c b/fs/f2fs/segment.c index 31ac93a93021..e77a01376288 100644 --- a/fs/f2fs/segment.c +++ b/fs/f2fs/segment.c @@ -3488,9 +3488,17 @@ int f2fs_allocate_segment_for_resize(struct f2fs_sb_info *sbi, int type, unlock: up_write(&SIT_I(sbi)->sentry_lock); - if (segno != curseg->segno) + if (segno != curseg->segno) { f2fs_notice(sbi, "For resize: curseg of type %d: %u ==> %u", type, segno, curseg->segno); + /* + * The chain of fsynced node blocks is cut here, so roll-forward + * recovery cannot find what is fsynced from now on. Let fsync + * write a checkpoint until the next checkpoint is done. + */ + if (type == CURSEG_WARM_NODE) + set_sbi_flag(sbi, SBI_NEED_CP); + } mutex_unlock(&curseg->curseg_mutex); f2fs_up_read(&SM_I(sbi)->curseg_lock); -- 2.56.0.611.g1030f0b940-goog