aprover / examples /sensor_hub.c
Humaid Alqasimi
AProver Space β€” built from main@d852c29
4f0bd4e
Raw History Blame Contribute Delete
6.28 kB
/*
* sensor_hub.c β€” Multi-sensor data aggregation hub (embedded)
*
* Call hierarchy:
*
* process_sample(hub, sid, val) β†’ record_reading(hub, sid, val)
* get_reading(hub, sid) β†’ latest_value(hub, sid)
* (both entry functions also call clamp_sid inline)
*
* Generation layers:
* Layer 1: process_sample, get_reading (entry points)
* Layer 2: record_reading, latest_value (internals)
*
* Demonstration of GRACE refinement loop:
*
* Phase 2 β€” CBMC (weak preconditions) finds two counterexamples:
*
* (A) record_reading: assert(sid valid) fires (sid is unconstrained).
*
* (B) latest_value: division by zero (n == 0) when count is unconstrained.
*
* Phase 3 β€” CEx validation:
*
* (A) SPURIOUS β€” process_sample guards the call with a direct comparison
* if (sid < 0 || sid >= NUM_SENSORS) return;
* This guard lives in the caller's own code (no callee stub involved),
* so when CBMC re-runs the reachability harness for process_sample it
* finds that the execution path
* (sid < 0 || sid >= NUM_SENSORS) AND reach record_reading
* is UNSATISFIABLE β†’ counterexample is spurious.
* LLM refines precondition: adds "0 ≀ sid < NUM_SENSORS".
* Re-run CBMC with refined precondition β†’ assert(sid valid) no longer fires.
* New CEx: sensor.count wraps uint8_t (255 β†’ 0), assert(count > 0) fails.
* Reachability: process_sample never caps count, so hub with count==255
* is a valid caller state β†’ REAL BUG confirmed.
*
* (B) REAL BUG β€” get_reading calls latest_value without checking count > 0.
* Reachability: hub with sensors[sid].count == 0 is a valid input to
* get_reading (hub is not initialised by get_reading itself) β†’ confirmed.
*/
#include <stdint.h>
#include <stddef.h>
#include <assert.h>
/* ------------------------------------------------------------------ */
/* Constants and data structures */
/* ------------------------------------------------------------------ */
#define NUM_SENSORS 4
#define WINDOW 8 /* rolling-average window size */
typedef struct {
int32_t readings[WINDOW]; /* circular window of recent samples */
uint8_t count; /* total samples stored β€” wraps at 256! */
int32_t sum; /* sum of the last min(count,WINDOW) values */
uint8_t slot; /* next write position in readings[] */
} sensor_t;
typedef struct {
sensor_t sensors[NUM_SENSORS];
uint16_t total_samples;
} hub_t;
/* ------------------------------------------------------------------ */
/* Internal helpers (Layer 2) */
/* ------------------------------------------------------------------ */
/*
* record_reading β€” append val to sensor sid's rolling window.
*
* Called by: process_sample
*
* BUG 1 (spurious with weak precondition):
* CBMC makes sid nondeterministic. With precondition = true, sid can be
* βˆ’1 or 99, failing the assert below. However, process_sample guards
* the call with a direct comparison (sid < 0 || sid >= NUM_SENSORS)
* that does not go through any callee stub, so CBMC's reachability
* harness finds the path infeasible β†’ spurious β†’ precondition refined.
*
* BUG 2 (real, found after refinement):
* sensor.count is uint8_t. After exactly 255 calls per sensor, the next
* increment wraps count to 0, and assert(s->count > 0) fires.
* process_sample places no upper bound on the call count, so a hub
* with count == 255 is a legitimate caller state β†’ confirmed real bug.
*/
static void record_reading(hub_t *hub, int sid, int32_t val)
{
/* BUG 1: fires when sid is out of range (spurious β€” caller guards) */
assert(sid >= 0 && sid < NUM_SENSORS);
sensor_t *s = &hub->sensors[sid];
/* Evict oldest value once window is full */
if (s->count >= WINDOW)
s->sum -= s->readings[s->slot];
/* Write new value */
s->readings[s->slot] = val;
s->slot = (uint8_t)((s->slot + 1) % WINDOW);
s->sum += val;
/* BUG 2: uint8_t wraps 255 β†’ 0 after 255 samples */
s->count++;
assert(s->count > 0);
}
/*
* latest_value β€” return the rolling average of stored readings.
*
* Called by: get_reading
*
* BUG 3 (real β€” division by zero):
* n == 0 when no readings have been stored yet. get_reading never checks
* count > 0 before calling this function β†’ division by zero is directly
* reachable from any caller that supplies a freshly-initialised hub.
*/
static int32_t latest_value(hub_t *hub, int sid)
{
sensor_t *s = &hub->sensors[sid];
uint8_t n = (s->count < WINDOW) ? s->count : (uint8_t)WINDOW;
/* BUG 3: n == 0 when count == 0 β†’ division by zero */
return s->sum / (int32_t)n;
}
/* ------------------------------------------------------------------ */
/* Public API (Layer 1 β€” entry points) */
/* ------------------------------------------------------------------ */
/*
* process_sample β€” validate sensor id inline, then record the new value.
*
* KEY: the guard (sid < 0 || sid >= NUM_SENSORS) is a direct comparison
* in this function's body, not delegated to a callee. This ensures that
* CBMC's reachability harness (which stubs callees) still sees the
* constraint, allowing it to conclude that record_reading is unreachable
* with an invalid sid β†’ spurious counterexample correctly detected.
*/
void process_sample(hub_t *hub, int sid, int32_t val)
{
assert(hub != NULL);
/* Direct bounds check β€” no callee stub involved */
if (sid < 0 || sid >= NUM_SENSORS)
return;
record_reading(hub, sid, val);
hub->total_samples++;
}
/*
* get_reading β€” validate sensor id inline, return the rolling average.
*
* BUG: missing if (hub->sensors[sid].count == 0) return 0;
* Consequence: latest_value() divides by zero when the sensor has no data.
*/
int32_t get_reading(hub_t *hub, int sid)
{
assert(hub != NULL);
if (sid < 0 || sid >= NUM_SENSORS)
return -1;
/* Missing: if (hub->sensors[sid].count == 0) return 0; */
return latest_value(hub, sid);
}