1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
|
Assertion failure in usb_cancel_packet()
Description of problem:
When I ran hcd-ohci with dev-storage, I found an assertion failure in
usb_cancel_packet() [1] due to p->state == USB_PACKET_COMPLETE. This is due to
the inconsistency when resetting device.
``` c
static inline bool usb_packet_is_inflight(USBPacket *p)
{
return (p->state == USB_PACKET_QUEUED ||
p->state == USB_PACKET_ASYNC);
}
void usb_cancel_packet(USBPacket * p)
{
bool callback = (p->state == USB_PACKET_ASYNC);
assert(usb_packet_is_inflight(p)); // <------------------------------- [1]
usb_packet_set_state(p, USB_PACKET_CANCELED);
QTAILQ_REMOVE(&p->ep->queue, p, queue);
if (callback) {
usb_device_cancel_packet(p->ep->dev, p);
}
}
```
Steps to reproduce:
Step 1: download the prepared rootfs and the image.
https://drive.google.com/file/d/1B95zWWcomvZt1wms31Ddc9Xwlq-bfqhq/view?usp=sharing
https://drive.google.com/file/d/1pxFzn49MKYmMMIIsaL9aUkzebRSYfq3J/view?usp=sharing
Step 2: run the following script.
``` bash
QEMU_PATH=../../../qemu/build/qemu-system-x86_64
KERNEL_PATH=./bzImage
ROOTFS_PATH=./rootfs.ext2
$QEMU_PATH \
-M q35 -m 1G \
-kernel $KERNEL_PATH \
-drive file=$ROOTFS_PATH,if=virtio,format=raw \
-append "root=/dev/vda console=ttyS0" \
-net nic,model=virtio -net user \
-usb \
-device pci-ohci,num-ports=6 \
-drive file=null-co://,if=none,format=raw,id=disk0 \
-device usb-storage,port=1,drive=disk0 \
-nographic
```
Step 3: with spawned shell (the user is root and the password is empty), run
`ohci-03`.
Additional information:
1 With crafted ED and TD, we can have the ohci->usb_packet's status to be
USB_RET_ASYNC [5]. And thus ohci->async_td is not NULL anymore [2].
```
ed0 = { flags = 0x685f0900, tail = 0x0, head = &td0, next = 0 }
td0 = { flags = 0x0, cbp = 0x1b8ffc, next = 0, be = 0x1b901a }
# data from cbp to be
55 53 42 43 00 00 00 00 00 00 00 00 00 00 00 03 USBC............
e8 56 20 40 e8 56 20 40 e8 56 20 40 e8 56 20
ed1 = { flags = 0x08303080, tail = 0x0, head = &td1, next = 0 }
td1 = { flags = 0x90000000, cbp = 0x19affc, next = 0, be = 0x19b01a }
# data from cbp to be
55 53 42 43 00 00 00 00 00 00 00 00 00 00 00 03 USBC............
e8 56 20 40 e8 56 20 40 e8 56 20 40 e8 56 20
```
``` c
static int ohci_service_td(OHCIState *ohci, struct ohci_ed *ed)
{
// ...
usb_handle_packet(dev, &ohci->usb_packet); // <------------------- [4]
if (ohci->usb_packet.status == USB_RET_ASYNC) {
usb_device_flush_ep_queue(dev, ep);
ohci->async_td = addr; // <----------------------------------- [2]
return 1;
}
```
At the same time, the dev-storage will ref the current usb_packet
(ohci->usb_packet) [4][3].
```
static void usb_msd_handle_data(USBDevice *dev, USBPacket *p) {
// ...
s->packet = p; // <----------------------------------------------- [3]
p->status = USB_RET_ASYNC; // <----------------------------------- [5]
// ...
}
```
2 We can first issue `MMIO_WRITE, 0xe0000054, 0x4, 0x4e33b4bf` to reset
the dev-storage device. This will mark the state of ohci->usb_packet to
USB_PACKET_COMPLETE and clear s->packet.
```
ohci_mem_write
ohci_port_set_status
usb_device_reset
usb_device_handle_reset
usb_msd_handle_reset
usb_msd_packet_complete
usb_packet_complete
```
3 We can then issue `MMIO_WRITE, 0xe0000004, 0x4, 0x3d8d323a` to reset the
roothub and this will invoke ohci_stop_endpoints() where usb_cancel_packet()
is invoked and thus [1] fails as the state of ohci->usb_packet has been changed
to USB_PACKET_COMPLETE.
```
ohci_set_ctl
ohci_roothub_reset
ohci_stop_endpoints
if (ohci->async_td != NULL) usb_cancel_packet(&ohci->usb_packet);
assert(usb_packet_is_inflight(p)); // boom
```
The above callstack are simplified. The complete callstack is in the following.
```
ohci_set_ctl
ohci_roothub_reset
usb_port_reset
usb_detach
ohci_detach
ohci_child_detach // <-------------------------------- [8]
usb_device_reset // <----------------------------------------- [6]
usb_device_handle_reset
usb_msd_handle_reset
usb_msd_packet_complete
usb_packet_complete
ohci_stop_endpoints // <------------------------------------------ [7]
if (ohci->async_td != NULL) usb_cancel_packet(&ohci->usb_packet);
assert(usb_packet_is_inflight(p)); // boom
```
Interestingly, in ohci_roothub_reset(), usb_device_reset() is also invoked [6]
just like what in step 2. I adjusted my PoC by removing step 2. However, I
cannot reproduce this assertion failure. Therefore, there is something different
bewteen [6] and step 2.
Then, I found at [8], ohci_child_detach() cancels the ohci->usb_packet and reset
ohci->async_td. With step 2, as the status of the ohci->usb_packet has changed
to USB_PACKET_COMPLETE, usb_cancel_packet() will not be invoked. Without step 2,
as the status of the ohci->usb_packet is still USB_PACKET_ASYNC,
usb_cancel_packet() will be invoked and thus everything goes fine.
```
static void ohci_child_detach(USBPort *port1, USBDevice *dev)
{
OHCIState *ohci = port1->opaque;
if (ohci->async_td &&
usb_packet_is_inflight(&ohci->usb_packet) &&
ohci->usb_packet.ep->dev == dev) {
usb_cancel_packet(&ohci->usb_packet);
ohci->async_td = 0;
}
}
```
|