CVE Candidate: GFS2 Journal Recovery CRC Bypass Leads to Statfs Corruption via Signed Integer Overflow

File: fs/gfs2/recovery.c
Function: __get_log_header()update_statfs_inode()
Severity: High
Type: Integrity Check Bypass + Signed Integer Overflow (CWE-354, CWE-190)


Vulnerability Description

1. CRC Integrity Check Bypass (__get_log_header, line 138)

The GFS2 log header is split into two regions:

  • V1 region (bytes 0 to LH_V1_SIZE): covered by a CRC-32 hash stored in lh_hash.
  • V2 extension region (bytes LH_V1_SIZE+4 to end of block): covered by a CRC-32C stored in lh_crc.

The V2 extension contains lh_local_total, lh_local_free, and lh_local_dinodes — the statfs delta fields applied to the master statfs inode during journal recovery.

The CRC validation for the V2 region is:

// recovery.c:135-139
crc = crc32c(~0, (void *)lh + LH_V1_SIZE + 4,
             sdp->sd_sb.sb_bsize - LH_V1_SIZE - 4);

if ((lh->lh_crc != 0 && be32_to_cpu(lh->lh_crc) != crc))
    return 1;

The condition short-circuits when lh->lh_crc == 0: the check is entirely skipped. An attacker who can write a crafted GFS2 filesystem image simply sets lh_crc = 0 to bypass the integrity check on all V2 extension fields, including the three statfs delta counters.

The lh_hash check (V1 region, line 132) still applies, but it does not cover lh_local_total/free/dinodes. Those fields are completely unprotected when lh_crc = 0.

2. Signed Integer Overflow in Statfs Update (update_statfs_inode, lines 325–327)

After the CRC bypass, the attacker-controlled values flow into:

// recovery.c:324-328
gfs2_statfs_change_in(&sc, bh->b_data + sizeof(struct gfs2_dinode));
sc.sc_total   += head->lh_local_total;
sc.sc_free    += head->lh_local_free;
sc.sc_dinodes += head->lh_local_dinodes;
gfs2_statfs_change_out(&sc, bh->b_data + sizeof(struct gfs2_dinode));

sc.sc_total, sc.sc_free, and sc.sc_dinodes are all s64 (signed 64-bit). Adding attacker-controlled lh_local_* values (which are decoded as s64 via be64_to_cpu) with no bounds checking causes signed integer overflow, which is undefined behavior in C and can produce arbitrary corrupted values.

The corrupted values are then written directly back to the master statfs inode buffer and synced to disk via gfs2_inode_metasync.


Attack Scenario

An attacker who can present a crafted GFS2 block device (e.g., via a malicious disk image, USB device, network block device, or VM disk) to a victim kernel mounts it normally. During mount, gfs2_recover_func is called. The crafted journal contains a log header with:

  1. Valid lh_header.mh_magic, mh_type, lh_blkno — passes the metadata check.
  2. Valid lh_hash — passes the V1 CRC-32 check.
  3. lh_crc = 0 — bypasses the V2 CRC-32C integrity check.
  4. lh_local_total = INT64_MAX, lh_local_free = INT64_MAX, lh_local_dinodes = INT64_MAX in the (now unchecked) V2 extension.

The kernel applies these values to the master statfs inode (shared across all cluster nodes in GFS2), corrupting global filesystem accounting. The corrupted values are then flushed to disk.


Impact

Impact Detail
Filesystem corruption Master statfs inode sc_total/sc_free/sc_dinodes written with overflowed garbage values
Cluster-wide effect GFS2 is a cluster filesystem; the master statfs inode is shared — corruption affects all nodes
Persistent Damage is flushed to disk; persists after unmount
Privilege required Attacker needs ability to present a crafted block device (local user with USB, or VM escape / container escape scenarios)
DoS df, quota accounting, and block allocator all rely on accurate statfs data

Root Cause

The lh_crc = 0 bypass was likely introduced as a compatibility shim to allow V1-format log headers (which lack the CRC field) to pass validation. However, it creates an exploitable path: an attacker can write a V2-format header that claims to be V1 by zeroing lh_crc, stripping all integrity protection from the extension fields.


Proof-of-Concept (Sketch)

import struct, binascii

# Patch an existing valid GFS2 log header block on-disk:
# 1. Set lh_crc = 0x00000000 (at offset LH_V1_SIZE)
# 2. Set lh_local_total = 0x7FFFFFFFFFFFFFFF (at its struct offset)
# 3. Set lh_local_free  = 0x7FFFFFFFFFFFFFFF
# The lh_hash covers only the V1 region and remains valid.
# Result: CRC check skipped; overflowed values written to master statfs inode.

Fix

Replace the bypass condition with an explicit version check based on lh_flags, or require a non-zero CRC for any header that contains non-zero V2 extension fields:

// Proposed fix: treat lh_crc==0 as invalid if V2 fields are present
if (be32_to_cpu(lh->lh_crc) != crc)
    return 1;

Additionally, add bounds validation on the statfs delta values before applying them:

if (head->lh_local_total < -max_blocks || head->lh_local_total > max_blocks)
    return -EIO;