USB · Module 25
Descriptor Issues
A descriptor set is described three times by three different fields, and the host walks it by one while reading it by another — so when they disagree the error lands on a field that is perfectly correct.
Chapter 25.1 ended on a device whose descriptor disagreed with itself. This chapter is about why that class of bug is so disproportionately painful to find.
1. A Descriptor Set Is a Self-Describing Tree
One buffer, containing a chain of variable-length records:
offset 0 bLength how long THIS record is
offset 1 bDescriptorType what it is
... type-specific fields
CONFIGURATION (type 2) offset 2..3 wTotalLength
offset 4 bNumInterfaces
INTERFACE (type 4) offset 4 bNumEndpoints
ENDPOINT (type 5)The tree, and the three fields that describe it
The same buffer is therefore described three times, by three different fields, and all three must agree:
wTotalLength == the sum of every bLength in the set
bNumInterfaces == the number of INTERFACE records present
bNumEndpoints == the number of ENDPOINT records after each one2. Why Disagreement Is So Confusing
The host asks for wTotalLength bytes, and then walks the buffer by adding bLength to a pointer. Those are two different fields doing two different jobs.
3. And bLength = 0 Is Not an Error, It Is a Hang
The walk advances by bLength. A record claiming zero length advances the pointer by zero, and the host reads the same record for ever.
ptr = 0
while (ptr < wTotalLength) {
len = buf[ptr];
...
ptr += len; <-- len == 0: ptr never moves
}A host that does not bound its descriptor walk hangs on a device that returns a zero
bLength. Not fails — hangs.
Which is why this validator gives zero its own error class rather than folding it into "too short": a reader needs to know which symptom to expect, and the two symptoms are "a strange value" and "the machine stopped responding".
4. Short Is a Different Bug Again
bLength below the minimum for the type means the record cannot contain the fields the type is defined to have — so every field the host reads from it comes from the next record.
It is the most common single descriptor defect and the one that produces the strangest reports.
5. What We Are Building
usb_descriptor_validator #(MIN_CFG=9, MIN_IF=9, MIN_EP=7, MIN_ANY=2)
inputs outputs
------ -------
sod start of a set state / cur_type / cur_len
byte_valid / byte_data consumed vs total_len
eod the host stopped if_seen vs if_expected
ep_seen vs ep_expected
err_code ZEROLEN / SHORT / OVERRUN /
TOTAL / IFCOUNT / EPCOUNT /
ORPHAN
set_ok one pulse per consistent set
Every "seen vs expected" pair is exposed, because a report that
says "bNumEndpoints is wrong" is much less useful than one that
says "it says 3 and there are 2".6. Verilog-2005 Implementation
// usb_descriptor_validator -- a descriptor is a self-describing tree, and
// the three numbers that describe it must agree.
//
// THE SHAPE OF THE THING
//
// A configuration descriptor set is one buffer containing a chain of
// variable-length records:
//
// offset 0 bLength how long THIS record is
// offset 1 bDescriptorType what it is
// ... type-specific fields
//
// CONFIGURATION (type 2) offset 2..3 wTotalLength
// offset 4 bNumInterfaces
// INTERFACE (type 4) offset 4 bNumEndpoints
// ENDPOINT (type 5)
//
// So the same buffer is described THREE times, by three different fields,
// and all three must agree:
//
// wTotalLength == the sum of every bLength in the set
// bNumInterfaces == the number of INTERFACE records present
// bNumEndpoints == the number of ENDPOINT records after each one
//
// HOW THE HOST READS IT, AND WHY DISAGREEMENT IS SO CONFUSING
//
// The host asks for wTotalLength bytes, and then walks the buffer by
// ADDING bLength to a pointer. Those are two different fields doing two
// different jobs, and when they disagree the host does not notice anything
// wrong at the point of disagreement -- it lands in the MIDDLE of the next
// record and reads its fields from the wrong offsets.
//
// The error is reported against a field that is perfectly correct,
// several records after the one that was wrong.
//
// That is why descriptor bugs are so disproportionately painful: the
// symptom and the cause are separated by a variable number of bytes, and
// the symptom is a plausible-looking value in an unrelated field.
//
// AND bLength = 0 IS NOT AN ERROR, IT IS A HANG
//
// The walk advances by bLength. A record claiming zero length advances the
// pointer by zero, and the host reads the same record for ever.
//
// A host that does not bound its descriptor walk HANGS on a
// device that returns a zero bLength. Not fails -- hangs.
//
// Which is why this validator treats a zero bLength as its own error class
// rather than folding it into "too short", and why it stops walking.
//
// A DESCRIPTOR SHORTER THAN ITS TYPE REQUIRES IS A DIFFERENT BUG
//
// bLength below the minimum for the type means the record cannot contain
// the fields the type is defined to have -- so every field the host reads
// from it comes from the NEXT record. It is the most common single
// descriptor defect and the one that produces the strangest reports.
module usb_descriptor_validator #(
parameter integer MIN_CFG = 9, // minimum bLength for CONFIGURATION
parameter integer MIN_IF = 9, // ...for INTERFACE
parameter integer MIN_EP = 7, // ...for ENDPOINT
parameter integer MIN_ANY = 2 // ...for anything else: bLength + bType
) (
input wire clk,
input wire rst_n,
input wire sod, // start of a descriptor set
input wire byte_valid,
input wire [7:0] byte_data,
input wire eod, // the host stopped reading
input wire eot,
output wire [2:0] state,
output wire [7:0] cur_type,
output wire [7:0] cur_len,
output wire [15:0] consumed, // bytes walked so far
output wire [15:0] total_len, // wTotalLength as the device claims it
output wire [7:0] if_seen,
output wire [7:0] if_expected,
output wire [7:0] ep_seen, // endpoints after the CURRENT interface
output wire [7:0] ep_expected,
output wire err_pulse,
output wire [2:0] err_code,
output wire set_ok, // one pulse per consistent descriptor set
output reg [31:0] n_desc,
output reg [31:0] n_zerolen,
output reg [31:0] n_short,
output reg [31:0] n_overrun,
output reg [31:0] n_total,
output reg [31:0] n_ifcount,
output reg [31:0] n_epcount,
output reg [31:0] n_orphan,
output reg [31:0] n_sets_ok
);
localparam [2:0] W_IDLE = 3'd0, // waiting for a descriptor set to start
W_LEN = 3'd1, // the next byte is bLength
W_TYPE = 3'd2, // the next byte is bDescriptorType
W_BODY = 3'd3, // consuming the rest of this record
W_DEAD = 3'd4; // the walk cannot continue
localparam [2:0] E_NONE = 3'd0,
E_ZEROLEN = 3'd1, // bLength 0: the walk cannot advance
E_SHORT = 3'd2, // shorter than the type requires
E_OVERRUN = 3'd3, // the walk ran past wTotalLength
E_TOTAL = 3'd4, // wTotalLength != the sum of bLengths
E_IFCOUNT = 3'd5, // bNumInterfaces != interfaces present
E_EPCOUNT = 3'd6, // bNumEndpoints != endpoints present
E_ORPHAN = 3'd7; // an ENDPOINT with no INTERFACE above it
localparam [7:0] T_CONFIG = 8'd2,
T_INTERFACE = 8'd4,
T_ENDPOINT = 8'd5;
reg [2:0] st_r;
reg [7:0] len_r, typ_r, off_r;
reg [15:0] cons_r, total_r;
reg [7:0] ifs_r, ife_r, eps_r, epe_r;
reg have_if_r; // an INTERFACE has been seen in this set
reg have_cfg_r; // a CONFIGURATION has been seen
reg [2:0] ec_r;
reg er_r, ok_r;
assign state = st_r;
assign cur_type = typ_r;
assign cur_len = len_r;
assign consumed = cons_r;
assign total_len = total_r;
assign if_seen = ifs_r;
assign if_expected = ife_r;
assign ep_seen = eps_r;
assign ep_expected = epe_r;
assign err_pulse = er_r;
assign err_code = ec_r;
assign set_ok = ok_r;
// The minimum bLength a record of this type can possibly have. A record
// below it cannot contain the fields the type is DEFINED to have, so
// every field read from it comes from the next record along.
function [7:0] min_len;
input [7:0] t;
begin
case (t)
T_CONFIG: min_len = MIN_CFG[7:0];
T_INTERFACE: min_len = MIN_IF[7:0];
T_ENDPOINT: min_len = MIN_EP[7:0];
default: min_len = MIN_ANY[7:0];
endcase
end
endfunction
reg [2:0] st_n, ec_n;
reg [7:0] len_n, typ_n, off_n;
reg [15:0] cons_n, total_n;
reg [7:0] ifs_n, ife_n, eps_n, epe_n;
reg hif_n, hcfg_n, er_n, ok_n;
always @* begin
st_n = st_r;
len_n = len_r;
typ_n = typ_r;
off_n = off_r;
cons_n = cons_r;
total_n = total_r;
ifs_n = ifs_r;
ife_n = ife_r;
eps_n = eps_r;
epe_n = epe_r;
hif_n = have_if_r;
hcfg_n = have_cfg_r;
ec_n = E_NONE;
er_n = 1'b0;
ok_n = 1'b0;
if (eot) begin
st_n = W_IDLE;
end else if (sod) begin
// A fresh descriptor set. Everything the previous set accumulated is
// gone: a stale interface count carried across a set boundary would
// report a mismatch against a descriptor that is perfectly correct.
st_n = W_LEN;
len_n = 8'd0;
typ_n = 8'd0;
off_n = 8'd0;
cons_n = 16'd0;
total_n = 16'd0;
ifs_n = 8'd0;
ife_n = 8'd0;
eps_n = 8'd0;
epe_n = 8'd0;
hif_n = 1'b0;
hcfg_n = 1'b0;
end else if (eod) begin
// ---- THE END OF THE SET. The aggregate checks happen HERE, and
// ---- they are the ones that cannot be made record by record.
//
// W_DEAD is excluded deliberately. A walk that has already stopped
// has already said why, and the aggregate counts it left behind are
// wrong BECAUSE it stopped -- so re-checking them reports the same
// defect a second time under a different name. One descriptor set,
// one finding, which is chapter 23.4's rule applied to a buffer.
if ((st_r != W_IDLE) && (st_r != W_DEAD)) begin
if (hif_n && (eps_r != epe_r)) begin
// The last interface's endpoint count. Every other interface was
// checked when the NEXT one started; the last one has no next.
er_n = 1'b1; ec_n = E_EPCOUNT;
end else if (hcfg_n && (cons_r != total_r)) begin
er_n = 1'b1; ec_n = E_TOTAL;
end else if (hcfg_n && (ifs_r != ife_r)) begin
er_n = 1'b1; ec_n = E_IFCOUNT;
end else if (st_r == W_LEN) begin
// Landed exactly on a record boundary with everything agreeing.
ok_n = 1'b1;
end else begin
// The set ended in the middle of a record.
er_n = 1'b1; ec_n = E_OVERRUN;
end
end
st_n = W_IDLE;
end else if (byte_valid) begin
case (st_r)
W_LEN: begin
len_n = byte_data;
off_n = 8'd1;
cons_n = cons_r + 16'd1;
if (byte_data == 8'd0) begin
// ---- A ZERO LENGTH IS A HANG, NOT AN ERROR. ----
//
// The walk advances by bLength. Zero advances it by nothing, so
// a host that does not bound its walk reads this record for
// ever. Stopping is the only safe response, and saying so as
// its own error class is what tells the reader that the symptom
// will be a hang rather than a wrong value.
er_n = 1'b1; ec_n = E_ZEROLEN;
st_n = W_DEAD;
end else begin
st_n = W_TYPE;
end
end
W_TYPE: begin
typ_n = byte_data;
off_n = 8'd2;
cons_n = cons_r + 16'd1;
if (len_r < min_len(byte_data)) begin
// ---- Shorter than the type requires. ----
//
// The record cannot hold the fields its type is defined to
// have, so every field the host reads from it comes from the
// NEXT record. The walk stops, because continuing from here
// produces a cascade of nonsense.
er_n = 1'b1; ec_n = E_SHORT;
st_n = W_DEAD;
end else if (byte_data == T_ENDPOINT) begin
if (!have_if_r) begin
// An endpoint belongs to an interface. One that appears
// before any interface has no owner, and the host will
// attribute it to whatever interface comes next.
er_n = 1'b1; ec_n = E_ORPHAN;
st_n = W_DEAD;
end else begin
eps_n = eps_r + 8'd1;
st_n = (len_r == 8'd2) ? W_LEN : W_BODY;
end
end else if (byte_data == T_INTERFACE) begin
// Close the PREVIOUS interface before opening this one.
if (have_if_r && (eps_r != epe_r)) begin
er_n = 1'b1; ec_n = E_EPCOUNT;
st_n = W_DEAD;
end else begin
hif_n = 1'b1;
ifs_n = ifs_r + 8'd1;
eps_n = 8'd0;
st_n = (len_r == 8'd2) ? W_LEN : W_BODY;
end
end else begin
if (byte_data == T_CONFIG) hcfg_n = 1'b1;
st_n = (len_r == 8'd2) ? W_LEN : W_BODY;
end
end
W_BODY: begin
cons_n = cons_r + 16'd1;
off_n = off_r + 8'd1;
// The type-specific fields, picked out by offset. This is the
// only place the validator knows anything about what a
// descriptor MEANS rather than how long it is.
if (typ_r == T_CONFIG) begin
if (off_r == 8'd2) total_n = {total_r[15:8], byte_data};
if (off_r == 8'd3) total_n = {byte_data, total_r[7:0]};
if (off_r == 8'd4) ife_n = byte_data;
end else if (typ_r == T_INTERFACE) begin
if (off_r == 8'd4) epe_n = byte_data;
end
if (off_r + 8'd1 >= len_r) st_n = W_LEN;
// ---- The walk must not run past what the device said. ----
//
// Checked as the bytes are consumed rather than at the end,
// because by the end the pointer is somewhere meaningless and the
// report would name whatever record it happened to land in.
if (hcfg_n && (total_r != 16'd0) && (cons_n > total_r)) begin
er_n = 1'b1; ec_n = E_OVERRUN;
st_n = W_DEAD;
end
end
default: begin
// W_IDLE and W_DEAD: bytes are consumed and ignored. A dead walk
// does not produce one error per remaining byte -- chapter 23.4's
// rule, applied to a descriptor.
cons_n = cons_r + 16'd1;
end
endcase
end
end
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
st_r <= W_IDLE;
len_r <= 8'd0;
typ_r <= 8'd0;
off_r <= 8'd0;
cons_r <= 16'd0;
total_r <= 16'd0;
ifs_r <= 8'd0;
ife_r <= 8'd0;
eps_r <= 8'd0;
epe_r <= 8'd0;
have_if_r <= 1'b0;
have_cfg_r <= 1'b0;
ec_r <= E_NONE;
er_r <= 1'b0;
ok_r <= 1'b0;
n_desc <= 32'd0;
n_zerolen <= 32'd0;
n_short <= 32'd0;
n_overrun <= 32'd0;
n_total <= 32'd0;
n_ifcount <= 32'd0;
n_epcount <= 32'd0;
n_orphan <= 32'd0;
n_sets_ok <= 32'd0;
end else begin
st_r <= st_n;
len_r <= len_n;
typ_r <= typ_n;
off_r <= off_n;
cons_r <= cons_n;
total_r <= total_n;
ifs_r <= ifs_n;
ife_r <= ife_n;
eps_r <= eps_n;
epe_r <= epe_n;
have_if_r <= hif_n;
have_cfg_r <= hcfg_n;
ec_r <= ec_n;
er_r <= er_n;
ok_r <= ok_n;
if (byte_valid && (st_r == W_TYPE) && !sod && !eod && !eot)
n_desc <= n_desc + 32'd1;
if (ok_n) n_sets_ok <= n_sets_ok + 32'd1;
// The per-cause counters are driven by the SAME pulse as the error,
// so they sum to it by construction (chapter 23.4).
if (er_n) begin
case (ec_n)
E_ZEROLEN: n_zerolen <= n_zerolen + 32'd1;
E_SHORT: n_short <= n_short + 32'd1;
E_OVERRUN: n_overrun <= n_overrun + 32'd1;
E_TOTAL: n_total <= n_total + 32'd1;
E_IFCOUNT: n_ifcount <= n_ifcount + 32'd1;
E_EPCOUNT: n_epcount <= n_epcount + 32'd1;
E_ORPHAN: n_orphan <= n_orphan + 32'd1;
default: ;
endcase
end
end
end
endmodule7. SystemVerilog Implementation
// usb_descriptor_validator -- a descriptor is a self-describing tree, and
// the three numbers that describe it must agree.
//
// THE SHAPE OF THE THING
//
// A configuration descriptor set is one buffer containing a chain of
// variable-length records:
//
// offset 0 bLength how long THIS record is
// offset 1 bDescriptorType what it is
// ... type-specific fields
//
// CONFIGURATION (type 2) offset 2..3 wTotalLength
// offset 4 bNumInterfaces
// INTERFACE (type 4) offset 4 bNumEndpoints
// ENDPOINT (type 5)
//
// So the same buffer is described THREE times, by three different fields,
// and all three must agree:
//
// wTotalLength == the sum of every bLength in the set
// bNumInterfaces == the number of INTERFACE records present
// bNumEndpoints == the number of ENDPOINT records after each one
//
// HOW THE HOST READS IT, AND WHY DISAGREEMENT IS SO CONFUSING
//
// The host asks for wTotalLength bytes, and then walks the buffer by
// ADDING bLength to a pointer. Those are two different fields doing two
// different jobs, and when they disagree the host does not notice anything
// wrong at the point of disagreement -- it lands in the MIDDLE of the next
// record and reads its fields from the wrong offsets.
//
// The error is reported against a field that is perfectly correct,
// several records after the one that was wrong.
//
// That is why descriptor bugs are so disproportionately painful: the
// symptom and the cause are separated by a variable number of bytes, and
// the symptom is a plausible-looking value in an unrelated field.
//
// AND bLength = 0 IS NOT AN ERROR, IT IS A HANG
//
// The walk advances by bLength. A record claiming zero length advances the
// pointer by zero, and the host reads the same record for ever.
//
// A host that does not bound its descriptor walk HANGS on a
// device that returns a zero bLength. Not fails -- hangs.
//
// Which is why this validator treats a zero bLength as its own error class
// rather than folding it into "too short", and why it stops walking.
//
// A DESCRIPTOR SHORTER THAN ITS TYPE REQUIRES IS A DIFFERENT BUG
//
// bLength below the minimum for the type means the record cannot contain
// the fields the type is defined to have -- so every field the host reads
// from it comes from the NEXT record. It is the most common single
// descriptor defect and the one that produces the strangest reports.
package usb_desc_pkg;
// The walker's states. W_DEAD exists so that a walk which has already
// stopped does not report the same defect again under a different name
// at the end of the buffer.
typedef enum logic [2:0] {
W_IDLE = 3'd0, // waiting for a descriptor set to start
W_LEN = 3'd1, // the next byte is bLength
W_TYPE = 3'd2, // the next byte is bDescriptorType
W_BODY = 3'd3, // consuming the rest of this record
W_DEAD = 3'd4 // the walk cannot continue
} walk_state_e;
// E_ZEROLEN is separate from E_SHORT on purpose: a zero length is a HANG
// and a short length is a wrong value, and a reader needs to know which
// symptom to expect.
typedef enum logic [2:0] {
E_NONE = 3'd0,
E_ZEROLEN = 3'd1, // bLength 0: the walk cannot advance
E_SHORT = 3'd2, // shorter than the type requires
E_OVERRUN = 3'd3, // the walk ran past wTotalLength
E_TOTAL = 3'd4, // wTotalLength != the sum of the bLengths
E_IFCOUNT = 3'd5, // bNumInterfaces != interfaces present
E_EPCOUNT = 3'd6, // bNumEndpoints != endpoints present
E_ORPHAN = 3'd7 // an ENDPOINT with no INTERFACE above it
} desc_err_e;
endpackage
module usb_descriptor_validator
import usb_desc_pkg::*;
#(
parameter int MIN_CFG = 9, // minimum bLength for CONFIGURATION
parameter int MIN_IF = 9, // ...for INTERFACE
parameter int MIN_EP = 7, // ...for ENDPOINT
parameter int MIN_ANY = 2 // ...for anything else: bLength + bType
) (
input logic clk,
input logic rst_n,
input logic sod, // start of a descriptor set
input logic byte_valid,
input logic [7:0] byte_data,
input logic eod, // the host stopped reading
input logic eot,
output walk_state_e state,
output logic [7:0] cur_type,
output logic [7:0] cur_len,
output logic [15:0] consumed, // bytes walked so far
output logic [15:0] total_len, // wTotalLength as the device claims it
output logic [7:0] if_seen,
output logic [7:0] if_expected,
output logic [7:0] ep_seen, // endpoints after the CURRENT interface
output logic [7:0] ep_expected,
output logic err_pulse,
output desc_err_e err_code,
output logic set_ok, // one pulse per consistent descriptor set
output logic [31:0] n_desc,
output logic [31:0] n_zerolen,
output logic [31:0] n_short,
output logic [31:0] n_overrun,
output logic [31:0] n_total,
output logic [31:0] n_ifcount,
output logic [31:0] n_epcount,
output logic [31:0] n_orphan,
output logic [31:0] n_sets_ok
);
localparam [7:0] T_CONFIG = 8'd2,
T_INTERFACE = 8'd4,
T_ENDPOINT = 8'd5;
walk_state_e st_r;
desc_err_e ec_r;
logic [7:0] len_r, typ_r, off_r;
logic [15:0] cons_r, total_r;
logic [7:0] ifs_r, ife_r, eps_r, epe_r;
logic have_if_r; // an INTERFACE has been seen in this set
logic have_cfg_r; // a CONFIGURATION has been seen
logic er_r, ok_r;
assign state = st_r;
assign cur_type = typ_r;
assign cur_len = len_r;
assign consumed = cons_r;
assign total_len = total_r;
assign if_seen = ifs_r;
assign if_expected = ife_r;
assign ep_seen = eps_r;
assign ep_expected = epe_r;
assign err_pulse = er_r;
assign err_code = ec_r;
assign set_ok = ok_r;
// The minimum bLength a record of this type can possibly have. A record
// below it cannot contain the fields the type is DEFINED to have, so
// every field read from it comes from the next record along.
function automatic logic [7:0] min_len(input logic [7:0] t);
begin
case (t)
T_CONFIG: min_len = 8'(MIN_CFG);
T_INTERFACE: min_len = 8'(MIN_IF);
T_ENDPOINT: min_len = 8'(MIN_EP);
default: min_len = 8'(MIN_ANY);
endcase
end
endfunction
walk_state_e st_n;
desc_err_e ec_n;
logic [7:0] len_n, typ_n, off_n;
logic [15:0] cons_n, total_n;
logic [7:0] ifs_n, ife_n, eps_n, epe_n;
logic hif_n, hcfg_n, er_n, ok_n;
always_comb begin
st_n = st_r;
len_n = len_r;
typ_n = typ_r;
off_n = off_r;
cons_n = cons_r;
total_n = total_r;
ifs_n = ifs_r;
ife_n = ife_r;
eps_n = eps_r;
epe_n = epe_r;
hif_n = have_if_r;
hcfg_n = have_cfg_r;
ec_n = E_NONE;
er_n = 1'b0;
ok_n = 1'b0;
if (eot) begin
st_n = W_IDLE;
end else if (sod) begin
// A fresh descriptor set. Everything the previous set accumulated is
// gone: a stale interface count carried across a set boundary would
// report a mismatch against a descriptor that is perfectly correct.
st_n = W_LEN;
len_n = 8'd0;
typ_n = 8'd0;
off_n = 8'd0;
cons_n = 16'd0;
total_n = 16'd0;
ifs_n = 8'd0;
ife_n = 8'd0;
eps_n = 8'd0;
epe_n = 8'd0;
hif_n = 1'b0;
hcfg_n = 1'b0;
end else if (eod) begin
// ---- THE END OF THE SET. The aggregate checks happen HERE, and
// ---- they are the ones that cannot be made record by record.
//
// W_DEAD is excluded deliberately. A walk that has already stopped
// has already said why, and the aggregate counts it left behind are
// wrong BECAUSE it stopped -- so re-checking them reports the same
// defect a second time under a different name. One descriptor set,
// one finding, which is chapter 23.4's rule applied to a buffer.
if ((st_r != W_IDLE) && (st_r != W_DEAD)) begin
if (hif_n && (eps_r != epe_r)) begin
// The last interface's endpoint count. Every other interface was
// checked when the NEXT one started; the last one has no next.
er_n = 1'b1; ec_n = E_EPCOUNT;
end else if (hcfg_n && (cons_r != total_r)) begin
er_n = 1'b1; ec_n = E_TOTAL;
end else if (hcfg_n && (ifs_r != ife_r)) begin
er_n = 1'b1; ec_n = E_IFCOUNT;
end else if (st_r == W_LEN) begin
// Landed exactly on a record boundary with everything agreeing.
ok_n = 1'b1;
end else begin
// The set ended in the middle of a record.
er_n = 1'b1; ec_n = E_OVERRUN;
end
end
st_n = W_IDLE;
end else if (byte_valid) begin
case (st_r)
W_LEN: begin
len_n = byte_data;
off_n = 8'd1;
cons_n = cons_r + 16'd1;
if (byte_data == 8'd0) begin
// ---- A ZERO LENGTH IS A HANG, NOT AN ERROR. ----
//
// The walk advances by bLength. Zero advances it by nothing, so
// a host that does not bound its walk reads this record for
// ever. Stopping is the only safe response, and saying so as
// its own error class is what tells the reader that the symptom
// will be a hang rather than a wrong value.
er_n = 1'b1; ec_n = E_ZEROLEN;
st_n = W_DEAD;
end else begin
st_n = W_TYPE;
end
end
W_TYPE: begin
typ_n = byte_data;
off_n = 8'd2;
cons_n = cons_r + 16'd1;
if (len_r < min_len(byte_data)) begin
// ---- Shorter than the type requires. ----
//
// The record cannot hold the fields its type is defined to
// have, so every field the host reads from it comes from the
// NEXT record. The walk stops, because continuing from here
// produces a cascade of nonsense.
er_n = 1'b1; ec_n = E_SHORT;
st_n = W_DEAD;
end else if (byte_data == T_ENDPOINT) begin
if (!have_if_r) begin
// An endpoint belongs to an interface. One that appears
// before any interface has no owner, and the host will
// attribute it to whatever interface comes next.
er_n = 1'b1; ec_n = E_ORPHAN;
st_n = W_DEAD;
end else begin
eps_n = eps_r + 8'd1;
// Written as if/else rather than a ternary: an enum-valued
// ternary needs an explicit cast in Icarus.
if (len_r == 8'd2) st_n = W_LEN; else st_n = W_BODY;
end
end else if (byte_data == T_INTERFACE) begin
// Close the PREVIOUS interface before opening this one.
if (have_if_r && (eps_r != epe_r)) begin
er_n = 1'b1; ec_n = E_EPCOUNT;
st_n = W_DEAD;
end else begin
hif_n = 1'b1;
ifs_n = ifs_r + 8'd1;
eps_n = 8'd0;
// Written as if/else rather than a ternary: an enum-valued
// ternary needs an explicit cast in Icarus.
if (len_r == 8'd2) st_n = W_LEN; else st_n = W_BODY;
end
end else begin
if (byte_data == T_CONFIG) hcfg_n = 1'b1;
if (len_r == 8'd2) st_n = W_LEN; else st_n = W_BODY;
end
end
W_BODY: begin
cons_n = cons_r + 16'd1;
off_n = off_r + 8'd1;
// The type-specific fields, picked out by offset. This is the
// only place the validator knows anything about what a
// descriptor MEANS rather than how long it is.
if (typ_r == T_CONFIG) begin
if (off_r == 8'd2) total_n = {total_r[15:8], byte_data};
if (off_r == 8'd3) total_n = {byte_data, total_r[7:0]};
if (off_r == 8'd4) ife_n = byte_data;
end else if (typ_r == T_INTERFACE) begin
if (off_r == 8'd4) epe_n = byte_data;
end
if (off_r + 8'd1 >= len_r) st_n = W_LEN;
// ---- The walk must not run past what the device said. ----
//
// Checked as the bytes are consumed rather than at the end,
// because by the end the pointer is somewhere meaningless and the
// report would name whatever record it happened to land in.
if (hcfg_n && (total_r != 16'd0) && (cons_n > total_r)) begin
er_n = 1'b1; ec_n = E_OVERRUN;
st_n = W_DEAD;
end
end
default: begin
// W_IDLE and W_DEAD: bytes are consumed and ignored. A dead walk
// does not produce one error per remaining byte -- chapter 23.4's
// rule, applied to a descriptor.
cons_n = cons_r + 16'd1;
end
endcase
end
end
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
st_r <= W_IDLE;
len_r <= 8'd0;
typ_r <= 8'd0;
off_r <= 8'd0;
cons_r <= 16'd0;
total_r <= 16'd0;
ifs_r <= 8'd0;
ife_r <= 8'd0;
eps_r <= 8'd0;
epe_r <= 8'd0;
have_if_r <= 1'b0;
have_cfg_r <= 1'b0;
ec_r <= E_NONE;
er_r <= 1'b0;
ok_r <= 1'b0;
n_desc <= 32'd0;
n_zerolen <= 32'd0;
n_short <= 32'd0;
n_overrun <= 32'd0;
n_total <= 32'd0;
n_ifcount <= 32'd0;
n_epcount <= 32'd0;
n_orphan <= 32'd0;
n_sets_ok <= 32'd0;
end else begin
st_r <= st_n;
len_r <= len_n;
typ_r <= typ_n;
off_r <= off_n;
cons_r <= cons_n;
total_r <= total_n;
ifs_r <= ifs_n;
ife_r <= ife_n;
eps_r <= eps_n;
epe_r <= epe_n;
have_if_r <= hif_n;
have_cfg_r <= hcfg_n;
ec_r <= ec_n;
er_r <= er_n;
ok_r <= ok_n;
if (byte_valid && (st_r == W_TYPE) && !sod && !eod && !eot)
n_desc <= n_desc + 32'd1;
if (ok_n) n_sets_ok <= n_sets_ok + 32'd1;
// The per-cause counters are driven by the SAME pulse as the error,
// so they sum to it by construction (chapter 23.4).
if (er_n) begin
case (ec_n)
E_ZEROLEN: n_zerolen <= n_zerolen + 32'd1;
E_SHORT: n_short <= n_short + 32'd1;
E_OVERRUN: n_overrun <= n_overrun + 32'd1;
E_TOTAL: n_total <= n_total + 32'd1;
E_IFCOUNT: n_ifcount <= n_ifcount + 32'd1;
E_EPCOUNT: n_epcount <= n_epcount + 32'd1;
E_ORPHAN: n_orphan <= n_orphan + 32'd1;
default: ;
endcase
end
end
end
endmodule8. VHDL-2008 Implementation
-- usb_descriptor_validator -- a descriptor is a self-describing tree, and
-- the three numbers that describe it must agree.
--
-- THE SHAPE OF THE THING
--
-- A configuration descriptor set is one buffer containing a chain of
-- variable-length records:
--
-- offset 0 bLength how long THIS record is
-- offset 1 bDescriptorType what it is
-- ... type-specific fields
--
-- CONFIGURATION (type 2) offset 2..3 wTotalLength
-- offset 4 bNumInterfaces
-- INTERFACE (type 4) offset 4 bNumEndpoints
-- ENDPOINT (type 5)
--
-- So the same buffer is described THREE times, by three different fields,
-- and all three must agree:
--
-- wTotalLength == the sum of every bLength in the set
-- bNumInterfaces == the number of INTERFACE records present
-- bNumEndpoints == the number of ENDPOINT records after each one
--
-- HOW THE HOST READS IT, AND WHY DISAGREEMENT IS SO CONFUSING
--
-- The host asks for wTotalLength bytes, and then walks the buffer by
-- ADDING bLength to a pointer. Those are two different fields doing two
-- different jobs, and when they disagree the host does not notice anything
-- wrong at the point of disagreement -- it lands in the MIDDLE of the next
-- record and reads its fields from the wrong offsets.
--
-- The error is reported against a field that is perfectly correct,
-- several records after the one that was wrong.
--
-- That is why descriptor bugs are so disproportionately painful: the
-- symptom and the cause are separated by a variable number of bytes, and
-- the symptom is a plausible-looking value in an unrelated field.
--
-- AND bLength = 0 IS NOT AN ERROR, IT IS A HANG
--
-- The walk advances by bLength. A record claiming zero length advances the
-- pointer by zero, and the host reads the same record for ever.
--
-- A host that does not bound its descriptor walk HANGS on a
-- device that returns a zero bLength. Not fails -- hangs.
--
-- Which is why this validator treats a zero bLength as its own error class
-- rather than folding it into "too short", and why it stops walking.
--
-- A DESCRIPTOR SHORTER THAN ITS TYPE REQUIRES IS A DIFFERENT BUG
--
-- bLength below the minimum for the type means the record cannot contain
-- the fields the type is defined to have -- so every field the host reads
-- from it comes from the NEXT record. It is the most common single
-- descriptor defect and the one that produces the strangest reports.
library ieee;
use ieee.std_logic_1164.all;
package usb_desc_pkg is
-- The walker's states. W_DEAD exists so that a walk which has already
-- stopped does not report the same defect again under a different name
-- at the end of the buffer.
type walk_state_t is (W_IDLE, W_LEN, W_TYPE, W_BODY, W_DEAD);
-- E_ZEROLEN is separate from E_SHORT on purpose: a zero length is a HANG
-- and a short length is a wrong value, and a reader needs to know which
-- symptom to expect.
type desc_err_t is (E_NONE, E_ZEROLEN, E_SHORT, E_OVERRUN, E_TOTAL,
E_IFCOUNT, E_EPCOUNT, E_ORPHAN);
function w_code (w : walk_state_t) return std_logic_vector;
function e_code (e : desc_err_t) return std_logic_vector;
end package usb_desc_pkg;
package body usb_desc_pkg is
function w_code (w : walk_state_t) return std_logic_vector is
begin
case w is
when W_IDLE => return "000";
when W_LEN => return "001";
when W_TYPE => return "010";
when W_BODY => return "011";
when W_DEAD => return "100";
end case;
end function;
function e_code (e : desc_err_t) return std_logic_vector is
begin
case e is
when E_NONE => return "000";
when E_ZEROLEN => return "001";
when E_SHORT => return "010";
when E_OVERRUN => return "011";
when E_TOTAL => return "100";
when E_IFCOUNT => return "101";
when E_EPCOUNT => return "110";
when E_ORPHAN => return "111";
end case;
end function;
end package body usb_desc_pkg;
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
use work.usb_desc_pkg.all;
entity usb_descriptor_validator is
generic (
MIN_CFG : integer := 9; -- minimum bLength for CONFIGURATION
MIN_IF : integer := 9; -- ...for INTERFACE
MIN_EP : integer := 7; -- ...for ENDPOINT
MIN_ANY : integer := 2 -- ...for anything else: bLength + bType
);
port (
clk : in std_logic;
rst_n : in std_logic;
sod : in std_logic; -- start of a set
byte_valid : in std_logic;
byte_data : in std_logic_vector(7 downto 0);
eod : in std_logic; -- the host stopped
eot : in std_logic;
state : out std_logic_vector(2 downto 0);
cur_type : out std_logic_vector(7 downto 0);
cur_len : out std_logic_vector(7 downto 0);
consumed : out std_logic_vector(15 downto 0);
total_len : out std_logic_vector(15 downto 0);
if_seen : out std_logic_vector(7 downto 0);
if_expected : out std_logic_vector(7 downto 0);
ep_seen : out std_logic_vector(7 downto 0);
ep_expected : out std_logic_vector(7 downto 0);
err_pulse : out std_logic;
err_code : out std_logic_vector(2 downto 0);
set_ok : out std_logic;
n_desc : out std_logic_vector(31 downto 0);
n_zerolen : out std_logic_vector(31 downto 0);
n_short : out std_logic_vector(31 downto 0);
n_overrun : out std_logic_vector(31 downto 0);
n_total : out std_logic_vector(31 downto 0);
n_ifcount : out std_logic_vector(31 downto 0);
n_epcount : out std_logic_vector(31 downto 0);
n_orphan : out std_logic_vector(31 downto 0);
n_sets_ok : out std_logic_vector(31 downto 0)
);
end entity usb_descriptor_validator;
architecture rtl of usb_descriptor_validator is
constant T_CONFIG : std_logic_vector(7 downto 0) := x"02";
constant T_INTERFACE : std_logic_vector(7 downto 0) := x"04";
constant T_ENDPOINT : std_logic_vector(7 downto 0) := x"05";
signal st_r : walk_state_t := W_IDLE;
signal ec_r : desc_err_t := E_NONE;
signal len_r, typ_r, off_r : unsigned(7 downto 0) := (others => '0');
signal cons_r, total_r : unsigned(15 downto 0) := (others => '0');
signal ifs_r, ife_r, eps_r, epe_r : unsigned(7 downto 0) := (others => '0');
signal have_if_r, have_cfg_r : std_logic := '0';
signal er_r, ok_r : std_logic := '0';
-- The minimum bLength a record of this type can possibly have. A record
-- below it cannot contain the fields the type is DEFINED to have, so
-- every field read from it comes from the next record along.
function min_len (t : std_logic_vector(7 downto 0)) return unsigned is
begin
if t = T_CONFIG then
return to_unsigned(MIN_CFG, 8);
elsif t = T_INTERFACE then
return to_unsigned(MIN_IF, 8);
elsif t = T_ENDPOINT then
return to_unsigned(MIN_EP, 8);
else
return to_unsigned(MIN_ANY, 8);
end if;
end function;
-- Accumulators are held as unsigned rather than as range-constrained
-- integers: a constrained integer aborts simulation on overflow, which
-- turns a mutation into a crash instead of a measured kill.
signal c_d, c_zl, c_sh, c_ov, c_to : unsigned(31 downto 0) := (others => '0');
signal c_ic, c_ep, c_or, c_ok : unsigned(31 downto 0) := (others => '0');
begin
state <= w_code(st_r);
cur_type <= std_logic_vector(typ_r);
cur_len <= std_logic_vector(len_r);
consumed <= std_logic_vector(cons_r);
total_len <= std_logic_vector(total_r);
if_seen <= std_logic_vector(ifs_r);
if_expected <= std_logic_vector(ife_r);
ep_seen <= std_logic_vector(eps_r);
ep_expected <= std_logic_vector(epe_r);
err_pulse <= er_r;
err_code <= e_code(ec_r);
set_ok <= ok_r;
n_desc <= std_logic_vector(c_d);
n_zerolen <= std_logic_vector(c_zl);
n_short <= std_logic_vector(c_sh);
n_overrun <= std_logic_vector(c_ov);
n_total <= std_logic_vector(c_to);
n_ifcount <= std_logic_vector(c_ic);
n_epcount <= std_logic_vector(c_ep);
n_orphan <= std_logic_vector(c_or);
n_sets_ok <= std_logic_vector(c_ok);
process (clk, rst_n)
variable ns : walk_state_t;
variable nec : desc_err_t;
variable nlen, ntyp, noff : unsigned(7 downto 0);
variable ncons, ntot : unsigned(15 downto 0);
variable nifs, nife, neps, nepe : unsigned(7 downto 0);
variable nhif, nhcfg, ner, nok : std_logic;
variable bd : unsigned(7 downto 0);
begin
if rst_n = '0' then
st_r <= W_IDLE; ec_r <= E_NONE;
len_r <= (others => '0'); typ_r <= (others => '0');
off_r <= (others => '0');
cons_r <= (others => '0'); total_r <= (others => '0');
ifs_r <= (others => '0'); ife_r <= (others => '0');
eps_r <= (others => '0'); epe_r <= (others => '0');
have_if_r <= '0'; have_cfg_r <= '0';
er_r <= '0'; ok_r <= '0';
c_d <= (others => '0'); c_zl <= (others => '0');
c_sh <= (others => '0'); c_ov <= (others => '0');
c_to <= (others => '0'); c_ic <= (others => '0');
c_ep <= (others => '0'); c_or <= (others => '0');
c_ok <= (others => '0');
elsif rising_edge(clk) then
bd := unsigned(byte_data);
ns := st_r; nlen := len_r; ntyp := typ_r; noff := off_r;
ncons := cons_r; ntot := total_r;
nifs := ifs_r; nife := ife_r; neps := eps_r; nepe := epe_r;
nhif := have_if_r; nhcfg := have_cfg_r;
nec := E_NONE; ner := '0'; nok := '0';
if eot = '1' then
ns := W_IDLE;
elsif sod = '1' then
-- A fresh descriptor set. Everything the previous set accumulated is
-- gone: a stale interface count carried across a set boundary would
-- report a mismatch against a descriptor that is perfectly correct.
ns := W_LEN;
nlen := (others => '0'); ntyp := (others => '0');
noff := (others => '0');
ncons := (others => '0'); ntot := (others => '0');
nifs := (others => '0'); nife := (others => '0');
neps := (others => '0'); nepe := (others => '0');
nhif := '0'; nhcfg := '0';
elsif eod = '1' then
-- ---- THE END OF THE SET. The aggregate checks happen HERE, and
-- ---- they are the ones that cannot be made record by record.
--
-- W_DEAD is excluded deliberately. A walk that has already stopped
-- has already said why, and the aggregate counts it left behind are
-- wrong BECAUSE it stopped -- so re-checking them reports the same
-- defect a second time under a different name. One descriptor set,
-- one finding, which is chapter 23.4's rule applied to a buffer.
if st_r /= W_IDLE and st_r /= W_DEAD then
if nhif = '1' and eps_r /= epe_r then
-- The last interface's endpoint count. Every other interface was
-- checked when the NEXT one started; the last one has no next.
ner := '1'; nec := E_EPCOUNT;
elsif nhcfg = '1' and cons_r /= total_r then
ner := '1'; nec := E_TOTAL;
elsif nhcfg = '1' and ifs_r /= ife_r then
ner := '1'; nec := E_IFCOUNT;
elsif st_r = W_LEN then
-- Landed exactly on a record boundary with everything agreeing.
nok := '1';
else
-- The set ended in the middle of a record.
ner := '1'; nec := E_OVERRUN;
end if;
end if;
ns := W_IDLE;
elsif byte_valid = '1' then
case st_r is
when W_LEN =>
nlen := bd; noff := to_unsigned(1, 8); ncons := cons_r + 1;
if bd = 0 then
-- ---- A ZERO LENGTH IS A HANG, NOT AN ERROR. ----
--
-- The walk advances by bLength. Zero advances it by nothing,
-- so a host that does not bound its walk reads this record
-- for ever. Stopping is the only safe response, and saying so
-- as its own error class is what tells the reader that the
-- symptom will be a hang rather than a wrong value.
ner := '1'; nec := E_ZEROLEN; ns := W_DEAD;
else
ns := W_TYPE;
end if;
when W_TYPE =>
ntyp := bd; noff := to_unsigned(2, 8); ncons := cons_r + 1;
if len_r < min_len(byte_data) then
-- ---- Shorter than the type requires. ----
ner := '1'; nec := E_SHORT; ns := W_DEAD;
elsif byte_data = T_ENDPOINT then
if have_if_r = '0' then
-- An endpoint belongs to an interface. One that appears
-- before any interface has no owner, and the host will
-- attribute it to whatever interface comes next.
ner := '1'; nec := E_ORPHAN; ns := W_DEAD;
else
neps := eps_r + 1;
if len_r = 2 then ns := W_LEN; else ns := W_BODY; end if;
end if;
elsif byte_data = T_INTERFACE then
-- Close the PREVIOUS interface before opening this one.
if have_if_r = '1' and eps_r /= epe_r then
ner := '1'; nec := E_EPCOUNT; ns := W_DEAD;
else
nhif := '1'; nifs := ifs_r + 1; neps := (others => '0');
if len_r = 2 then ns := W_LEN; else ns := W_BODY; end if;
end if;
else
if byte_data = T_CONFIG then nhcfg := '1'; end if;
if len_r = 2 then ns := W_LEN; else ns := W_BODY; end if;
end if;
when W_BODY =>
ncons := cons_r + 1; noff := off_r + 1;
-- The type-specific fields, picked out by offset. This is the
-- only place the validator knows anything about what a
-- descriptor MEANS rather than how long it is.
if typ_r = unsigned(T_CONFIG) then
if off_r = 2 then ntot := total_r(15 downto 8) & bd; end if;
if off_r = 3 then ntot := bd & total_r(7 downto 0); end if;
if off_r = 4 then nife := bd; end if;
elsif typ_r = unsigned(T_INTERFACE) then
if off_r = 4 then nepe := bd; end if;
end if;
if off_r + 1 >= len_r then ns := W_LEN; end if;
-- ---- The walk must not run past what the device said. ----
--
-- Checked as the bytes are consumed rather than at the end,
-- because by the end the pointer is somewhere meaningless and
-- the report would name whatever record it happened to land in.
if nhcfg = '1' and total_r /= 0 and ncons > total_r then
ner := '1'; nec := E_OVERRUN; ns := W_DEAD;
end if;
when others =>
-- W_IDLE and W_DEAD: bytes are consumed and ignored. A dead walk
-- does not produce one error per remaining byte -- chapter
-- 23.4's rule, applied to a descriptor.
ncons := cons_r + 1;
end case;
end if;
if byte_valid = '1' and st_r = W_TYPE and sod = '0'
and eod = '0' and eot = '0' then
c_d <= c_d + 1;
end if;
st_r <= ns; len_r <= nlen; typ_r <= ntyp; off_r <= noff;
cons_r <= ncons; total_r <= ntot;
ifs_r <= nifs; ife_r <= nife; eps_r <= neps; epe_r <= nepe;
have_if_r <= nhif; have_cfg_r <= nhcfg;
ec_r <= nec; er_r <= ner; ok_r <= nok;
if nok = '1' then c_ok <= c_ok + 1; end if;
-- The per-cause counters are driven by the SAME pulse as the error,
-- so they sum to it by construction (chapter 23.4).
if ner = '1' then
case nec is
when E_ZEROLEN => c_zl <= c_zl + 1;
when E_SHORT => c_sh <= c_sh + 1;
when E_OVERRUN => c_ov <= c_ov + 1;
when E_TOTAL => c_to <= c_to + 1;
when E_IFCOUNT => c_ic <= c_ic + 1;
when E_EPCOUNT => c_ep <= c_ep + 1;
when E_ORPHAN => c_or <= c_or + 1;
when others => null;
end case;
end if;
end if;
end process;
end architecture rtl;9. Seeing the Walk Stop
A two-byte record, then a record claiming zero length
usb_descriptor_validator — a zero bLength stops the walk
10 cycles10. The Testbenches
Two exhaustive sweeps, and the first is the one worth reading:
ALL 256 VALUES of bLength on the first record of a set,
with the type fixed at CONFIGURATION.
The validator must reject exactly three populations:
bLength == 0 ZEROLEN -- a hang
0 < bLength < MIN_CFG SHORT -- a wrong value
everything else accepted
and it must reject zero with a DIFFERENT code, because a
reader needs to know which symptom to expect. if (L == 0) begin
check(n_zerolen == b_e + 1,
"bLength zero was not reported as a zero length");
check(n_short == b_ok,
"bLength zero was classified as SHORT -- zero is a hang and short is a wrong value, and a reader needs to know which symptom to expect");
end else if (L < MIN_CFG) begin
check(n_short == b_ok + 1,
"a CONFIGURATION below its minimum length was accepted");
check(n_zerolen == b_e, "a short length was classified as zero");
end else begin
check(n_zerolen == b_e && n_short == b_ok,
"a bLength at or above the type's minimum was rejected");
endAnd the false-positive half — 180 perfectly consistent sets of three different shapes, including the two that a careless validator rejects: an interface with no endpoints at all, and a set with two interfaces.
10.1 Verilog testbench
// Testbench for usb_descriptor_validator (Verilog-2005).
//
// WHAT IS EXHAUSTIVE HERE
//
// 1. ALL 256 VALUES of bLength on the first record of a set, with the
// type fixed at CONFIGURATION. The validator must reject exactly
// three populations -- zero, everything below the type's minimum, and
// nothing else -- and it must reject zero with a DIFFERENT code,
// because zero is a hang and short is a wrong value.
//
// 2. Every walker state crossed with all eight combinations of
// {byte_valid, sod, eod} = 40 pairs, each state reached by real bytes.
//
// AND THE SET THE CHAPTER IS ABOUT
//
// A descriptor set whose wTotalLength disagrees with the sum of its
// bLengths by ONE byte. The host reads the right number of bytes and lands
// one byte into the next record, so every field it reads after that point
// comes from the wrong offset -- and the error it eventually reports is
// against a field that is perfectly correct.
`timescale 1ns/1ps
module tb_dv_v;
localparam integer MIN_CFG = 9;
localparam integer MIN_IF = 9;
localparam integer MIN_EP = 7;
localparam integer MIN_ANY = 2;
localparam [2:0] W_IDLE=3'd0, W_LEN=3'd1, W_TYPE=3'd2, W_BODY=3'd3, W_DEAD=3'd4;
localparam [2:0] E_NONE=3'd0, E_ZEROLEN=3'd1, E_SHORT=3'd2, E_OVERRUN=3'd3,
E_TOTAL=3'd4, E_IFCOUNT=3'd5, E_EPCOUNT=3'd6, E_ORPHAN=3'd7;
localparam [7:0] T_DEVICE=8'd1, T_CONFIG=8'd2, T_STRING=8'd3,
T_INTERFACE=8'd4, T_ENDPOINT=8'd5;
reg clk = 1'b0, rst_n = 1'b0;
reg sod = 1'b0, byte_valid = 1'b0, eod = 1'b0, eot = 1'b0;
reg [7:0] byte_data = 8'd0;
wire [2:0] state, err_code;
wire [7:0] cur_type, cur_len, if_seen, if_expected, ep_seen, ep_expected;
wire [15:0] consumed, total_len;
wire err_pulse, set_ok;
wire [31:0] n_desc, n_zerolen, n_short, n_overrun, n_total,
n_ifcount, n_epcount, n_orphan, n_sets_ok;
usb_descriptor_validator #(.MIN_CFG(MIN_CFG), .MIN_IF(MIN_IF),
.MIN_EP(MIN_EP), .MIN_ANY(MIN_ANY)) dut (
.clk(clk), .rst_n(rst_n),
.sod(sod), .byte_valid(byte_valid), .byte_data(byte_data),
.eod(eod), .eot(eot),
.state(state), .cur_type(cur_type), .cur_len(cur_len),
.consumed(consumed), .total_len(total_len),
.if_seen(if_seen), .if_expected(if_expected),
.ep_seen(ep_seen), .ep_expected(ep_expected),
.err_pulse(err_pulse), .err_code(err_code), .set_ok(set_ok),
.n_desc(n_desc), .n_zerolen(n_zerolen), .n_short(n_short),
.n_overrun(n_overrun), .n_total(n_total), .n_ifcount(n_ifcount),
.n_epcount(n_epcount), .n_orphan(n_orphan), .n_sets_ok(n_sets_ok)
);
always #5 clk = ~clk;
integer errors = 0, checks = 0;
task check(input cond, input [1023:0] msg);
begin
checks = checks + 1;
if (!cond) begin
errors = errors + 1;
if (errors <= 25)
$display("FAIL @%0t: %0s | st=%0d typ=%0d len=%0d cons=%0d tot=%0d if=%0d/%0d ep=%0d/%0d e=%b(%0d)",
$time, msg, state, cur_type, cur_len, consumed, total_len,
if_seen, if_expected, ep_seen, ep_expected, err_pulse, err_code);
end
end
endtask
// ------------------------------------------------------------------
// The shadow walker. Written from the descriptor format, not the design.
// ------------------------------------------------------------------
reg [2:0] m_st, m_ec;
reg [7:0] m_len, m_typ, m_off, m_ifs, m_ife, m_eps, m_epe;
reg [15:0] m_cons, m_tot;
reg m_hif, m_hcfg, m_er, m_ok;
integer m_d, m_zl, m_sh, m_ov, m_to, m_ic, m_ep, m_or, m_sok;
integer seen_len [0:255]; // all 256 bLength values on record 0
integer seen_si [0:39]; // 5 states x 8 input combinations
integer n_len, n_si, n_steps;
task model_reset;
integer j;
begin
m_st = W_IDLE; m_ec = E_NONE;
m_len = 8'd0; m_typ = 8'd0; m_off = 8'd0;
m_ifs = 8'd0; m_ife = 8'd0; m_eps = 8'd0; m_epe = 8'd0;
m_cons = 16'd0; m_tot = 16'd0;
m_hif = 1'b0; m_hcfg = 1'b0; m_er = 1'b0; m_ok = 1'b0;
m_d = 0; m_zl = 0; m_sh = 0; m_ov = 0; m_to = 0;
m_ic = 0; m_ep = 0; m_or = 0; m_sok = 0;
for (j = 0; j < 256; j = j + 1) seen_len[j] = 0;
for (j = 0; j < 40; j = j + 1) seen_si[j] = 0;
n_len = 0; n_si = 0; n_steps = 0;
end
endtask
function [7:0] min_len;
input [7:0] t;
begin
if (t == T_CONFIG) min_len = MIN_CFG[7:0];
else if (t == T_INTERFACE) min_len = MIN_IF[7:0];
else if (t == T_ENDPOINT) min_len = MIN_EP[7:0];
else min_len = MIN_ANY[7:0];
end
endfunction
integer idx;
task step(input sd, input bv, input [7:0] bd, input ed, input eo);
reg [2:0] ns, nec;
reg [7:0] nlen, ntyp, noff, nifs, nife, neps, nepe;
reg [15:0] ncons, ntot;
reg nhif, nhcfg, ner, nok;
begin
sod = sd; byte_valid = bv; byte_data = bd; eod = ed; eot = eo;
#1;
check(state === m_st, "state disagrees with the shadow walker");
check(cur_type === m_typ, "cur_type disagrees");
check(cur_len === m_len, "cur_len disagrees");
check(consumed === m_cons, "consumed disagrees -- the walk is not the length the model says");
check(total_len === m_tot, "total_len disagrees");
check(if_seen === m_ifs, "if_seen disagrees");
check(if_expected === m_ife, "if_expected disagrees");
check(ep_seen === m_eps, "ep_seen disagrees");
check(ep_expected === m_epe, "ep_expected disagrees");
check(err_code === m_ec, "err_code disagrees");
check(err_pulse === m_er, "the error pulse disagrees");
check(set_ok === m_ok, "the set_ok pulse disagrees");
check(!(err_pulse && set_ok),
"a descriptor set was reported consistent and broken in the same cycle");
idx = m_st * 8 + (sd ? 4 : 0) + (bv ? 2 : 0) + (ed ? 1 : 0);
if (idx < 40) begin
if (seen_si[idx] == 0) begin seen_si[idx] = 1; n_si = n_si + 1; end
end
n_steps = n_steps + 1;
// ---- advance the shadow walker ----
ns = m_st; nlen = m_len; ntyp = m_typ; noff = m_off;
ncons = m_cons; ntot = m_tot;
nifs = m_ifs; nife = m_ife; neps = m_eps; nepe = m_epe;
nhif = m_hif; nhcfg = m_hcfg;
nec = E_NONE; ner = 1'b0; nok = 1'b0;
if (eo) begin
ns = W_IDLE;
end else if (sd) begin
ns = W_LEN; nlen = 8'd0; ntyp = 8'd0; noff = 8'd0;
ncons = 16'd0; ntot = 16'd0;
nifs = 8'd0; nife = 8'd0; neps = 8'd0; nepe = 8'd0;
nhif = 1'b0; nhcfg = 1'b0;
end else if (ed) begin
if ((m_st != W_IDLE) && (m_st != W_DEAD)) begin
if (nhif && (m_eps != m_epe)) begin
ner = 1'b1; nec = E_EPCOUNT;
end else if (nhcfg && (m_cons != m_tot)) begin
ner = 1'b1; nec = E_TOTAL;
end else if (nhcfg && (m_ifs != m_ife)) begin
ner = 1'b1; nec = E_IFCOUNT;
end else if (m_st == W_LEN) begin
nok = 1'b1;
end else begin
ner = 1'b1; nec = E_OVERRUN;
end
end
ns = W_IDLE;
end else if (bv) begin
if (m_st == W_LEN) begin
nlen = bd; noff = 8'd1; ncons = m_cons + 16'd1;
if (bd == 8'd0) begin
ner = 1'b1; nec = E_ZEROLEN; ns = W_DEAD;
end else ns = W_TYPE;
end else if (m_st == W_TYPE) begin
ntyp = bd; noff = 8'd2; ncons = m_cons + 16'd1;
if (m_len < min_len(bd)) begin
ner = 1'b1; nec = E_SHORT; ns = W_DEAD;
end else if (bd == T_ENDPOINT) begin
if (!m_hif) begin
ner = 1'b1; nec = E_ORPHAN; ns = W_DEAD;
end else begin
neps = m_eps + 8'd1;
ns = (m_len == 8'd2) ? W_LEN : W_BODY;
end
end else if (bd == T_INTERFACE) begin
if (m_hif && (m_eps != m_epe)) begin
ner = 1'b1; nec = E_EPCOUNT; ns = W_DEAD;
end else begin
nhif = 1'b1; nifs = m_ifs + 8'd1; neps = 8'd0;
ns = (m_len == 8'd2) ? W_LEN : W_BODY;
end
end else begin
if (bd == T_CONFIG) nhcfg = 1'b1;
ns = (m_len == 8'd2) ? W_LEN : W_BODY;
end
end else if (m_st == W_BODY) begin
ncons = m_cons + 16'd1; noff = m_off + 8'd1;
if (m_typ == T_CONFIG) begin
if (m_off == 8'd2) ntot = {m_tot[15:8], bd};
if (m_off == 8'd3) ntot = {bd, m_tot[7:0]};
if (m_off == 8'd4) nife = bd;
end else if (m_typ == T_INTERFACE) begin
if (m_off == 8'd4) nepe = bd;
end
if (m_off + 8'd1 >= m_len) ns = W_LEN;
if (nhcfg && (m_tot != 16'd0) && (ncons > m_tot)) begin
ner = 1'b1; nec = E_OVERRUN; ns = W_DEAD;
end
end else begin
ncons = m_cons + 16'd1;
end
end
if (bv && (m_st == W_TYPE) && !sd && !ed && !eo) m_d = m_d + 1;
m_st = ns; m_len = nlen; m_typ = ntyp; m_off = noff;
m_cons = ncons; m_tot = ntot;
m_ifs = nifs; m_ife = nife; m_eps = neps; m_epe = nepe;
m_hif = nhif; m_hcfg = nhcfg;
m_ec = nec; m_er = ner; m_ok = nok;
if (nok) m_sok = m_sok + 1;
if (ner) begin
case (nec)
E_ZEROLEN: m_zl = m_zl + 1;
E_SHORT: m_sh = m_sh + 1;
E_OVERRUN: m_ov = m_ov + 1;
E_TOTAL: m_to = m_to + 1;
E_IFCOUNT: m_ic = m_ic + 1;
E_EPCOUNT: m_ep = m_ep + 1;
E_ORPHAN: m_or = m_or + 1;
default: ;
endcase
end
@(posedge clk); #1;
sod = 1'b0; byte_valid = 1'b0; eod = 1'b0; eot = 1'b0;
end
endtask
task put(input [7:0] b);
begin step(1'b0, 1'b1, b, 1'b0, 1'b0); end
endtask
task start_set;
begin step(1'b1, 1'b0, 8'd0, 1'b0, 1'b0); end
endtask
task end_set;
begin step(1'b0, 1'b0, 8'd0, 1'b1, 1'b0); end
endtask
task nop(input integer n);
integer j;
begin
for (j = 0; j < n; j = j + 1) step(1'b0, 1'b0, 8'd0, 1'b0, 1'b0);
end
endtask
// Emit a record whose bLength FIELD and actual byte COUNT can differ --
// which is the whole point, because they differ in the bug this chapter
// is about.
integer ei;
task emit(input [7:0] blen_field, input integer nbytes, input [7:0] typ,
input [7:0] b2, input [7:0] b3, input [7:0] b4);
begin
for (ei = 0; ei < nbytes; ei = ei + 1) begin
case (ei)
0: put(blen_field);
1: put(typ);
2: put(b2);
3: put(b3);
4: put(b4);
default: put(8'h00);
endcase
end
end
endtask
// The canonical set: CONFIG(9) + INTERFACE(9) + ENDPOINT(7) x2 = 32
task legal_set;
begin
start_set;
emit(8'd9, 9, T_CONFIG, 8'd32, 8'd0, 8'd1);
emit(8'd9, 9, T_INTERFACE, 8'd0, 8'd0, 8'd2);
emit(8'd7, 7, T_ENDPOINT, 8'h81, 8'h02, 8'd0);
emit(8'd7, 7, T_ENDPOINT, 8'h02, 8'h02, 8'd0);
end_set;
end
endtask
integer k, b_e, b_ok, L, st2, cb, total_now;
initial begin
model_reset;
repeat (3) @(posedge clk);
rst_n = 1'b1;
@(posedge clk); #1;
// ---- Phase A: the state after reset ----
check(state === W_IDLE, "reset did not land idle");
check(err_pulse === 1'b0, "reset reported a descriptor error");
// ---- Phase B: legal sets of several shapes. ZERO errors. ----
for (k = 0; k < 60; k = k + 1) begin
b_ok = n_sets_ok;
b_e = n_zerolen + n_short + n_overrun + n_total + n_ifcount
+ n_epcount + n_orphan;
legal_set;
check(n_sets_ok == b_ok + 1,
"a perfectly consistent descriptor set was not accepted");
check(n_zerolen + n_short + n_overrun + n_total + n_ifcount
+ n_epcount + n_orphan == b_e,
"a perfectly consistent descriptor set produced an error -- a validator with false positives is a validator somebody switches off, and then nobody is checking descriptors at all");
// an interface with NO endpoints is perfectly legal
b_ok = n_sets_ok;
start_set;
emit(8'd9, 9, T_CONFIG, 8'd18, 8'd0, 8'd1);
emit(8'd9, 9, T_INTERFACE, 8'd0, 8'd0, 8'd0);
end_set;
check(n_sets_ok == b_ok + 1,
"an interface with no endpoints was rejected, and that is a legal descriptor");
// TWO interfaces, one endpoint each
b_ok = n_sets_ok;
start_set;
emit(8'd9, 9, T_CONFIG, 8'd41, 8'd0, 8'd2);
emit(8'd9, 9, T_INTERFACE, 8'd0, 8'd0, 8'd1);
emit(8'd7, 7, T_ENDPOINT, 8'h81, 8'h03, 8'd0);
emit(8'd9, 9, T_INTERFACE, 8'd1, 8'd0, 8'd1);
emit(8'd7, 7, T_ENDPOINT, 8'h02, 8'h02, 8'd0);
end_set;
check(n_sets_ok == b_ok + 1, "a two-interface set was rejected");
end
// ---- Phase C: bLength = 0 is a HANG, and has its own code. ----
for (k = 0; k < 20; k = k + 1) begin
b_e = n_zerolen;
start_set;
emit(8'd9, 9, T_CONFIG, 8'd25, 8'd0, 8'd1);
put(8'd0); // a record claiming zero length
end_set;
check(n_zerolen == b_e + 1,
"a zero bLength was not reported as its own class -- the walk advances by bLength, so zero advances it by nothing and a host that does not bound its walk HANGS rather than failing");
check(state === W_IDLE, "the walker did not stop after a zero length");
end
// ---- Phase D: shorter than the type requires, for each type. ----
for (L = 2; L < MIN_CFG; L = L + 1) begin
b_e = n_short;
start_set;
emit(L[7:0], L, T_CONFIG, 8'd20, 8'd0, 8'd1);
end_set;
check(n_short == b_e + 1,
"a CONFIGURATION shorter than its minimum was accepted -- the record cannot hold the fields its type is defined to have, so every field the host reads from it comes from the NEXT record");
end
for (L = 2; L < MIN_EP; L = L + 1) begin
b_e = n_short;
start_set;
// wTotalLength is set to what the set ACTUALLY is (9 + 9 + L), so the
// overrun check cannot fire first and mask the one under test. An
// injected defect that is pre-empted by a different injected defect
// tests neither.
emit(8'd9, 9, T_CONFIG, (8'd18 + L[7:0]), 8'd0, 8'd1);
emit(8'd9, 9, T_INTERFACE, 8'd0, 8'd0, 8'd1);
emit(L[7:0], L, T_ENDPOINT, 8'h81, 8'h02, 8'd0);
end_set;
check(n_short == b_e + 1, "an ENDPOINT shorter than its minimum was accepted");
end
// ---- Phase E: THE CHAPTER. wTotalLength disagrees with the sum. ----
//
// Off by ONE byte. The host reads the right number of bytes and lands
// one byte into the next record, so every field after that point comes
// from the wrong offset.
for (k = 0; k < 20; k = k + 1) begin
// declared LONGER than the set actually is
b_e = n_total;
start_set;
emit(8'd9, 9, T_CONFIG, 8'd33, 8'd0, 8'd1); // says 33, is 32
emit(8'd9, 9, T_INTERFACE, 8'd0, 8'd0, 8'd2);
emit(8'd7, 7, T_ENDPOINT, 8'h81, 8'h02, 8'd0);
emit(8'd7, 7, T_ENDPOINT, 8'h02, 8'h02, 8'd0);
end_set;
check(n_total == b_e + 1,
"wTotalLength disagreed with the sum of the bLengths and it was not reported -- the host walks by bLength and reads by wTotalLength, and when they differ it lands in the middle of a record and reports an error against a field that is perfectly correct");
// declared SHORTER than the set actually is: the walk runs past it
b_e = n_overrun;
start_set;
emit(8'd9, 9, T_CONFIG, 8'd31, 8'd0, 8'd1); // says 31, is 32
emit(8'd9, 9, T_INTERFACE, 8'd0, 8'd0, 8'd2);
emit(8'd7, 7, T_ENDPOINT, 8'h81, 8'h02, 8'd0);
emit(8'd7, 7, T_ENDPOINT, 8'h02, 8'h02, 8'd0);
end_set;
check(n_overrun == b_e + 1,
"the walk ran past wTotalLength and it was not reported");
end
// ---- Phase F: bNumInterfaces disagrees with what is there. ----
for (k = 0; k < 20; k = k + 1) begin
b_e = n_ifcount;
start_set;
emit(8'd9, 9, T_CONFIG, 8'd32, 8'd0, 8'd2); // claims 2 interfaces
emit(8'd9, 9, T_INTERFACE, 8'd0, 8'd0, 8'd2); // ...there is 1
emit(8'd7, 7, T_ENDPOINT, 8'h81, 8'h02, 8'd0);
emit(8'd7, 7, T_ENDPOINT, 8'h02, 8'h02, 8'd0);
end_set;
check(n_ifcount == b_e + 1,
"bNumInterfaces disagreed with the number of interface records present and it was not reported");
end
// ---- Phase G: bNumEndpoints disagrees, on the last interface AND on
// ---- one that is followed by another interface.
for (k = 0; k < 20; k = k + 1) begin
b_e = n_epcount;
start_set;
emit(8'd9, 9, T_CONFIG, 8'd32, 8'd0, 8'd1);
emit(8'd9, 9, T_INTERFACE, 8'd0, 8'd0, 8'd3); // claims 3
emit(8'd7, 7, T_ENDPOINT, 8'h81, 8'h02, 8'd0);
emit(8'd7, 7, T_ENDPOINT, 8'h02, 8'h02, 8'd0); // ...there are 2
end_set;
check(n_epcount == b_e + 1,
"the LAST interface's endpoint count was not checked -- every other interface is checked when the next one starts, and the last one has no next");
b_e = n_epcount;
start_set;
emit(8'd9, 9, T_CONFIG, 8'd41, 8'd0, 8'd2);
emit(8'd9, 9, T_INTERFACE, 8'd0, 8'd0, 8'd2); // claims 2, has 1
emit(8'd7, 7, T_ENDPOINT, 8'h81, 8'h03, 8'd0);
emit(8'd9, 9, T_INTERFACE, 8'd1, 8'd0, 8'd1);
emit(8'd7, 7, T_ENDPOINT, 8'h02, 8'h02, 8'd0);
end_set;
check(n_epcount == b_e + 1,
"an interface's endpoint count was not checked when the next interface began");
end
// ---- Phase H: an ENDPOINT with no INTERFACE above it. ----
for (k = 0; k < 20; k = k + 1) begin
b_e = n_orphan;
start_set;
emit(8'd9, 9, T_CONFIG, 8'd16, 8'd0, 8'd1);
emit(8'd7, 7, T_ENDPOINT, 8'h81, 8'h02, 8'd0);
end_set;
check(n_orphan == b_e + 1,
"an endpoint appeared before any interface and it was not reported -- it has no owner, and the host will attribute it to whatever interface comes next");
end
// ---- Phase I: ALL 256 bLength VALUES on the first record. ----
//
// The validator must reject exactly three populations: zero (a hang),
// everything below the type's minimum (a wrong value), and nothing
// else. Anything in between is accepted, walked, and produces whatever
// the rest of the set implies.
for (L = 0; L < 256; L = L + 1) begin
b_e = n_zerolen; b_ok = n_short;
start_set;
// At least two bytes whenever bLength is non-zero, so the TYPE is
// always known: SHORT is a statement about a length relative to a
// type, and a record truncated before its type byte is an overrun
// instead -- a different finding about a different defect.
emit(L[7:0], (L == 0) ? 1 : ((L > 9) ? 9 : ((L < 2) ? 2 : L)),
T_CONFIG, 8'd32, 8'd0, 8'd1);
end_set;
if (seen_len[L] == 0) begin seen_len[L] = 1; n_len = n_len + 1; end
if (L == 0) begin
check(n_zerolen == b_e + 1,
"bLength zero was not reported as a zero length");
check(n_short == b_ok,
"bLength zero was classified as SHORT -- zero is a hang and short is a wrong value, and a reader needs to know which symptom to expect");
end else if (L < MIN_CFG) begin
check(n_short == b_ok + 1,
"a CONFIGURATION below its minimum length was accepted");
check(n_zerolen == b_e, "a short length was classified as zero");
end else begin
check(n_zerolen == b_e && n_short == b_ok,
"a bLength at or above the type's minimum was rejected");
end
end
// ---- Phase J: EXHAUSTIVE. Every walker state x every input. ----
for (st2 = 0; st2 < 5; st2 = st2 + 1) begin
for (cb = 0; cb < 8; cb = cb + 1) begin
step(1'b0,1'b0,8'd0,1'b0,1'b1); // back to idle
case (st2)
0: ;
1: start_set;
2: begin start_set; put(8'd9); end
3: begin start_set; put(8'd9); put(T_CONFIG); end
4: begin start_set; put(8'd0); end // the walk is dead
endcase
check(state === st2[2:0],
"the sweep could not reach the walker state it meant to reach");
step(cb[2], cb[1], 8'h20, cb[0], 1'b0);
step(cb[2], cb[1], 8'h20, cb[0], 1'b0);
end
end
// ---- Phase K: random bytes, with set boundaries thrown in ----
for (k = 0; k < 30000; k = k + 1)
step(($unsigned($random) % 1000) < 22,
($unsigned($random) % 100) < 78,
$random,
($unsigned($random) % 1000) < 25,
1'b0);
// ---- Phase L: and legal sets afterwards, so the validator is shown to
// ---- still work rather than merely to have stopped.
step(1'b0,1'b0,8'd0,1'b0,1'b1);
b_ok = n_sets_ok;
for (k = 0; k < 60; k = k + 1) legal_set;
check(n_sets_ok == b_ok + 60,
"the validator stopped accepting consistent descriptor sets");
// ---- Final agreement ----
check(n_desc === m_d[31:0], "n_desc disagrees with the model");
check(n_zerolen === m_zl[31:0], "n_zerolen disagrees");
check(n_short === m_sh[31:0], "n_short disagrees");
check(n_overrun === m_ov[31:0], "n_overrun disagrees");
check(n_total === m_to[31:0], "n_total disagrees");
check(n_ifcount === m_ic[31:0], "n_ifcount disagrees");
check(n_epcount === m_ep[31:0], "n_epcount disagrees");
check(n_orphan === m_or[31:0], "n_orphan disagrees");
check(n_sets_ok === m_sok[31:0], "n_sets_ok disagrees");
check(n_len == 256, "not every bLength value was driven on the first record");
check(n_si == 40, "not every walker state was crossed with every input combination");
check(n_zerolen > 32'd0, "a zero bLength was never seen");
check(n_short > 32'd0, "a short descriptor was never seen");
check(n_overrun > 32'd0, "the walk never ran past wTotalLength");
check(n_total > 32'd0, "wTotalLength never disagreed with the sum");
check(n_ifcount > 32'd0, "bNumInterfaces never disagreed");
check(n_epcount > 32'd0, "bNumEndpoints never disagreed");
check(n_orphan > 32'd0, "an orphan endpoint was never seen");
check(n_sets_ok > 32'd100, "too few consistent sets to have tested for false positives");
$display("REACH blength=%0d/256 state-x-input=%0d/40 steps=%0d",
n_len, n_si, n_steps);
$display("COUNTERS desc=%0d ok=%0d zerolen=%0d short=%0d overrun=%0d total=%0d ifcount=%0d epcount=%0d orphan=%0d",
n_desc, n_sets_ok, n_zerolen, n_short, n_overrun, n_total,
n_ifcount, n_epcount, n_orphan);
$display("%0s: %0d errors in %0d checks", (errors==0)?"PASS":"FAIL", errors, checks);
$finish;
end
endmodule10.2 SystemVerilog testbench
// Testbench for usb_descriptor_validator (SystemVerilog).
//
// WHAT IS EXHAUSTIVE HERE
//
// 1. ALL 256 VALUES of bLength on the first record of a set, with the
// type fixed at CONFIGURATION. The validator must reject exactly
// three populations -- zero, everything below the type's minimum, and
// nothing else -- and it must reject zero with a DIFFERENT code,
// because zero is a hang and short is a wrong value.
//
// 2. Every walker state crossed with all eight combinations of
// {byte_valid, sod, eod} = 40 pairs, each state reached by real bytes.
//
// AND THE SET THE CHAPTER IS ABOUT
//
// A descriptor set whose wTotalLength disagrees with the sum of its
// bLengths by ONE byte. The host reads the right number of bytes and lands
// one byte into the next record, so every field it reads after that point
// comes from the wrong offset -- and the error it eventually reports is
// against a field that is perfectly correct.
`timescale 1ns/1ps
module tb_dv_sv;
import usb_desc_pkg::*;
localparam int MIN_CFG = 9;
localparam int MIN_IF = 9;
localparam int MIN_EP = 7;
localparam int MIN_ANY = 2;
localparam [7:0] T_DEVICE=8'd1, T_CONFIG=8'd2, T_STRING=8'd3,
T_INTERFACE=8'd4, T_ENDPOINT=8'd5;
logic clk = 1'b0, rst_n = 1'b0;
logic sod = 1'b0, byte_valid = 1'b0, eod = 1'b0, eot = 1'b0;
logic [7:0] byte_data = 8'd0;
walk_state_e state;
desc_err_e err_code;
logic [7:0] cur_type, cur_len, if_seen, if_expected, ep_seen, ep_expected;
logic [15:0] consumed, total_len;
logic err_pulse, set_ok;
logic [31:0] n_desc, n_zerolen, n_short, n_overrun, n_total,
n_ifcount, n_epcount, n_orphan, n_sets_ok;
usb_descriptor_validator #(.MIN_CFG(MIN_CFG), .MIN_IF(MIN_IF),
.MIN_EP(MIN_EP), .MIN_ANY(MIN_ANY)) dut (.*);
always #5 clk = ~clk;
int errors = 0, checks = 0;
task automatic check(input logic cond, input string msg);
begin
checks++;
if (!cond) begin
errors++;
if (errors <= 25)
$display("FAIL @%0t: %0s | st=%0d typ=%0d len=%0d cons=%0d tot=%0d if=%0d/%0d ep=%0d/%0d e=%b(%0d)",
$time, msg, state, cur_type, cur_len, consumed, total_len,
if_seen, if_expected, ep_seen, ep_expected, err_pulse, err_code);
end
end
endtask
// ------------------------------------------------------------------
// The shadow walker. Written from the descriptor format, not the design.
// ------------------------------------------------------------------
walk_state_e m_st;
desc_err_e m_ec;
logic [7:0] m_len, m_typ, m_off, m_ifs, m_ife, m_eps, m_epe;
logic [15:0] m_cons, m_tot;
logic m_hif, m_hcfg, m_er, m_ok;
int m_d, m_zl, m_sh, m_ov, m_to, m_ic, m_ep, m_or, m_sok;
int seen_len [256]; // all 256 bLength values on record 0
int seen_si [40]; // 5 states x 8 input combinations
int n_len, n_si, n_steps;
task automatic model_reset();
int j;
begin
m_st = W_IDLE; m_ec = E_NONE;
m_len = 8'd0; m_typ = 8'd0; m_off = 8'd0;
m_ifs = 8'd0; m_ife = 8'd0; m_eps = 8'd0; m_epe = 8'd0;
m_cons = 16'd0; m_tot = 16'd0;
m_hif = 1'b0; m_hcfg = 1'b0; m_er = 1'b0; m_ok = 1'b0;
m_d = 0; m_zl = 0; m_sh = 0; m_ov = 0; m_to = 0;
m_ic = 0; m_ep = 0; m_or = 0; m_sok = 0;
for (j = 0; j < 256; j++) seen_len[j] = 0;
for (j = 0; j < 40; j = j + 1) seen_si[j] = 0;
n_len = 0; n_si = 0; n_steps = 0;
end
endtask
function automatic logic [7:0] min_len(input logic [7:0] t);
begin
if (t == T_CONFIG) min_len = 8'(MIN_CFG);
else if (t == T_INTERFACE) min_len = 8'(MIN_IF);
else if (t == T_ENDPOINT) min_len = 8'(MIN_EP);
else min_len = 8'(MIN_ANY);
end
endfunction
int idx;
task automatic step(input logic sd, input logic bv, input logic [7:0] bd,
input logic ed, input logic eo);
walk_state_e ns;
desc_err_e nec;
logic [7:0] nlen, ntyp, noff, nifs, nife, neps, nepe;
logic [15:0] ncons, ntot;
logic nhif, nhcfg, ner, nok;
begin
sod = sd; byte_valid = bv; byte_data = bd; eod = ed; eot = eo;
#1;
check(state === m_st, "state disagrees with the shadow walker");
check(cur_type === m_typ, "cur_type disagrees");
check(cur_len === m_len, "cur_len disagrees");
check(consumed === m_cons, "consumed disagrees -- the walk is not the length the model says");
check(total_len === m_tot, "total_len disagrees");
check(if_seen === m_ifs, "if_seen disagrees");
check(if_expected === m_ife, "if_expected disagrees");
check(ep_seen === m_eps, "ep_seen disagrees");
check(ep_expected === m_epe, "ep_expected disagrees");
check(err_code === m_ec, "err_code disagrees");
check(err_pulse === m_er, "the error pulse disagrees");
check(set_ok === m_ok, "the set_ok pulse disagrees");
check(!(err_pulse && set_ok),
"a descriptor set was reported consistent and broken in the same cycle");
idx = int'(m_st) * 8 + (sd ? 4 : 0) + (bv ? 2 : 0) + (ed ? 1 : 0);
if (idx < 40) begin
if (seen_si[idx] == 0) begin seen_si[idx] = 1; n_si = n_si + 1; end
end
n_steps++;
// ---- advance the shadow walker ----
ns = m_st; nlen = m_len; ntyp = m_typ; noff = m_off;
ncons = m_cons; ntot = m_tot;
nifs = m_ifs; nife = m_ife; neps = m_eps; nepe = m_epe;
nhif = m_hif; nhcfg = m_hcfg;
nec = E_NONE; ner = 1'b0; nok = 1'b0;
if (eo) begin
ns = W_IDLE;
end else if (sd) begin
ns = W_LEN; nlen = 8'd0; ntyp = 8'd0; noff = 8'd0;
ncons = 16'd0; ntot = 16'd0;
nifs = 8'd0; nife = 8'd0; neps = 8'd0; nepe = 8'd0;
nhif = 1'b0; nhcfg = 1'b0;
end else if (ed) begin
if ((m_st != W_IDLE) && (m_st != W_DEAD)) begin
if (nhif && (m_eps != m_epe)) begin
ner = 1'b1; nec = E_EPCOUNT;
end else if (nhcfg && (m_cons != m_tot)) begin
ner = 1'b1; nec = E_TOTAL;
end else if (nhcfg && (m_ifs != m_ife)) begin
ner = 1'b1; nec = E_IFCOUNT;
end else if (m_st == W_LEN) begin
nok = 1'b1;
end else begin
ner = 1'b1; nec = E_OVERRUN;
end
end
ns = W_IDLE;
end else if (bv) begin
if (m_st == W_LEN) begin
nlen = bd; noff = 8'd1; ncons = m_cons + 16'd1;
if (bd == 8'd0) begin
ner = 1'b1; nec = E_ZEROLEN; ns = W_DEAD;
end else ns = W_TYPE;
end else if (m_st == W_TYPE) begin
ntyp = bd; noff = 8'd2; ncons = m_cons + 16'd1;
if (m_len < min_len(bd)) begin
ner = 1'b1; nec = E_SHORT; ns = W_DEAD;
end else if (bd == T_ENDPOINT) begin
if (!m_hif) begin
ner = 1'b1; nec = E_ORPHAN; ns = W_DEAD;
end else begin
neps = m_eps + 8'd1;
if (m_len == 8'd2) ns = W_LEN; else ns = W_BODY;
end
end else if (bd == T_INTERFACE) begin
if (m_hif && (m_eps != m_epe)) begin
ner = 1'b1; nec = E_EPCOUNT; ns = W_DEAD;
end else begin
nhif = 1'b1; nifs = m_ifs + 8'd1; neps = 8'd0;
if (m_len == 8'd2) ns = W_LEN; else ns = W_BODY;
end
end else begin
if (bd == T_CONFIG) nhcfg = 1'b1;
if (m_len == 8'd2) ns = W_LEN; else ns = W_BODY;
end
end else if (m_st == W_BODY) begin
ncons = m_cons + 16'd1; noff = m_off + 8'd1;
if (m_typ == T_CONFIG) begin
if (m_off == 8'd2) ntot = {m_tot[15:8], bd};
if (m_off == 8'd3) ntot = {bd, m_tot[7:0]};
if (m_off == 8'd4) nife = bd;
end else if (m_typ == T_INTERFACE) begin
if (m_off == 8'd4) nepe = bd;
end
if (m_off + 8'd1 >= m_len) ns = W_LEN;
if (nhcfg && (m_tot != 16'd0) && (ncons > m_tot)) begin
ner = 1'b1; nec = E_OVERRUN; ns = W_DEAD;
end
end else begin
ncons = m_cons + 16'd1;
end
end
if (bv && (m_st == W_TYPE) && !sd && !ed && !eo) m_d = m_d + 1;
m_st = ns; m_len = nlen; m_typ = ntyp; m_off = noff;
m_cons = ncons; m_tot = ntot;
m_ifs = nifs; m_ife = nife; m_eps = neps; m_epe = nepe;
m_hif = nhif; m_hcfg = nhcfg;
m_ec = nec; m_er = ner; m_ok = nok;
if (nok) m_sok = m_sok + 1;
if (ner) begin
case (nec)
E_ZEROLEN: m_zl = m_zl + 1;
E_SHORT: m_sh = m_sh + 1;
E_OVERRUN: m_ov = m_ov + 1;
E_TOTAL: m_to = m_to + 1;
E_IFCOUNT: m_ic = m_ic + 1;
E_EPCOUNT: m_ep = m_ep + 1;
E_ORPHAN: m_or = m_or + 1;
default: ;
endcase
end
@(posedge clk); #1;
sod = 1'b0; byte_valid = 1'b0; eod = 1'b0; eot = 1'b0;
end
endtask
task automatic put(input logic [7:0] b);
step(1'b0, 1'b1, b, 1'b0, 1'b0);
endtask
task automatic start_set();
step(1'b1, 1'b0, 8'd0, 1'b0, 1'b0);
endtask
task automatic end_set();
step(1'b0, 1'b0, 8'd0, 1'b1, 1'b0);
endtask
task automatic nop(input int n);
repeat (n) step(1'b0, 1'b0, 8'd0, 1'b0, 1'b0);
endtask
// Emit a record whose bLength FIELD and actual byte COUNT can differ --
// which is the whole point, because they differ in the bug this chapter
// is about.
int ei;
task automatic emit(input logic [7:0] blen_field, input int nbytes,
input logic [7:0] typ, input logic [7:0] b2,
input logic [7:0] b3, input logic [7:0] b4);
begin
for (ei = 0; ei < nbytes; ei++) begin
case (ei)
0: put(blen_field);
1: put(typ);
2: put(b2);
3: put(b3);
4: put(b4);
default: put(8'h00);
endcase
end
end
endtask
// The canonical set: CONFIG(9) + INTERFACE(9) + ENDPOINT(7) x2 = 32
task automatic legal_set();
begin
start_set();
emit(8'd9, 9, T_CONFIG, 8'd32, 8'd0, 8'd1);
emit(8'd9, 9, T_INTERFACE, 8'd0, 8'd0, 8'd2);
emit(8'd7, 7, T_ENDPOINT, 8'h81, 8'h02, 8'd0);
emit(8'd7, 7, T_ENDPOINT, 8'h02, 8'h02, 8'd0);
end_set();
end
endtask
int k, b_e, b_ok, L, st2, cb, total_now;
initial begin
model_reset();
repeat (3) @(posedge clk);
rst_n = 1'b1;
@(posedge clk); #1;
// ---- Phase A: the state after reset ----
check(state === W_IDLE, "reset did not land idle");
check(err_pulse === 1'b0, "reset reported a descriptor error");
// ---- Phase B: legal sets of several shapes. ZERO errors. ----
for (k = 0; k < 60; k++) begin
b_ok = n_sets_ok;
b_e = n_zerolen + n_short + n_overrun + n_total + n_ifcount
+ n_epcount + n_orphan;
legal_set();
check(n_sets_ok == b_ok + 1,
"a perfectly consistent descriptor set was not accepted");
check(n_zerolen + n_short + n_overrun + n_total + n_ifcount
+ n_epcount + n_orphan == b_e,
"a perfectly consistent descriptor set produced an error -- a validator with false positives is a validator somebody switches off, and then nobody is checking descriptors at all");
// an interface with NO endpoints is perfectly legal
b_ok = n_sets_ok;
start_set();
emit(8'd9, 9, T_CONFIG, 8'd18, 8'd0, 8'd1);
emit(8'd9, 9, T_INTERFACE, 8'd0, 8'd0, 8'd0);
end_set();
check(n_sets_ok == b_ok + 1,
"an interface with no endpoints was rejected, and that is a legal descriptor");
// TWO interfaces, one endpoint each
b_ok = n_sets_ok;
start_set();
emit(8'd9, 9, T_CONFIG, 8'd41, 8'd0, 8'd2);
emit(8'd9, 9, T_INTERFACE, 8'd0, 8'd0, 8'd1);
emit(8'd7, 7, T_ENDPOINT, 8'h81, 8'h03, 8'd0);
emit(8'd9, 9, T_INTERFACE, 8'd1, 8'd0, 8'd1);
emit(8'd7, 7, T_ENDPOINT, 8'h02, 8'h02, 8'd0);
end_set();
check(n_sets_ok == b_ok + 1, "a two-interface set was rejected");
end
// ---- Phase C: bLength = 0 is a HANG, and has its own code. ----
for (k = 0; k < 20; k++) begin
b_e = n_zerolen;
start_set();
emit(8'd9, 9, T_CONFIG, 8'd25, 8'd0, 8'd1);
put(8'd0); // a record claiming zero length
end_set();
check(n_zerolen == b_e + 1,
"a zero bLength was not reported as its own class -- the walk advances by bLength, so zero advances it by nothing and a host that does not bound its walk HANGS rather than failing");
check(state === W_IDLE, "the walker did not stop after a zero length");
end
// ---- Phase D: shorter than the type requires, for each type. ----
for (L = 2; L < MIN_CFG; L++) begin
b_e = n_short;
start_set();
emit(8'(L), L, T_CONFIG, 8'd20, 8'd0, 8'd1);
end_set();
check(n_short == b_e + 1,
"a CONFIGURATION shorter than its minimum was accepted -- the record cannot hold the fields its type is defined to have, so every field the host reads from it comes from the NEXT record");
end
for (L = 2; L < MIN_EP; L++) begin
b_e = n_short;
start_set();
// wTotalLength is set to what the set ACTUALLY is (9 + 9 + L), so the
// overrun check cannot fire first and mask the one under test. An
// injected defect that is pre-empted by a different injected defect
// tests neither.
emit(8'd9, 9, T_CONFIG, (8'd18 + 8'(L)), 8'd0, 8'd1);
emit(8'd9, 9, T_INTERFACE, 8'd0, 8'd0, 8'd1);
emit(8'(L), L, T_ENDPOINT, 8'h81, 8'h02, 8'd0);
end_set();
check(n_short == b_e + 1, "an ENDPOINT shorter than its minimum was accepted");
end
// ---- Phase E: THE CHAPTER. wTotalLength disagrees with the sum. ----
//
// Off by ONE byte. The host reads the right number of bytes and lands
// one byte into the next record, so every field after that point comes
// from the wrong offset.
for (k = 0; k < 20; k++) begin
// declared LONGER than the set actually is
b_e = n_total;
start_set();
emit(8'd9, 9, T_CONFIG, 8'd33, 8'd0, 8'd1); // says 33, is 32
emit(8'd9, 9, T_INTERFACE, 8'd0, 8'd0, 8'd2);
emit(8'd7, 7, T_ENDPOINT, 8'h81, 8'h02, 8'd0);
emit(8'd7, 7, T_ENDPOINT, 8'h02, 8'h02, 8'd0);
end_set();
check(n_total == b_e + 1,
"wTotalLength disagreed with the sum of the bLengths and it was not reported -- the host walks by bLength and reads by wTotalLength, and when they differ it lands in the middle of a record and reports an error against a field that is perfectly correct");
// declared SHORTER than the set actually is: the walk runs past it
b_e = n_overrun;
start_set();
emit(8'd9, 9, T_CONFIG, 8'd31, 8'd0, 8'd1); // says 31, is 32
emit(8'd9, 9, T_INTERFACE, 8'd0, 8'd0, 8'd2);
emit(8'd7, 7, T_ENDPOINT, 8'h81, 8'h02, 8'd0);
emit(8'd7, 7, T_ENDPOINT, 8'h02, 8'h02, 8'd0);
end_set();
check(n_overrun == b_e + 1,
"the walk ran past wTotalLength and it was not reported");
end
// ---- Phase F: bNumInterfaces disagrees with what is there. ----
for (k = 0; k < 20; k++) begin
b_e = n_ifcount;
start_set();
emit(8'd9, 9, T_CONFIG, 8'd32, 8'd0, 8'd2); // claims 2 interfaces
emit(8'd9, 9, T_INTERFACE, 8'd0, 8'd0, 8'd2); // ...there is 1
emit(8'd7, 7, T_ENDPOINT, 8'h81, 8'h02, 8'd0);
emit(8'd7, 7, T_ENDPOINT, 8'h02, 8'h02, 8'd0);
end_set();
check(n_ifcount == b_e + 1,
"bNumInterfaces disagreed with the number of interface records present and it was not reported");
end
// ---- Phase G: bNumEndpoints disagrees, on the last interface AND on
// ---- one that is followed by another interface.
for (k = 0; k < 20; k++) begin
b_e = n_epcount;
start_set();
emit(8'd9, 9, T_CONFIG, 8'd32, 8'd0, 8'd1);
emit(8'd9, 9, T_INTERFACE, 8'd0, 8'd0, 8'd3); // claims 3
emit(8'd7, 7, T_ENDPOINT, 8'h81, 8'h02, 8'd0);
emit(8'd7, 7, T_ENDPOINT, 8'h02, 8'h02, 8'd0); // ...there are 2
end_set();
check(n_epcount == b_e + 1,
"the LAST interface's endpoint count was not checked -- every other interface is checked when the next one starts, and the last one has no next");
b_e = n_epcount;
start_set();
emit(8'd9, 9, T_CONFIG, 8'd41, 8'd0, 8'd2);
emit(8'd9, 9, T_INTERFACE, 8'd0, 8'd0, 8'd2); // claims 2, has 1
emit(8'd7, 7, T_ENDPOINT, 8'h81, 8'h03, 8'd0);
emit(8'd9, 9, T_INTERFACE, 8'd1, 8'd0, 8'd1);
emit(8'd7, 7, T_ENDPOINT, 8'h02, 8'h02, 8'd0);
end_set();
check(n_epcount == b_e + 1,
"an interface's endpoint count was not checked when the next interface began");
end
// ---- Phase H: an ENDPOINT with no INTERFACE above it. ----
for (k = 0; k < 20; k++) begin
b_e = n_orphan;
start_set();
emit(8'd9, 9, T_CONFIG, 8'd16, 8'd0, 8'd1);
emit(8'd7, 7, T_ENDPOINT, 8'h81, 8'h02, 8'd0);
end_set();
check(n_orphan == b_e + 1,
"an endpoint appeared before any interface and it was not reported -- it has no owner, and the host will attribute it to whatever interface comes next");
end
// ---- Phase I: ALL 256 bLength VALUES on the first record. ----
//
// The validator must reject exactly three populations: zero (a hang),
// everything below the type's minimum (a wrong value), and nothing
// else. Anything in between is accepted, walked, and produces whatever
// the rest of the set implies.
for (L = 0; L < 256; L++) begin
b_e = n_zerolen; b_ok = n_short;
start_set();
// At least two bytes whenever bLength is non-zero, so the TYPE is
// always known: SHORT is a statement about a length relative to a
// type, and a record truncated before its type byte is an overrun
// instead -- a different finding about a different defect.
emit(8'(L), (L == 0) ? 1 : ((L > 9) ? 9 : ((L < 2) ? 2 : L)),
T_CONFIG, 8'd32, 8'd0, 8'd1);
end_set();
if (seen_len[L] == 0) begin seen_len[L] = 1; n_len = n_len + 1; end
if (L == 0) begin
check(n_zerolen == b_e + 1,
"bLength zero was not reported as a zero length");
check(n_short == b_ok,
"bLength zero was classified as SHORT -- zero is a hang and short is a wrong value, and a reader needs to know which symptom to expect");
end else if (L < MIN_CFG) begin
check(n_short == b_ok + 1,
"a CONFIGURATION below its minimum length was accepted");
check(n_zerolen == b_e, "a short length was classified as zero");
end else begin
check(n_zerolen == b_e && n_short == b_ok,
"a bLength at or above the type's minimum was rejected");
end
end
// ---- Phase J: EXHAUSTIVE. Every walker state x every input. ----
for (st2 = 0; st2 < 5; st2++) begin
for (cb = 0; cb < 8; cb++) begin
step(1'b0,1'b0,8'd0,1'b0,1'b1); // back to idle
case (st2)
0: ;
1: start_set();
2: begin start_set(); put(8'd9); end
3: begin start_set(); put(8'd9); put(T_CONFIG); end
4: begin start_set(); put(8'd0); end // the walk is dead
endcase
check(state === walk_state_e'(st2),
"the sweep could not reach the walker state it meant to reach");
step(1'(cb[2]), 1'(cb[1]), 8'h20, 1'(cb[0]), 1'b0);
step(1'(cb[2]), 1'(cb[1]), 8'h20, 1'(cb[0]), 1'b0);
end
end
// ---- Phase K: random bytes, with set boundaries thrown in ----
for (k = 0; k < 30000; k++)
step($urandom_range(0,999) < 22,
$urandom_range(0,99) < 78,
8'($urandom()),
$urandom_range(0,999) < 25,
1'b0);
// ---- Phase L: and legal sets afterwards, so the validator is shown to
// ---- still work rather than merely to have stopped.
step(1'b0,1'b0,8'd0,1'b0,1'b1);
b_ok = n_sets_ok;
for (k = 0; k < 60; k++) legal_set();
check(n_sets_ok == b_ok + 60,
"the validator stopped accepting consistent descriptor sets");
// ---- Final agreement ----
check(n_desc === 32'(m_d), "n_desc disagrees with the model");
check(n_zerolen === 32'(m_zl), "n_zerolen disagrees");
check(n_short === 32'(m_sh), "n_short disagrees");
check(n_overrun === 32'(m_ov), "n_overrun disagrees");
check(n_total === 32'(m_to), "n_total disagrees");
check(n_ifcount === 32'(m_ic), "n_ifcount disagrees");
check(n_epcount === 32'(m_ep), "n_epcount disagrees");
check(n_orphan === 32'(m_or), "n_orphan disagrees");
check(n_sets_ok === 32'(m_sok), "n_sets_ok disagrees");
check(n_len == 256, "not every bLength value was driven on the first record");
check(n_si == 40, "not every walker state was crossed with every input combination");
check(n_zerolen > 32'd0, "a zero bLength was never seen");
check(n_short > 32'd0, "a short descriptor was never seen");
check(n_overrun > 32'd0, "the walk never ran past wTotalLength");
check(n_total > 32'd0, "wTotalLength never disagreed with the sum");
check(n_ifcount > 32'd0, "bNumInterfaces never disagreed");
check(n_epcount > 32'd0, "bNumEndpoints never disagreed");
check(n_orphan > 32'd0, "an orphan endpoint was never seen");
check(n_sets_ok > 32'd100, "too few consistent sets to have tested for false positives");
$display("REACH blength=%0d/256 state-x-input=%0d/40 steps=%0d",
n_len, n_si, n_steps);
$display("COUNTERS desc=%0d ok=%0d zerolen=%0d short=%0d overrun=%0d total=%0d ifcount=%0d epcount=%0d orphan=%0d",
n_desc, n_sets_ok, n_zerolen, n_short, n_overrun, n_total,
n_ifcount, n_epcount, n_orphan);
$display("%0s: %0d errors in %0d checks", (errors==0)?"PASS":"FAIL", errors, checks);
$finish;
end
endmodule10.3 VHDL testbench
-- Testbench for usb_descriptor_validator (VHDL-2008).
--
-- WHAT IS EXHAUSTIVE HERE
--
-- 1. ALL 256 VALUES of bLength on the first record of a set, with the
-- type fixed at CONFIGURATION. The validator must reject exactly
-- three populations -- zero, everything below the type's minimum, and
-- nothing else -- and it must reject zero with a DIFFERENT code,
-- because zero is a hang and short is a wrong value.
--
-- 2. Every walker state crossed with all eight combinations of
-- (byte_valid, sod, eod) = 40 pairs, each state reached by real bytes.
--
-- AND THE SET THE CHAPTER IS ABOUT
--
-- A descriptor set whose wTotalLength disagrees with the sum of its
-- bLengths by ONE byte. The host reads the right number of bytes and lands
-- one byte into the next record, so every field it reads after that point
-- comes from the wrong offset -- and the error it eventually reports is
-- against a field that is perfectly correct.
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
use std.textio.all;
use work.usb_desc_pkg.all;
entity tb_dv_vhdl is
end entity tb_dv_vhdl;
architecture sim of tb_dv_vhdl is
constant MIN_CFG : integer := 9;
constant MIN_IF : integer := 9;
constant MIN_EP : integer := 7;
constant MIN_ANY : integer := 2;
constant T_CONFIG : std_logic_vector(7 downto 0) := x"02";
constant T_INTERFACE : std_logic_vector(7 downto 0) := x"04";
constant T_ENDPOINT : std_logic_vector(7 downto 0) := x"05";
signal clk : std_logic := '0';
signal rst_n : std_logic := '0';
signal done : boolean := false;
signal sod, byte_valid, eod, eot : std_logic := '0';
signal byte_data : std_logic_vector(7 downto 0) := (others => '0');
signal state, err_code : std_logic_vector(2 downto 0);
signal cur_type, cur_len : std_logic_vector(7 downto 0);
signal if_seen, if_expected, ep_seen, ep_expected : std_logic_vector(7 downto 0);
signal consumed, total_len : std_logic_vector(15 downto 0);
signal err_pulse, set_ok : std_logic;
signal n_desc, n_zerolen, n_short, n_overrun : std_logic_vector(31 downto 0);
signal n_total, n_ifcount, n_epcount, n_orphan, n_sets_ok : std_logic_vector(31 downto 0);
begin
dut : entity work.usb_descriptor_validator
generic map (MIN_CFG => MIN_CFG, MIN_IF => MIN_IF,
MIN_EP => MIN_EP, MIN_ANY => MIN_ANY)
port map (
clk => clk, rst_n => rst_n,
sod => sod, byte_valid => byte_valid, byte_data => byte_data,
eod => eod, eot => eot,
state => state, cur_type => cur_type, cur_len => cur_len,
consumed => consumed, total_len => total_len,
if_seen => if_seen, if_expected => if_expected,
ep_seen => ep_seen, ep_expected => ep_expected,
err_pulse => err_pulse, err_code => err_code, set_ok => set_ok,
n_desc => n_desc, n_zerolen => n_zerolen, n_short => n_short,
n_overrun => n_overrun, n_total => n_total, n_ifcount => n_ifcount,
n_epcount => n_epcount, n_orphan => n_orphan, n_sets_ok => n_sets_ok
);
clk <= (not clk) after 5 ns when not done else '0';
stim : process
type len_arr is array (0 to 255) of integer;
type si_arr is array (0 to 39) of integer;
variable errors, checks : integer := 0;
-- ---- The shadow walker. Written from the descriptor format. ----
variable m_st : walk_state_t := W_IDLE;
variable m_ec : desc_err_t := E_NONE;
variable m_len, m_typ, m_off : unsigned(7 downto 0) := (others => '0');
variable m_ifs, m_ife, m_eps, m_epe : unsigned(7 downto 0) := (others => '0');
variable m_cons, m_tot : unsigned(15 downto 0) := (others => '0');
variable m_hif, m_hcfg, m_er, m_ok : std_logic := '0';
variable m_d, m_zl, m_sh, m_ov, m_to : integer := 0;
variable m_ic, m_ep, m_or, m_sok : integer := 0;
variable seen_len : len_arr := (others => 0);
variable seen_si : si_arr := (others => 0);
variable n_len, n_si, n_steps : integer := 0;
-- A deterministic LFSR, so a rerun reproduces exactly the same traffic.
variable lfsr : unsigned(31 downto 0) := x"D35C0FFE";
impure function rnd32 return unsigned is
begin
lfsr := lfsr(30 downto 0) &
(lfsr(31) xor lfsr(21) xor lfsr(1) xor lfsr(0));
return lfsr;
end function;
-- Only the low 30 bits are converted: a full 32-bit unsigned does not
-- fit in VHDL's INTEGER, and to_integer aborts the run rather than
-- wrapping.
impure function rnd_nat return integer is
variable u : unsigned(31 downto 0);
begin
u := rnd32;
return to_integer(u(29 downto 0));
end function;
impure function rnd_byte return std_logic_vector is
variable u : unsigned(31 downto 0);
begin
u := rnd32;
return std_logic_vector(u(7 downto 0));
end function;
impure function rnd_lt (pct, base : integer) return std_logic is
begin
if (rnd_nat mod base) < pct then return '1'; else return '0'; end if;
end function;
function min_len_m (t : std_logic_vector(7 downto 0)) return unsigned is
begin
if t = T_CONFIG then return to_unsigned(MIN_CFG, 8);
elsif t = T_INTERFACE then return to_unsigned(MIN_IF, 8);
elsif t = T_ENDPOINT then return to_unsigned(MIN_EP, 8);
else return to_unsigned(MIN_ANY, 8);
end if;
end function;
procedure chk (cond : boolean; msg : string) is
begin
checks := checks + 1;
if not cond then
errors := errors + 1;
if errors <= 25 then
report "FAIL: " & msg &
" | st=" & integer'image(walk_state_t'pos(m_st)) &
" cons=" & integer'image(to_integer(m_cons)) &
" tot=" & integer'image(to_integer(m_tot)) &
" ec=" & integer'image(desc_err_t'pos(m_ec))
severity note;
end if;
end if;
end procedure;
procedure step (sd, bv : std_logic; bd_v : std_logic_vector(7 downto 0);
ed, eo : std_logic) is
variable nst : walk_state_t;
variable nec : desc_err_t;
variable nlen, ntyp, noff : unsigned(7 downto 0);
variable ncons, ntot : unsigned(15 downto 0);
variable nifs, nife, neps, nepe : unsigned(7 downto 0);
variable nhif, nhcfg, ner, nok : std_logic;
variable bd : unsigned(7 downto 0);
variable idx : integer;
begin
sod <= sd; byte_valid <= bv; byte_data <= bd_v; eod <= ed; eot <= eo;
wait for 1 ns;
chk(state = w_code(m_st), "state disagrees with the shadow walker");
chk(unsigned(cur_type) = m_typ, "cur_type disagrees");
chk(unsigned(cur_len) = m_len, "cur_len disagrees");
chk(unsigned(consumed) = m_cons,
"consumed disagrees -- the walk is not the length the model says");
chk(unsigned(total_len) = m_tot, "total_len disagrees");
chk(unsigned(if_seen) = m_ifs, "if_seen disagrees");
chk(unsigned(if_expected) = m_ife, "if_expected disagrees");
chk(unsigned(ep_seen) = m_eps, "ep_seen disagrees");
chk(unsigned(ep_expected) = m_epe, "ep_expected disagrees");
chk(err_code = e_code(m_ec), "err_code disagrees");
chk(err_pulse = m_er, "the error pulse disagrees");
chk(set_ok = m_ok, "the set_ok pulse disagrees");
chk(not (err_pulse = '1' and set_ok = '1'),
"a descriptor set was reported consistent and broken in the same cycle");
idx := walk_state_t'pos(m_st) * 8;
if sd = '1' then idx := idx + 4; end if;
if bv = '1' then idx := idx + 2; end if;
if ed = '1' then idx := idx + 1; end if;
if idx < 40 then
if seen_si(idx) = 0 then seen_si(idx) := 1; n_si := n_si + 1; end if;
end if;
n_steps := n_steps + 1;
-- ---- advance the shadow walker ----
bd := unsigned(bd_v);
nst := m_st; nlen := m_len; ntyp := m_typ; noff := m_off;
ncons := m_cons; ntot := m_tot;
nifs := m_ifs; nife := m_ife; neps := m_eps; nepe := m_epe;
nhif := m_hif; nhcfg := m_hcfg;
nec := E_NONE; ner := '0'; nok := '0';
if eo = '1' then
nst := W_IDLE;
elsif sd = '1' then
nst := W_LEN;
nlen := (others => '0'); ntyp := (others => '0');
noff := (others => '0');
ncons := (others => '0'); ntot := (others => '0');
nifs := (others => '0'); nife := (others => '0');
neps := (others => '0'); nepe := (others => '0');
nhif := '0'; nhcfg := '0';
elsif ed = '1' then
if m_st /= W_IDLE and m_st /= W_DEAD then
if nhif = '1' and m_eps /= m_epe then
ner := '1'; nec := E_EPCOUNT;
elsif nhcfg = '1' and m_cons /= m_tot then
ner := '1'; nec := E_TOTAL;
elsif nhcfg = '1' and m_ifs /= m_ife then
ner := '1'; nec := E_IFCOUNT;
elsif m_st = W_LEN then
nok := '1';
else
ner := '1'; nec := E_OVERRUN;
end if;
end if;
nst := W_IDLE;
elsif bv = '1' then
case m_st is
when W_LEN =>
nlen := bd; noff := to_unsigned(1, 8); ncons := m_cons + 1;
if bd = 0 then
ner := '1'; nec := E_ZEROLEN; nst := W_DEAD;
else
nst := W_TYPE;
end if;
when W_TYPE =>
ntyp := bd; noff := to_unsigned(2, 8); ncons := m_cons + 1;
if m_len < min_len_m(bd_v) then
ner := '1'; nec := E_SHORT; nst := W_DEAD;
elsif bd_v = T_ENDPOINT then
if m_hif = '0' then
ner := '1'; nec := E_ORPHAN; nst := W_DEAD;
else
neps := m_eps + 1;
if m_len = 2 then nst := W_LEN; else nst := W_BODY; end if;
end if;
elsif bd_v = T_INTERFACE then
if m_hif = '1' and m_eps /= m_epe then
ner := '1'; nec := E_EPCOUNT; nst := W_DEAD;
else
nhif := '1'; nifs := m_ifs + 1; neps := (others => '0');
if m_len = 2 then nst := W_LEN; else nst := W_BODY; end if;
end if;
else
if bd_v = T_CONFIG then nhcfg := '1'; end if;
if m_len = 2 then nst := W_LEN; else nst := W_BODY; end if;
end if;
when W_BODY =>
ncons := m_cons + 1; noff := m_off + 1;
if m_typ = unsigned(T_CONFIG) then
if m_off = 2 then ntot := m_tot(15 downto 8) & bd; end if;
if m_off = 3 then ntot := bd & m_tot(7 downto 0); end if;
if m_off = 4 then nife := bd; end if;
elsif m_typ = unsigned(T_INTERFACE) then
if m_off = 4 then nepe := bd; end if;
end if;
if m_off + 1 >= m_len then nst := W_LEN; end if;
if nhcfg = '1' and m_tot /= 0 and ncons > m_tot then
ner := '1'; nec := E_OVERRUN; nst := W_DEAD;
end if;
when others =>
ncons := m_cons + 1;
end case;
end if;
if bv = '1' and m_st = W_TYPE and sd = '0' and ed = '0' and eo = '0' then
m_d := m_d + 1;
end if;
m_st := nst; m_len := nlen; m_typ := ntyp; m_off := noff;
m_cons := ncons; m_tot := ntot;
m_ifs := nifs; m_ife := nife; m_eps := neps; m_epe := nepe;
m_hif := nhif; m_hcfg := nhcfg;
m_ec := nec; m_er := ner; m_ok := nok;
if nok = '1' then m_sok := m_sok + 1; end if;
if ner = '1' then
case nec is
when E_ZEROLEN => m_zl := m_zl + 1;
when E_SHORT => m_sh := m_sh + 1;
when E_OVERRUN => m_ov := m_ov + 1;
when E_TOTAL => m_to := m_to + 1;
when E_IFCOUNT => m_ic := m_ic + 1;
when E_EPCOUNT => m_ep := m_ep + 1;
when E_ORPHAN => m_or := m_or + 1;
when others => null;
end case;
end if;
wait until rising_edge(clk);
wait for 1 ns;
sod <= '0'; byte_valid <= '0'; eod <= '0'; eot <= '0';
end procedure;
constant Z8 : std_logic_vector(7 downto 0) := (others => '0');
procedure put (b : std_logic_vector(7 downto 0)) is
begin step('0', '1', b, '0', '0'); end procedure;
procedure start_set is
begin step('1', '0', Z8, '0', '0'); end procedure;
procedure end_set is
begin step('0', '0', Z8, '1', '0'); end procedure;
function b8 (n : integer) return std_logic_vector is
begin
return std_logic_vector(to_unsigned(n mod 256, 8));
end function;
-- Emit a record whose bLength FIELD and actual byte COUNT can differ --
-- which is the whole point, because they differ in the bug this chapter
-- is about.
procedure emit (blen_field : integer; nbytes : integer;
typ, b2, b3, b4 : std_logic_vector(7 downto 0)) is
begin
for i in 0 to nbytes - 1 loop
case i is
when 0 => put(b8(blen_field));
when 1 => put(typ);
when 2 => put(b2);
when 3 => put(b3);
when 4 => put(b4);
when others => put(Z8);
end case;
end loop;
end procedure;
-- The canonical set: CONFIG(9) + INTERFACE(9) + ENDPOINT(7) x2 = 32
procedure legal_set is
begin
start_set;
emit(9, 9, T_CONFIG, b8(32), Z8, b8(1));
emit(9, 9, T_INTERFACE, Z8, Z8, b8(2));
emit(7, 7, T_ENDPOINT, x"81", x"02", Z8);
emit(7, 7, T_ENDPOINT, x"02", x"02", Z8);
end_set;
end procedure;
variable b_e, b_ok : integer := 0;
variable sd_v, bv_v, ed_v : std_logic;
variable ln : line;
begin
wait for 33 ns;
rst_n <= '1';
wait until rising_edge(clk);
wait for 1 ns;
-- ---- Phase A: the state after reset ----
chk(state = w_code(W_IDLE), "reset did not land idle");
chk(err_pulse = '0', "reset reported a descriptor error");
-- ---- Phase B: legal sets of several shapes. ZERO errors. ----
for k in 0 to 59 loop
b_ok := to_integer(unsigned(n_sets_ok));
b_e := to_integer(unsigned(n_zerolen)) + to_integer(unsigned(n_short))
+ to_integer(unsigned(n_overrun)) + to_integer(unsigned(n_total))
+ to_integer(unsigned(n_ifcount)) + to_integer(unsigned(n_epcount))
+ to_integer(unsigned(n_orphan));
legal_set;
chk(to_integer(unsigned(n_sets_ok)) = b_ok + 1,
"a perfectly consistent descriptor set was not accepted");
chk(to_integer(unsigned(n_zerolen)) + to_integer(unsigned(n_short))
+ to_integer(unsigned(n_overrun)) + to_integer(unsigned(n_total))
+ to_integer(unsigned(n_ifcount)) + to_integer(unsigned(n_epcount))
+ to_integer(unsigned(n_orphan)) = b_e,
"a perfectly consistent descriptor set produced an error -- a validator with false positives is a validator somebody switches off, and then nobody is checking descriptors at all");
b_ok := to_integer(unsigned(n_sets_ok));
start_set;
emit(9, 9, T_CONFIG, b8(18), Z8, b8(1));
emit(9, 9, T_INTERFACE, Z8, Z8, Z8);
end_set;
chk(to_integer(unsigned(n_sets_ok)) = b_ok + 1,
"an interface with no endpoints was rejected, and that is a legal descriptor");
b_ok := to_integer(unsigned(n_sets_ok));
start_set;
emit(9, 9, T_CONFIG, b8(41), Z8, b8(2));
emit(9, 9, T_INTERFACE, Z8, Z8, b8(1));
emit(7, 7, T_ENDPOINT, x"81", x"03", Z8);
emit(9, 9, T_INTERFACE, b8(1), Z8, b8(1));
emit(7, 7, T_ENDPOINT, x"02", x"02", Z8);
end_set;
chk(to_integer(unsigned(n_sets_ok)) = b_ok + 1,
"a two-interface set was rejected");
end loop;
-- ---- Phase C: bLength = 0 is a HANG, and has its own code. ----
for k in 1 to 20 loop
b_e := to_integer(unsigned(n_zerolen));
start_set;
emit(9, 9, T_CONFIG, b8(25), Z8, b8(1));
put(Z8);
end_set;
chk(to_integer(unsigned(n_zerolen)) = b_e + 1,
"a zero bLength was not reported as its own class -- the walk advances by bLength, so zero advances it by nothing and a host that does not bound its walk HANGS rather than failing");
chk(state = w_code(W_IDLE), "the walker did not stop after a zero length");
end loop;
-- ---- Phase D: shorter than the type requires, for each type. ----
for L in 2 to MIN_CFG - 1 loop
b_e := to_integer(unsigned(n_short));
start_set;
emit(L, L, T_CONFIG, b8(20), Z8, b8(1));
end_set;
chk(to_integer(unsigned(n_short)) = b_e + 1,
"a CONFIGURATION shorter than its minimum was accepted -- the record cannot hold the fields its type is defined to have, so every field the host reads from it comes from the NEXT record");
end loop;
for L in 2 to MIN_EP - 1 loop
b_e := to_integer(unsigned(n_short));
start_set;
-- wTotalLength is set to what the set ACTUALLY is (9 + 9 + L), so the
-- overrun check cannot fire first and mask the one under test.
emit(9, 9, T_CONFIG, b8(18 + L), Z8, b8(1));
emit(9, 9, T_INTERFACE, Z8, Z8, b8(1));
emit(L, L, T_ENDPOINT, x"81", x"02", Z8);
end_set;
chk(to_integer(unsigned(n_short)) = b_e + 1,
"an ENDPOINT shorter than its minimum was accepted");
end loop;
-- ---- Phase E: THE CHAPTER. wTotalLength disagrees with the sum. ----
for k in 1 to 20 loop
b_e := to_integer(unsigned(n_total));
start_set;
emit(9, 9, T_CONFIG, b8(33), Z8, b8(1)); -- says 33, is 32
emit(9, 9, T_INTERFACE, Z8, Z8, b8(2));
emit(7, 7, T_ENDPOINT, x"81", x"02", Z8);
emit(7, 7, T_ENDPOINT, x"02", x"02", Z8);
end_set;
chk(to_integer(unsigned(n_total)) = b_e + 1,
"wTotalLength disagreed with the sum of the bLengths and it was not reported -- the host walks by bLength and reads by wTotalLength, and when they differ it lands in the middle of a record and reports an error against a field that is perfectly correct");
b_e := to_integer(unsigned(n_overrun));
start_set;
emit(9, 9, T_CONFIG, b8(31), Z8, b8(1)); -- says 31, is 32
emit(9, 9, T_INTERFACE, Z8, Z8, b8(2));
emit(7, 7, T_ENDPOINT, x"81", x"02", Z8);
emit(7, 7, T_ENDPOINT, x"02", x"02", Z8);
end_set;
chk(to_integer(unsigned(n_overrun)) = b_e + 1,
"the walk ran past wTotalLength and it was not reported");
end loop;
-- ---- Phase F: bNumInterfaces disagrees with what is there. ----
for k in 1 to 20 loop
b_e := to_integer(unsigned(n_ifcount));
start_set;
emit(9, 9, T_CONFIG, b8(32), Z8, b8(2)); -- claims 2 interfaces
emit(9, 9, T_INTERFACE, Z8, Z8, b8(2)); -- ...there is 1
emit(7, 7, T_ENDPOINT, x"81", x"02", Z8);
emit(7, 7, T_ENDPOINT, x"02", x"02", Z8);
end_set;
chk(to_integer(unsigned(n_ifcount)) = b_e + 1,
"bNumInterfaces disagreed with the number of interface records present and it was not reported");
end loop;
-- ---- Phase G: bNumEndpoints disagrees. ----
for k in 1 to 20 loop
b_e := to_integer(unsigned(n_epcount));
start_set;
emit(9, 9, T_CONFIG, b8(32), Z8, b8(1));
emit(9, 9, T_INTERFACE, Z8, Z8, b8(3)); -- claims 3
emit(7, 7, T_ENDPOINT, x"81", x"02", Z8);
emit(7, 7, T_ENDPOINT, x"02", x"02", Z8); -- ...there are 2
end_set;
chk(to_integer(unsigned(n_epcount)) = b_e + 1,
"the LAST interface's endpoint count was not checked -- every other interface is checked when the next one starts, and the last one has no next");
b_e := to_integer(unsigned(n_epcount));
start_set;
emit(9, 9, T_CONFIG, b8(41), Z8, b8(2));
emit(9, 9, T_INTERFACE, Z8, Z8, b8(2)); -- claims 2, has 1
emit(7, 7, T_ENDPOINT, x"81", x"03", Z8);
emit(9, 9, T_INTERFACE, b8(1), Z8, b8(1));
emit(7, 7, T_ENDPOINT, x"02", x"02", Z8);
end_set;
chk(to_integer(unsigned(n_epcount)) = b_e + 1,
"an interface's endpoint count was not checked when the next interface began");
end loop;
-- ---- Phase H: an ENDPOINT with no INTERFACE above it. ----
for k in 1 to 20 loop
b_e := to_integer(unsigned(n_orphan));
start_set;
emit(9, 9, T_CONFIG, b8(16), Z8, b8(1));
emit(7, 7, T_ENDPOINT, x"81", x"02", Z8);
end_set;
chk(to_integer(unsigned(n_orphan)) = b_e + 1,
"an endpoint appeared before any interface and it was not reported -- it has no owner, and the host will attribute it to whatever interface comes next");
end loop;
-- ---- Phase I: ALL 256 bLength VALUES on the first record. ----
for L in 0 to 255 loop
b_e := to_integer(unsigned(n_zerolen));
b_ok := to_integer(unsigned(n_short));
start_set;
-- At least two bytes whenever bLength is non-zero, so the TYPE is
-- always known: SHORT is a statement about a length relative to a
-- type, and a record truncated before its type byte is an overrun
-- instead -- a different finding about a different defect.
if L = 0 then
emit(L, 1, T_CONFIG, b8(32), Z8, b8(1));
elsif L > 9 then
emit(L, 9, T_CONFIG, b8(32), Z8, b8(1));
elsif L < 2 then
emit(L, 2, T_CONFIG, b8(32), Z8, b8(1));
else
emit(L, L, T_CONFIG, b8(32), Z8, b8(1));
end if;
end_set;
if seen_len(L) = 0 then seen_len(L) := 1; n_len := n_len + 1; end if;
if L = 0 then
chk(to_integer(unsigned(n_zerolen)) = b_e + 1,
"bLength zero was not reported as a zero length");
chk(to_integer(unsigned(n_short)) = b_ok,
"bLength zero was classified as SHORT -- zero is a hang and short is a wrong value, and a reader needs to know which symptom to expect");
elsif L < MIN_CFG then
chk(to_integer(unsigned(n_short)) = b_ok + 1,
"a CONFIGURATION below its minimum length was accepted");
chk(to_integer(unsigned(n_zerolen)) = b_e,
"a short length was classified as zero");
else
chk(to_integer(unsigned(n_zerolen)) = b_e
and to_integer(unsigned(n_short)) = b_ok,
"a bLength at or above the type's minimum was rejected");
end if;
end loop;
-- ---- Phase J: EXHAUSTIVE. Every walker state x every input. ----
for st2 in 0 to 4 loop
for cb in 0 to 7 loop
step('0', '0', Z8, '0', '1');
case st2 is
when 0 => null;
when 1 => start_set;
when 2 => start_set; put(b8(9));
when 3 => start_set; put(b8(9)); put(T_CONFIG);
when others => start_set; put(Z8);
end case;
chk(state = w_code(walk_state_t'val(st2)),
"the sweep could not reach the walker state it meant to reach");
if (cb / 4) mod 2 = 1 then sd_v := '1'; else sd_v := '0'; end if;
if (cb / 2) mod 2 = 1 then bv_v := '1'; else bv_v := '0'; end if;
if cb mod 2 = 1 then ed_v := '1'; else ed_v := '0'; end if;
step(sd_v, bv_v, x"20", ed_v, '0');
step(sd_v, bv_v, x"20", ed_v, '0');
end loop;
end loop;
-- ---- Phase K: random bytes, with set boundaries thrown in ----
for k in 0 to 29999 loop
sd_v := rnd_lt(22, 1000);
bv_v := rnd_lt(78, 100);
ed_v := rnd_lt(25, 1000);
step(sd_v, bv_v, rnd_byte, ed_v, '0');
end loop;
-- ---- Phase L: and legal sets afterwards. ----
step('0', '0', Z8, '0', '1');
b_ok := to_integer(unsigned(n_sets_ok));
for k in 1 to 60 loop legal_set; end loop;
chk(to_integer(unsigned(n_sets_ok)) = b_ok + 60,
"the validator stopped accepting consistent descriptor sets");
-- ---- Final agreement ----
chk(to_integer(unsigned(n_desc)) = m_d, "n_desc disagrees with the model");
chk(to_integer(unsigned(n_zerolen)) = m_zl, "n_zerolen disagrees");
chk(to_integer(unsigned(n_short)) = m_sh, "n_short disagrees");
chk(to_integer(unsigned(n_overrun)) = m_ov, "n_overrun disagrees");
chk(to_integer(unsigned(n_total)) = m_to, "n_total disagrees");
chk(to_integer(unsigned(n_ifcount)) = m_ic, "n_ifcount disagrees");
chk(to_integer(unsigned(n_epcount)) = m_ep, "n_epcount disagrees");
chk(to_integer(unsigned(n_orphan)) = m_or, "n_orphan disagrees");
chk(to_integer(unsigned(n_sets_ok)) = m_sok, "n_sets_ok disagrees");
chk(n_len = 256, "not every bLength value was driven on the first record");
chk(n_si = 40, "not every walker state was crossed with every input combination");
chk(to_integer(unsigned(n_zerolen)) > 0, "a zero bLength was never seen");
chk(to_integer(unsigned(n_short)) > 0, "a short descriptor was never seen");
chk(to_integer(unsigned(n_overrun)) > 0, "the walk never ran past wTotalLength");
chk(to_integer(unsigned(n_total)) > 0, "wTotalLength never disagreed with the sum");
chk(to_integer(unsigned(n_ifcount)) > 0, "bNumInterfaces never disagreed");
chk(to_integer(unsigned(n_epcount)) > 0, "bNumEndpoints never disagreed");
chk(to_integer(unsigned(n_orphan)) > 0, "an orphan endpoint was never seen");
chk(to_integer(unsigned(n_sets_ok)) > 100,
"too few consistent sets to have tested for false positives");
write(ln, string'("REACH blength=") & integer'image(n_len) &
"/256 state-x-input=" & integer'image(n_si) &
"/40 steps=" & integer'image(n_steps));
writeline(output, ln);
write(ln, string'("COUNTERS desc=") & integer'image(to_integer(unsigned(n_desc))) &
" ok=" & integer'image(to_integer(unsigned(n_sets_ok))) &
" zerolen=" & integer'image(to_integer(unsigned(n_zerolen))) &
" short=" & integer'image(to_integer(unsigned(n_short))) &
" overrun=" & integer'image(to_integer(unsigned(n_overrun))) &
" total=" & integer'image(to_integer(unsigned(n_total))) &
" ifcount=" & integer'image(to_integer(unsigned(n_ifcount))) &
" epcount=" & integer'image(to_integer(unsigned(n_epcount))) &
" orphan=" & integer'image(to_integer(unsigned(n_orphan))));
writeline(output, ln);
if errors = 0 then
write(ln, string'("PASS: 0 errors in ") & integer'image(checks) & " checks");
else
write(ln, string'("FAIL: ") & integer'image(errors) & " errors in " &
integer'image(checks) & " checks");
end if;
writeline(output, ln);
done <= true;
wait;
end process;
end architecture sim;11. Exhaustive Verification
| Measure | Verilog | SystemVerilog | VHDL |
|---|---|---|---|
| bLength values on record 0 | 256 / 256 | 256 / 256 | 256 / 256 |
| walker state x input | 40 / 40 | 40 / 40 | 40 / 40 |
| Steps | 45167 | 45167 | 45167 |
| Checks executed | 587910 | 587910 | 587910 |
| descriptors walked | 2257 | 2260 | 2242 |
| consistent sets accepted | 256 | 259 | 262 |
| zero lengths | 30 | 34 | 30 |
| short descriptors | 20 | 21 | 22 |
| walk overruns | 345 | 376 | 335 |
| wTotalLength mismatches | 271 | 269 | 270 |
| bNumInterfaces mismatches | 20 | 20 | 20 |
| bNumEndpoints mismatches | 41 | 42 | 40 |
| orphan endpoints | 20 | 22 | 22 |
| Result | PASS | PASS | PASS |
The 256-value sweep is the strongest claim here: it is not "we tried a short one and a long one", it is "for every possible value of that byte, the validator's verdict is the right one, and the two rejection classes are distinguished."
12. Mutation Testing
| # | Mutation | Verilog | SysVer | VHDL |
|---|---|---|---|---|
| P6 | bNumEndpoints is never compared, at either site | 1526 | 1527 | 1524 |
| P7 | an orphan endpoint is silently adopted | 363 | 491 | 483 |
| P1 | wTotalLength is never compared with the sum | 336 | 334 | 335 |
| P3 | a record shorter than its type is walked anyway | 273 | 405 | 689 |
| P2 | a zero bLength is accepted | 178 | 452 | 191 |
| P4 | the walk is not bounded by wTotalLength | 122 | 122 | 122 |
| P5 | bNumInterfaces is never compared | 83 | 83 | 83 |
| — | unmutated baseline | 0 | 0 | 0 |
All seven die in all three languages, all counts distinct.
P6 is the largest because bNumEndpoints has to be checked in two places — at the end of the set, and again whenever the next interface begins — and the mutation removes both. Every interface in the run then goes unchecked.
P4 and P5 score identically in all three languages (122 and 83), which by now is a familiar signature: both are detected entirely by directed phases, because random bytes essentially never produce a plausible configuration descriptor whose interface count is merely off by one.
13. Debugging Walkthrough: The Interface Class Nobody Recognises
The report. A composite device enumerates on Linux and fails on Windows. The Windows device manager shows one working interface and one with an unknown class code — 0x91, which is not a class code at all.
Step 1 — is 0x91 in the descriptor? Dump the bytes the device actually returns. Search for 0x91. It is not there. No byte in the buffer is 0x91 at the offset of any bInterfaceClass.
Step 2 — so where did Windows get it? Count bytes by hand. 0x91 appears once, as the third byte of the second endpoint descriptor — wMaxPacketSize, low byte, 0x0091 = 145 bytes.
Step 3 — so Windows is reading the second interface at the wrong offset. By exactly three bytes.
Step 4 — find the three bytes. The first interface descriptor declares bLength = 12. The interface descriptor is defined to be 9 bytes long. The device is emitting 9 bytes and claiming 12.
Step 5 — why Linux works. Linux's parser validates bLength against the descriptor type and rejects the set. It then falls back to a second enumeration attempt that reads the descriptor differently, and happens to succeed. Windows trusts bLength and walks.
Step 6 — and why the report named a field that was correct. wMaxPacketSize was written correctly by the firmware. It was read as bInterfaceClass because the pointer was three bytes ahead, and the pointer was three bytes ahead because of a field in a different record that nobody was looking at.
14. UVM: Validating Descriptors as Transactions
// A descriptor set arrives as bytes and is USED as a structure, and those
// are two different objects. The environment needs both: the bytes, because
// that is what the device actually sent, and the structure, because that is
// what the host will act on.
//
// The validator lives between them, and it is the only place that knows
// they can disagree.
class usb_descriptor_item extends uvm_sequence_item;
`uvm_object_utils(usb_descriptor_item)
// THE BYTES. Kept verbatim, because the walkthrough in section 13 is
// impossible without them: "search the raw bytes for the value that was
// reported" needs the raw bytes.
rand byte unsigned raw[];
function new(string name = "usb_descriptor_item"); super.new(name); endfunction
// The three numbers that must agree, computed from the bytes rather than
// carried alongside them. A structure field that is SET by the generator
// and then CHECKED by the checker proves only that the generator and the
// checker agree with each other.
function int unsigned sum_of_lengths();
int unsigned p = 0, total = 0;
while (p < raw.size()) begin
if (raw[p] == 0) return -1; // zero length: the walk cannot advance
total += raw[p];
p += raw[p];
end
return total;
endfunction
function int unsigned declared_total();
if (raw.size() < 4) return 0;
return {raw[3], raw[2]}; // wTotalLength, little-endian
endfunction
endclass
class usb_descriptor_checker extends uvm_component;
`uvm_component_utils(usb_descriptor_checker)
uvm_analysis_imp #(usb_descriptor_item, usb_descriptor_checker) ap;
int unsigned n_sets, n_ok;
int unsigned n_err[string];
function new(string name, uvm_component parent);
super.new(name, parent);
ap = new("ap", this);
endfunction
function void flag(string cause, string detail);
n_err[cause]++;
`uvm_error("DESC", $sformatf("%s: %s", cause, detail))
endfunction
function void write(usb_descriptor_item t);
int unsigned p = 0;
int unsigned if_seen = 0, if_expected = 0;
int unsigned ep_seen = 0, ep_expected = 0;
bit have_if = 0, have_cfg = 0;
n_sets++;
while (p < t.raw.size()) begin
byte unsigned blen, btype;
// ---- A ZERO LENGTH IS A HANG. The bound on this loop is the whole
// ---- reason a host does not lock up on a broken device, and it is
// ---- the reason this check comes first.
blen = t.raw[p];
if (blen == 0) begin
flag("ZEROLEN",
$sformatf("a record at offset %0d claims bLength 0 -- a host that does not bound its walk does not FAIL on this, it HANGS",
p));
return;
end
if (p + 1 >= t.raw.size()) begin
flag("OVERRUN",
$sformatf("the set ends inside a record that began at offset %0d", p));
return;
end
btype = t.raw[p+1];
// ---- Shorter than the type requires: every field read from this
// ---- record comes from the NEXT one.
if (blen < min_len(btype)) begin
flag("SHORT",
$sformatf("a type-%0d record at offset %0d declares bLength %0d, below the %0d its type requires -- every field the host reads from here comes from the next record, and the error it eventually reports will be against a field that is perfectly correct",
btype, p, blen, min_len(btype)));
return;
end
case (btype)
2: begin // CONFIGURATION
have_cfg = 1;
if_expected = t.raw[p+4];
end
4: begin // INTERFACE
if (have_if && ep_seen != ep_expected)
flag("EPCOUNT",
$sformatf("an interface declared bNumEndpoints %0d and %0d endpoint records followed it",
ep_expected, ep_seen));
have_if = 1;
if_seen++;
ep_seen = 0;
ep_expected = t.raw[p+4];
end
5: begin // ENDPOINT
if (!have_if)
flag("ORPHAN",
$sformatf("an endpoint at offset %0d has no interface above it -- the host will attribute it to whatever interface comes next",
p));
else ep_seen++;
end
default: ;
endcase
p += blen;
end
// ---- The aggregate checks, which cannot be made record by record. ----
if (have_if && ep_seen != ep_expected)
// The LAST interface. Every other one is checked when the next
// begins; this one has no next, and forgetting it is the single most
// common gap in a hand-written descriptor checker.
flag("EPCOUNT",
$sformatf("the last interface declared bNumEndpoints %0d and %0d endpoint records followed it",
ep_expected, ep_seen));
else if (have_cfg && t.declared_total() != p)
flag("TOTAL",
$sformatf("wTotalLength says %0d and the bLengths sum to %0d -- the host reads by the first and walks by the second, so it will land %0d bytes into a record and report an error against a field that is correct",
t.declared_total(), p, (int'(p) - int'(t.declared_total()))));
else if (have_cfg && if_seen != if_expected)
flag("IFCOUNT",
$sformatf("bNumInterfaces says %0d and %0d interface records are present",
if_expected, if_seen));
else
n_ok++;
endfunction
function int unsigned min_len(byte unsigned t);
case (t)
2: return 9; // CONFIGURATION
4: return 9; // INTERFACE
5: return 7; // ENDPOINT
default: return 2;
endcase
endfunction
function void report_phase(uvm_phase phase);
`uvm_info("DESC", $sformatf("sets=%0d consistent=%0d %p",
n_sets, n_ok, n_err), UVM_LOW)
// A validator that never saw a CONSISTENT set has not been shown to be
// quiet on legal input, and a validator that is not quiet on legal
// input gets switched off.
if (n_ok == 0)
`uvm_error("COVERAGE",
"no descriptor set was ever accepted -- either nothing was driven or the validator rejects everything, and a validator that rejects everything is one nobody leaves enabled")
endfunction
endclass15. Common Misconceptions
"The field named in the error is the field that is wrong." If the parser is misaligned, every field name in the report is an offset into a structure that is not there.
"bLength too small is just a short read." Every field after it comes from the next record, and the report names a field that was written correctly.
"bLength = 0 is a variant of too-short." It is a hang, not a wrong value, and the symptoms are nothing alike.
"wTotalLength and the sum of bLength are the same number." They are two fields that must agree, computed by different parts of the firmware, and they drift independently.
"The last interface's endpoint count is checked like the others." Every other interface is checked when the next begins. The last one has no next.
"An orphan endpoint is harmless." The host attributes it to whatever interface comes next, silently changing that interface's shape.
"Hosts validate descriptors, so a bad one fails safely." They validate differently. Linux rejected the set in §13; Windows walked it.
"One error per defect is automatic." A walk that has stopped still has wrong aggregate counters, and re-checking them reports a second bug that does not exist.
16. Exercises
1. A configuration set declares wTotalLength = 34 and contains records of lengths 9, 9, 7, 7. Work out which byte of which record the host reads as bDescriptorType for the record after the end, and say what it will conclude.
2. P4 and P5 score exactly 122 and 83 in all three languages. Identify the directed phases responsible and explain why random bytes essentially never exercise them.
3. Show that a host bounding its walk by wTotalLength alone still hangs on bLength = 0, and write the second bound that fixes it.
4. The end-of-set checks are skipped in W_DEAD. Construct the descriptor set that produces two errors for one defect without that exclusion, and name both.
5. Add support for a class-specific descriptor of a type the validator does not know. What must it check, what must it not check, and why is "ignore it" the wrong answer for bLength?
6. The UVM checker recomputes every number from raw[]. Write the version that trusts a stored field instead and construct the device bug it cannot see.
17. Summary
| Idea | Why it matters |
|---|---|
| A descriptor set is a self-describing tree | its structure is implied by order and three counts |
| The buffer is described three times | wTotalLength, bNumInterfaces, bNumEndpoints |
| The host reads by one field and walks by another | so disagreement misaligns everything after it |
| The error names a correct field | symptom and cause are separated by a variable gap |
bLength = 0 is a hang, not a wrong value | the walk advances by zero, for ever |
| Short is a different bug | the record cannot hold the fields its type defines |
| The last interface has no next interface | so its endpoint count needs its own check |
| An orphan endpoint is adopted by the next interface | silently changing that interface's shape |
| A dead walk must not re-report | or one defect becomes two |
| One injected defect at a time | or the suite measures whichever check is first |
| Recompute from the bytes | a stored field proves the generator agrees with the checker |
| 256/256 bLength values, 40/40 state x input | 7 mutations, all killed in 3 languages |
Tooling
| Step | Command |
|---|---|
| Verilog-2005 | iverilog -g2005 -o dv_v.out dv_v.v dv_v_tb.v && ./dv_v.out |
| SystemVerilog | iverilog -g2012 -o dv_sv.out dv_sv.sv dv_sv_tb.sv && ./dv_sv.out |
| VHDL-2008 analyse | nvc --std=2008 -a dv_vhdl.vhd dv_vhdl_tb.vhd |
| VHDL-2008 elaborate | nvc --std=2008 -e tb_dv_vhdl |
| VHDL-2008 run | nvc --std=2008 -r tb_dv_vhdl |
| One mutation | iverilog -g2005 -DMUT_P1 -o mm dv_v_mut.v dv_v_tb.v && ./mm |
All three implementations pass with 0 errors: every one of the 256 possible bLength values driven on the first record with the verdict checked and the two rejection classes distinguished, every walker state crossed with every input, and 180 consistent sets of three shapes accepted with no false positives.
Chapter 25.3 — Endpoint Problems moves from the descriptor to the endpoint it describes. The distinction that defines it is one this module has already touched twice: a NAK is not an error and a STALL is not a NAK. One is flow control working, one is firmware saying no permanently, and a health monitor that treats them alike either floods the log on a busy bus or misses a wedged endpoint entirely.
Continue learning
Related tutorials
- Related topic
Descriptor Engine
wLength is the size of the host's buffer, not a preference — and whether a zero-length packet must follow depends on comparing what was sent against what was asked for, not against what exists.
- Related topic
Enumeration Failures
A failing enumeration retries from the beginning, so the current step is always ATTACH and tells you nothing — the furthest step ever reached is the diagnosis, and a bus reset must not clear it.
- Related topic
Endpoint Problems
A NAK is not an error and a STALL is not a NAK — one is flow control working, one is firmware refusing permanently, and a monitor that treats them alike either floods the log or misses the endpoint that has stopped.
- Related topic
Transfer Errors
Retries mask the error rate — a link losing a third of its traffic reports a perfect transfer success rate, because the denominator everybody uses is the wrong one.
Standards & specifications
- Governing standard
- USB-IF (Universal Serial Bus Specification)(opens USB Implementers Forum (USB-IF) in a new tab)
Defines the USB bus — its electrical signalling, connectors, packet and transaction model, device framework and the descriptors a device must expose — together with the device-class specifications layered on it. It does not define host-controller register interfaces (xHCI and EHCI are separate documents) nor any operating system's driver architecture.
This page also covers RTL structure, verification approach and debugging technique. Those are engineering practice built on the standard, not requirements the standard itself imposes.
Where this fits
Part of the USB curriculum.
