β¬’ DragonFlyBSD Kernel Audit
← triage Β· dashboard
DF-4639

ips: debug.ips.ignore_flush_cmd polarity inverted - the adapter cache-flush command is silently dropped on the DEFAULT configuration (fsync() durability broken)

Summary

The flush path tests !flag and skips issuing IPS_CACHE_FLUSH_CMD when the flag is CLEAR - but the flag defaults to 0. Every BUF_CMD_FLUSH bio on an ips disk (fsync/filesystem/unmount) is completed no-error WITHOUT the ServeRAID adapter's volatile write cache ever being flushed: the durability contract of fsync is silently violated; power loss after a returned fsync can lose the synced data. The sysctl description states the opposite semantics; the block is DF-specific. VERIFIED on guest: live default is 0, verbatim-branch harness shows flag=0 -> 0 adapter commands / bio finished no-error, flag=1 -> exactly one 0x0A command. Invert-the-condition fix.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-4639 Β· 11 files
FileTypeDescriptionSize
README.md β€” 2.5 KB ↓ raw
VERDICT.md β€” 3.6 KB ↓ raw
flush_invert_harness.c β€” 3.5 KB view raw
build.sh β€” 66 B view raw
run.sh β€” 90 B view raw
build.log β€” 9 B view raw
run.log β€” 832 B view raw
env.txt β€” 208 B view raw
fix.diff β€” 478 B view raw
verdict.json β€” 4.0 KB view raw
manifest.json β€” 1.1 KB view raw

DF-4639 β€” ips(4): debug.ips.ignore_flush_cmd polarity inverted β€” adapter cache-flush silently dropped on DEFAULT configuration

File: sys/dev/raid/ips/ips_commands.c:195-220 (supporting: :35-40, :249-252) Severity: Medium (data integrity on default config) Β· Confidence: certain Class: CWE-670 (Always-Incorrect Control Flow Implementation)

Build

sh build.sh          # cc -O2 -Wall -o /tmp/flush_invert_harness flush_invert_harness.c

Run

sh run.sh            # uname, live sysctl default, harness as unprivileged user

Expected output (bug present)

debug.ips.ignore_flush_cmd: 0
DEFAULT (flag=0):   flag=0 -> adapter_cmds_issued=0 last_opcode=0x00 bio_finished=1(no error)
DEBUG SET (flag=1): flag=1 -> adapter_cmds_issued=1 last_opcode=0x0a bio_finished=0(no error)
RESULT: INVERTED β€” with the DEFAULT value the real adapter cache-flush ...
harness exit code: 42

What it proves

  1. Live guest kernel (ips driver compiled into X86_64_GENERIC): the sysctl exists and its default is 0 (run.log line 2; source ips_commands.c:35-36 TUNABLE_INT(..., &ips_debug_ignore_flush_cmd) β€” static, i.e. zero-initialized).
  2. The branch (ips_commands.c:202): if (!ips_debug_ignore_flush_cmd) is taken at the DEFAULT β€” it frees the command slot, completes the bio with no error, and returns without ever building/issuing IPS_CACHE_FLUSH_CMD to the adapter. Setting the debug flag to 1 β€” which per the sysctl's own description ("Do not issue IPS_CACHE_FLUSH_CMD on BUF_CMD_FLUSH", ips_commands.c:40) should SUPPRESS the command β€” instead issues it.

Impact

Every BUF_CMD_FLUSH bio on an ips disk (fsync(2)/close-time flush on the raw device, filesystem metadata flushes, unmount flush) is reported complete while the ServeRAID adapter's volatile write cache is never told to flush. After fsync() returns, the just-synced data can still be lost on power failure β€” the durability contract of fsync is silently violated on a stock kernel. (The separate shutdown-path flush ips_flush_cache(), ips_commands.c:476-493, does issue the real command, so the damage window is "flush returned β†’ next real flush/shutdown".)

Fix

fix.diff β€” invert the condition so the debug knob does what it documents (skip the command only when SET). verdict.json records fix_status: not_testable: an end-to-end A/B (observing opcode 0x0A on the wire) requires ServeRAID hardware, which the audit guest cannot instantiate (QEMU has no such device model; env.txt).

VERDICT.md
↓ download raw

DF-4639 VERDICT β€” reproduced (logic inversion on live default; hardware end-to-end not observable on guest)

The claim

ips_send_flush_request (sys/dev/raid/ips/ips_commands.c:195-220) contains a debug tunable whose polarity is inverted, so the default configuration drops every BUF_CMD_FLUSH instead of forwarding it to the adapter as a IPS_CACHE_FLUSH_CMD.

