xfs: detect and handle invalid iclog size set by mkfs
authorBrian Foster <bfoster@redhat.com>
Mon, 4 Jan 2016 04:55:10 +0000 (15:55 +1100)
committerDave Chinner <david@fromorbit.com>
Mon, 4 Jan 2016 04:55:10 +0000 (15:55 +1100)
XFS log records have separate fields for the record size and the iclog
size used to write the record. mkfs.xfs zeroes the log and writes an
unmount record to generate a clean log for the subsequent mount. The
userspace record logging code has a bug where the iclog size (h_size)
field of the log record is hardcoded to 32k, even if a log stripe unit
is specified. The log record length is correctly extended to the stripe
unit. Since the kernel log recovery code uses the h_size field to
determine the log buffer size, this means that the kernel can attempt to
read/process records larger than the buffer size and overrun the buffer.

This has historically not been a problem because the kernel doesn't
actually run through log recovery in the clean unmount case. Instead,
the kernel detects that a single unmount record exists between the head
and tail and pushes the tail forward such that the log is viewed as
clean (head == tail). Once CRC verification is enabled, however, all
records at the head of the log are verified for CRC errors and thus we
are susceptible to overrun problems if the iclog field is not correct.

While the core problem must be fixed in userspace, this is historical
behavior that must be detected in the kernel to avoid severe side
effects such as memory corruption and crashes. Update the log buffer
size calculation code to detect this condition, warn the user and resize
the log buffer based on the log stripe unit. Return a corruption error
in cases where this does not look like a clean filesystem (i.e., the log
record header indicates more than one operation).

Signed-off-by: Brian Foster <bfoster@redhat.com>
Reviewed-by: Dave Chinner <dchinner@redhat.com>
Signed-off-by: Dave Chinner <david@fromorbit.com>
fs/xfs/xfs_log_recover.c

index c5ecaacdd21833eed42a4852ce16f9a37d5d1bd6..4f880d6cfb93dd21d40aa2ca4c9837c012f5e40e 100644 (file)
@@ -4245,7 +4245,7 @@ xlog_do_recovery_pass(
        xfs_daddr_t             blk_no;
        char                    *offset;
        xfs_buf_t               *hbp, *dbp;
-       int                     error = 0, h_size;
+       int                     error = 0, h_size, h_len;
        int                     bblks, split_bblks;
        int                     hblks, split_hblks, wrapped_hblks;
        struct hlist_head       rhash[XLOG_RHASH_SIZE];
@@ -4274,7 +4274,31 @@ xlog_do_recovery_pass(
                error = xlog_valid_rec_header(log, rhead, tail_blk);
                if (error)
                        goto bread_err1;
+
+               /*
+                * xfsprogs has a bug where record length is based on lsunit but
+                * h_size (iclog size) is hardcoded to 32k. Now that we
+                * unconditionally CRC verify the unmount record, this means the
+                * log buffer can be too small for the record and cause an
+                * overrun.
+                *
+                * Detect this condition here. Use lsunit for the buffer size as
+                * long as this looks like the mkfs case. Otherwise, return an
+                * error to avoid a buffer overrun.
+                */
                h_size = be32_to_cpu(rhead->h_size);
+               h_len = be32_to_cpu(rhead->h_len);
+               if (h_len > h_size) {
+                       if (h_len <= log->l_mp->m_logbsize &&
+                           be32_to_cpu(rhead->h_num_logops) == 1) {
+                               xfs_warn(log->l_mp,
+               "invalid iclog size (%d bytes), using lsunit (%d bytes)",
+                                        h_size, log->l_mp->m_logbsize);
+                               h_size = log->l_mp->m_logbsize;
+                       } else
+                               return -EFSCORRUPTED;
+               }
+
                if ((be32_to_cpu(rhead->h_version) & XLOG_VERSION_2) &&
                    (h_size > XLOG_HEADER_CYCLE_SIZE)) {
                        hblks = h_size / XLOG_HEADER_CYCLE_SIZE;