aboutsummaryrefslogtreecommitdiffstats
path: root/fs
diff options
context:
space:
mode:
authorOmar Sandoval <osandov@osandov.com>2015-02-14 20:08:51 -0500
committerTheodore Ts'o <tytso@mit.edu>2015-02-14 20:08:51 -0500
commit6f30b7e37a8239f9d27db626a1d3427bc7951908 (patch)
tree773aba374bce42581ec3abb2def044827c108ffd /fs
parent2d5b86e048780c5efa7f7d9708815555919e7b05 (diff)
ext4: fix indirect punch hole corruption
Commit 4f579ae7de56 (ext4: fix punch hole on files with indirect mapping) rewrote FALLOC_FL_PUNCH_HOLE for ext4 files with indirect mapping. However, there are bugs in several corner cases. This fixes 5 distinct bugs: 1. When there is at least one entire level of indirection between the start and end of the punch range and the end of the punch range is the first block of its level, we can't return early; we have to free the intervening levels. 2. When the end is at a higher level of indirection than the start and ext4_find_shared returns a top branch for the end, we still need to free the rest of the shared branch it returns; we can't decrement partial2. 3. When a punch happens within one level of indirection, we need to converge on an indirect block that contains the start and end. However, because the branches returned from ext4_find_shared do not necessarily start at the same level (e.g., the partial2 chain will be shallower if the last block occurs at the beginning of an indirect group), the walk of the two chains can end up "missing" each other and freeing a bunch of extra blocks in the process. This mismatch can be handled by first making sure that the chains are at the same level, then walking them together until they converge. 4. When the punch happens within one level of indirection and ext4_find_shared returns a top branch for the start, we must free it, but only if the end does not occur within that branch. 5. When the punch happens within one level of indirection and ext4_find_shared returns a top branch for the end, then we shouldn't free the block referenced by the end of the returned chain (this mirrors the different levels case). Signed-off-by: Omar Sandoval <osandov@osandov.com>
Diffstat (limited to 'fs')
-rw-r--r--fs/ext4/indirect.c105
1 files changed, 71 insertions, 34 deletions
diff --git a/fs/ext4/indirect.c b/fs/ext4/indirect.c
index 36b369697a13..5e7af1c69577 100644
--- a/fs/ext4/indirect.c
+++ b/fs/ext4/indirect.c
@@ -1393,10 +1393,7 @@ end_range:
1393 * to free. Everything was covered by the start 1393 * to free. Everything was covered by the start
1394 * of the range. 1394 * of the range.
1395 */ 1395 */
1396 return 0; 1396 goto do_indirects;
1397 } else {
1398 /* Shared branch grows from an indirect block */
1399 partial2--;
1400 } 1397 }
1401 } else { 1398 } else {
1402 /* 1399 /*
@@ -1427,56 +1424,96 @@ end_range:
1427 /* Punch happened within the same level (n == n2) */ 1424 /* Punch happened within the same level (n == n2) */
1428 partial = ext4_find_shared(inode, n, offsets, chain, &nr); 1425 partial = ext4_find_shared(inode, n, offsets, chain, &nr);
1429 partial2 = ext4_find_shared(inode, n2, offsets2, chain2, &nr2); 1426 partial2 = ext4_find_shared(inode, n2, offsets2, chain2, &nr2);
1430 /* 1427
1431 * ext4_find_shared returns Indirect structure which 1428 /* Free top, but only if partial2 isn't its subtree. */
1432 * points to the last element which should not be 1429 if (nr) {
1433 * removed by truncate. But this is end of the range 1430 int level = min(partial - chain, partial2 - chain2);
1434 * in punch_hole so we need to point to the next element 1431 int i;
1435 */ 1432 int subtree = 1;
1436 partial2->p++; 1433
1437 while ((partial > chain) || (partial2 > chain2)) { 1434 for (i = 0; i <= level; i++) {
1438 /* We're at the same block, so we're almost finished */ 1435 if (offsets[i] != offsets2[i]) {
1439 if ((partial->bh && partial2->bh) && 1436 subtree = 0;
1440 (partial->bh->b_blocknr == partial2->bh->b_blocknr)) { 1437 break;
1441 if ((partial > chain) && (partial2 > chain2)) { 1438 }
1439 }
1440
1441 if (!subtree) {
1442 if (partial == chain) {
1443 /* Shared branch grows from the inode */
1444 ext4_free_branches(handle, inode, NULL,
1445 &nr, &nr+1,
1446 (chain+n-1) - partial);
1447 *partial->p = 0;
1448 } else {
1449 /* Shared branch grows from an indirect block */
1450 BUFFER_TRACE(partial->bh, "get_write_access");
1442 ext4_free_branches(handle, inode, partial->bh, 1451 ext4_free_branches(handle, inode, partial->bh,
1443 partial->p + 1, 1452 partial->p,
1444 partial2->p, 1453 partial->p+1,
1445 (chain+n-1) - partial); 1454 (chain+n-1) - partial);
1446 BUFFER_TRACE(partial->bh, "call brelse");
1447 brelse(partial->bh);
1448 BUFFER_TRACE(partial2->bh, "call brelse");
1449 brelse(partial2->bh);
1450 } 1455 }
1451 return 0;
1452 } 1456 }
1457 }
1458
1459 if (!nr2) {
1453 /* 1460 /*
1454 * Clear the ends of indirect blocks on the shared branch 1461 * ext4_find_shared returns Indirect structure which
1455 * at the start of the range 1462 * points to the last element which should not be
1463 * removed by truncate. But this is end of the range
1464 * in punch_hole so we need to point to the next element
1456 */ 1465 */
1457 if (partial > chain) { 1466 partial2->p++;
1467 }
1468
1469 while (partial > chain || partial2 > chain2) {
1470 int depth = (chain+n-1) - partial;
1471 int depth2 = (chain2+n2-1) - partial2;
1472
1473 if (partial > chain && partial2 > chain2 &&
1474 partial->bh->b_blocknr == partial2->bh->b_blocknr) {
1475 /*
1476 * We've converged on the same block. Clear the range,
1477 * then we're done.
1478 */
1458 ext4_free_branches(handle, inode, partial->bh, 1479 ext4_free_branches(handle, inode, partial->bh,
1459 partial->p + 1, 1480 partial->p + 1,
1460 (__le32 *)partial->bh->b_data+addr_per_block, 1481 partial2->p,
1461 (chain+n-1) - partial); 1482 (chain+n-1) - partial);
1462 BUFFER_TRACE(partial->bh, "call brelse"); 1483 BUFFER_TRACE(partial->bh, "call brelse");
1463 brelse(partial->bh); 1484 brelse(partial->bh);
1464 partial--; 1485 BUFFER_TRACE(partial2->bh, "call brelse");
1486 brelse(partial2->bh);
1487 return 0;
1465 } 1488 }
1489
1466 /* 1490 /*
1467 * Clear the ends of indirect blocks on the shared branch 1491 * The start and end partial branches may not be at the same
1468 * at the end of the range 1492 * level even though the punch happened within one level. So, we
1493 * give them a chance to arrive at the same level, then walk
1494 * them in step with each other until we converge on the same
1495 * block.
1469 */ 1496 */
1470 if (partial2 > chain2) { 1497 if (partial > chain && depth <= depth2) {
1498 ext4_free_branches(handle, inode, partial->bh,
1499 partial->p + 1,
1500 (__le32 *)partial->bh->b_data+addr_per_block,
1501 (chain+n-1) - partial);
1502 BUFFER_TRACE(partial->bh, "call brelse");
1503 brelse(partial->bh);
1504 partial--;
1505 }
1506 if (partial2 > chain2 && depth2 <= depth) {
1471 ext4_free_branches(handle, inode, partial2->bh, 1507 ext4_free_branches(handle, inode, partial2->bh,
1472 (__le32 *)partial2->bh->b_data, 1508 (__le32 *)partial2->bh->b_data,
1473 partial2->p, 1509 partial2->p,
1474 (chain2+n-1) - partial2); 1510 (chain2+n2-1) - partial2);
1475 BUFFER_TRACE(partial2->bh, "call brelse"); 1511 BUFFER_TRACE(partial2->bh, "call brelse");
1476 brelse(partial2->bh); 1512 brelse(partial2->bh);
1477 partial2--; 1513 partial2--;
1478 } 1514 }
1479 } 1515 }
1516 return 0;
1480 1517
1481do_indirects: 1518do_indirects:
1482 /* Kill the remaining (whole) subtrees */ 1519 /* Kill the remaining (whole) subtrees */