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
0toLH_V1_SIZE): covered by a CRC-32 hash stored inlh_hash. - V2 extension region (bytes
LH_V1_SIZE+4to end of block): covered by a CRC-32C stored inlh_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:
- Valid
lh_header.mh_magic,mh_type,lh_blkno— passes the metadata check. - Valid
lh_hash— passes the V1 CRC-32 check. lh_crc = 0— bypasses the V2 CRC-32C integrity check.lh_local_total = INT64_MAX,lh_local_free = INT64_MAX,lh_local_dinodes = INT64_MAXin 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;