⬢ DragonFlyBSD Kernel Audit
DF-4739 / alpm_wait_offbyone_demo.c
← back to finding ↓ download raw
/*
 * DF-4739 demo: alpm_wait() off-by-one — exact replica of the loop in
 * sys/dev/powermng/alpm/alpm.c:297-332, driven with fixture register reads.
 *
 * Bug family: DF-4549 (viapm_wait), DF-4737 (amdpm_wait).
 *
 * Case A (genuine SMBus timeout, slave never completes):
 *   SMI_I_STS never sets during all 10000 polls. while(count--) exits with
 *   count == -1, so `if (!count)` is false and SMB_ETIMEOUT is never
 *   reported -> alpm_wait() returns SMB_ENOERR and every alpm_*x*() caller
 *   reads STALE SMBHDATA/SMBHDATB/SMBHBLOCK register data as if the
 *   transaction succeeded (alpm.c:413-414, 467-468, 523-528, 603-615).
 *
 * Case B (completion on the final poll):
 *   SMI_I_STS sets on poll #10000 (count entered the loop test as 1 and was
 *   post-decremented to 0): break leaves count == 0 -> `!count` is true and
 *   SMB_ETIMEOUT is spuriously OR'd into a transfer that actually finished.
 *
 * Case C (fixed loop, `if (count < 0)`): shows the corrected behaviour.
 *
 * Build: cc -Wall -Wextra -o /tmp/df4739 /tmp/df4739.c
 */
#include <stdio.h>

#define	SMBCMD		0x1
#define	SMBHDATA	0x4
#define	SMBHDATB	0x5
#define	SMBHBLOCK	0x6

#define TERMINATE	0x80
#define BUS_COLLI	0x40
#define DEVICE_ERR	0x20
#define SMI_I_STS	0x10
#define HST_BSY		0x08
#define IDL_STS		0x04

#define SMB_ENOERR	0x0
#define SMB_ENOACK	0x4
#define SMB_EABORT	0x10
#define SMB_ETIMEOUT	0x20
#define SMB_EBUSERR	0x1

static unsigned char fixture_sts;	/* stands in for ALPM_SMBINB(sc, SMBSTS) */
static int fixture_set_on_poll;		/* 0 = never; N>0 = set SMI_I_STS on poll N */
static int polls;

static unsigned char
ALPM_SMBINB(int reg)
{
	(void)reg;
	polls++;
	if (fixture_set_on_poll > 0 && polls >= fixture_set_on_poll)
		return (fixture_sts | SMI_I_STS);
	return (fixture_sts);
}

/* verbatim structure of alpm_wait(), alpm.c:298-332 */
static int
alpm_wait_orig(void)
{
	int count = 10000;
	u_char sts = 0;
	int error;

	while (count--) {
		/* DELAY(10) elided */
		sts = ALPM_SMBINB(SMBCMD);
		if (sts & SMI_I_STS)
			break;
	}

	error = SMB_ENOERR;

	if (!count)
		error |= SMB_ETIMEOUT;

	if (sts & TERMINATE)
		error |= SMB_EABORT;
	if (sts & BUS_COLLI)
		error |= SMB_ENOACK;
	if (sts & DEVICE_ERR)
		error |= SMB_EBUSERR;

	return (error);
}

/* corrected loop guard (the proposed fix) */
static int
alpm_wait_fixed(void)
{
	int count = 10000;
	u_char sts = 0;
	int error;

	while (count--) {
		sts = ALPM_SMBINB(SMBCMD);
		if (sts & SMI_I_STS)
			break;
	}

	error = SMB_ENOERR;

	if (count < 0)			/* loop exhaustion leaves count == -1 */
		error |= SMB_ETIMEOUT;

	if (sts & TERMINATE)
		error |= SMB_EABORT;
	if (sts & BUS_COLLI)
		error |= SMB_ENOACK;
	if (sts & DEVICE_ERR)
		error |= SMB_EBUSERR;

	return (error);
}

int
main(void)
{
	int r;

	polls = 0; fixture_sts = 0; fixture_set_on_poll = 0;
	r = alpm_wait_orig();
	printf("A: genuine timeout (never completes, %d polls): "
	    "error=0x%x  %s\n", polls, r,
	    (r & SMB_ETIMEOUT) ? "ETIMEOUT reported (correct)" :
	    "BUG: ETIMEOUT NEVER reported -> alpm_*x*() returns success and "
	    "copies stale SMBHDATA/SMBHBLOCK to the caller");

	polls = 0; fixture_sts = 0; fixture_set_on_poll = 10000;
	r = alpm_wait_orig();
	printf("B: completes on final poll #10000: "
	    "error=0x%x  %s\n", r,
	    (r & SMB_ETIMEOUT) ? "BUG: spurious ETIMEOUT on a transfer that "
	    "completed on the last poll" : "correct");

	polls = 0; fixture_sts = 0; fixture_set_on_poll = 0;
	r = alpm_wait_fixed();
	printf("C-fix: genuine timeout: error=0x%x  %s\n", r,
	    (r & SMB_ETIMEOUT) ? "ETIMEOUT reported (correct)" : "still wrong");

	polls = 0; fixture_sts = 0; fixture_set_on_poll = 10000;
	r = alpm_wait_fixed();
	printf("D-fix: completes on final poll: error=0x%x  %s\n", r,
	    (r & SMB_ETIMEOUT) ? "still wrong" : "success, no spurious error (correct)");

	return (0);
}