Proof chain

  1. Default value is 0. - Source: ips_commands.c:35-36 β€” static int ips_debug_ignore_flush_cmd;

    • TUNABLE_INT("debug.ips.ignore_flush_cmd", ...) (BSS β‡’ 0).
    • Live guest: sysctl debug.ips.ignore_flush_cmd β†’ 0 (run.log; the ips driver is compiled into the guest's X86_64_GENERIC kernel).
  2. Dispatch reaches the function for every flush bio. ipsd_strategy (ips_disk.c:128-140) queues the bio and calls ips_start_io_request (ips_commands.c:237-253), which at :249 branches b_cmd == BUF_CMD_FLUSH β†’ ips_send_flush_request (:250).

  3. The branch skips the adapter command at flag==0. ips_commands.c:202-206: c if (!ips_debug_ignore_flush_cmd) { ips_insert_free_cmd(sc, command); ipsd_finish(bio); /* completed, NO B_ERROR set */ return 0; } The real command build (flush_cmd->command = IPS_CACHE_FLUSH_CMD; :210) and issue (sc->ips_issue_cmd(command) :218) are only reachable when the flag is SET (1).

  4. Documented semantics are the opposite. ips_commands.c:38-40: SYSCTL description "Do not issue IPS_CACHE_FLUSH_CMD on BUF_CMD_FLUSH" β€” i.e. flag SET β‡’ skip. FreeBSD upstream has no such tunable (the whole flush-request block is DragonFly-added; verified against freebsd-src ips_commands.c HEAD β€” no ignore_flush_cmd/ips_send_flush_request there), so this is DF-introduced, not inherited.

  5. Harness run (guest, unprivileged user maxx): the exact branch extracted verbatim into flush_invert_harness.c β€” flag=0 β†’ adapter_cmds_issued=0, bio_finished=1 (no error); flag=1 β†’ adapter_cmds_issued=1, last_opcode=0x0A (IPS_CACHE_FLUSH_CMD). Exit marker 42. See run.log.

What was and was not reproduced

  • REPRODUCED: the inverted control flow itself, at the live kernel's default flag value, from an unprivileged account (the harness is exact-logic; the sysctl default is read from the running kernel).
  • NOT observable on this guest: the on-the-wire adapter command. The guest has zero ServeRAID PCI devices and QEMU has no ips device model (env.txt: 0 matches for vendor 0x1014/0x9005; /dev/ips* absent), so the path cannot be instantiated with hardware. The consequence β€” fsync() data lying in the adapter cache β€” follows mechanically from the proven branch.

Fix validation

fix.diff flips the condition to if (ips_debug_ignore_flush_cmd) so the skip path only runs when the operator explicitly asks for it. An A/B kernel run cannot change the observable (the branch is proven by harness either way; the end-to-end effect needs hardware), so fix_status: not_testable, fix_baseline_reproduced: 1, fix_patched_reproduced: 0.

  • DF-4613..4616 (ips.c watchdog/queue/reinit/dump) β€” adjacent ips family, all distinct root causes.
  • No other consumer of ips_debug_ignore_flush_cmd exists (rg ignore_flush sys/ β†’ ips_commands.c only).

Fix status (batch validation)

Applied with the 36 sibling fix.diffs of this batch in findings/poc/_batch41/all_fixes.patch; single in-guest make -j6 nativekernel KERNCONF=X86_64_GENERIC completed RC=0 with zero errors under -Werror (full log: findings/poc/_batch41/fix_build8.log). Behavior A/B intentionally skipped for this Info-tier item.

Fix verification

not_testable
baseline reproduced→ patch + rebuild →patched clean

fix.diff flips the polarity to match the documented tunable semantics. Baseline inversion reproduced via harness; an end-to-end patched-kernel A/B (observing IPS_CACHE_FLUSH_CMD reach a real adapter) is impossible on the audit guest - QEMU has no ServeRAID device model and the guest has zero 0x1014/0x9005 PCI devices (env.txt). The one-line fix is self-evident against the proven branch.

fix.diff (ips_commands.c:202 '!ips_debug_ignore_flush_cmd' -> 'ips_debug_ignore_flush_cmd'); run.log baseline harness output
↓ fix.diffper-fix-DF-4639

Confirmed kernel references

Detail

Evidence (decisive lines)

run.log (live sysctl default 0 + harness both cases + exit 42), flush_invert_harness.c (verbatim branch extraction), VERDICT.md proof chain with ips_disk.c:128-140/ips_commands.c:35-40,195-220,249-252, fix.diff

PoC changes

n/a - evidence pack authored fresh in this run (harness + build/run scripts); orchestrator seed was only the ips.c reviewer's cross-file lead

Verified recommended fix

Invert the condition at ips_commands.c:202 to 'if (ips_debug_ignore_flush_cmd)' so the skip path only runs when the operator explicitly enables it (fix.diff).

Verdict

ips_send_flush_request (ips_commands.c:202-206) is inverted against its own debug knob: with the DEFAULT value debug.ips.ignore_flush_cmd=0 (static BSS + TUNABLE_INT, ips_commands.c:35-36; confirmed live on the guest kernel, which has ips compiled in) the function frees the command slot and completes the BUF_CMD_FLUSH bio with no error WITHOUT ever building/issuing IPS_CACHE_FLUSH_CMD; the real adapter cache-flush (opcode 0x0A, ips_commands.c:210-218) only runs when the flag is SET - the exact opposite of the sysctl description 'Do not issue IPS_CACHE_FLUSH_CMD on BUF_CMD_FLUSH' (ips_commands.c:40). Reproduced on the guest from an unprivileged account with a verbatim extraction of the branch (run.log: flag=0 -> 0 adapter commands, bio finished no-error; flag=1 -> 1 command opcode 0x0a; exit 42). Net effect on stock systems: every flush bio on an ips disk (fsync on the raw device, filesystem/unmount flushes via ipsd_strategy -> ips_start_io_request, ips_commands.c:249-250) reports durability while the adapter write cache is never flushed - power-loss data loss after a returned fsync. impact=none in the poc sense (no kernel memory corruption / privilege effect); the harm is silent data-integrity violation. The whole flush-request block is DragonFly-specific (no such tunable or function in FreeBSD upstream).