@samitouri / QOSamiQemu / commits / 26c871c768

parallels: reject BAT entries pointing outside backed storage

parallels_open()'s BAT scan and parallels_check_outside_image() only checked entries against the file's upper end, matching just half of what docs/interop/parallels.rst requires: an entry's offset must be both >= data_start and < the file size. An entry below data_start resolves into the header/BAT region itself, corrupting metadata on write or losing the write silently on a partial overlap, and neither qemu-img check nor the open-time scan ever caught it. Check both bounds everywhere a BAT entry is resolved to a host offset: seek_to_sector(), the open-time scan (without letting a bad entry inflate data_end), and parallels_check_outside_image(). Signed-off-by: Denis V. Lunev <den@openvz.org> CC: Thomas Huth <thuth@redhat.com> CC: Stefan Hajnoczi <stefanha@redhat.com>

Denis V. Lunev committed Jul 22, 2026 at 18:54 UTC 26c871c76885b1e03786d2cd6da090bc30e8faf2
3 files changed +116 -4
block/parallels.c
+23 -4
@@ -119,6 +119,7 @@ static uint32_t bat_entry_off(uint32_t idx)
119 static int64_t seek_to_sector(BDRVParallelsState *s, int64_t sector_num)
120 {
121 uint32_t index, offset;
122 + int64_t cluster_off;
123
124 index = sector_num / s->tracks;
125 offset = sector_num % s->tracks;
@@ -127,7 +128,14 @@ static int64_t seek_to_sector(BDRVParallelsState *s, int64_t sector_num)
128 if ((index >= s->bat_size) || (s->bat_bitmap[index] == 0)) {
129 return -1;
130 }
130 - return bat2sect(s, index) + offset;
131 +
132 + cluster_off = bat2sect(s, index);
133 + if (cluster_off < s->data_start || cluster_off + s->tracks > s->data_end) {
134 + /* Cluster is outside of the image file or overlaps the header. */
135 + return -1;
136 + }
137 +
138 + return cluster_off + offset;
139 }
140
141 static int cluster_remainder(BDRVParallelsState *s, int64_t sector_num,
@@ -703,18 +711,22 @@ parallels_check_outside_image(BlockDriverState *bs, BdrvCheckResult *res,
711 {
712 BDRVParallelsState *s = bs->opaque;
713 uint32_t i;
706 - int64_t off, high_off, size;
714 + int64_t off, high_off, size, data_start_off;
715
716 size = bdrv_co_getlength(bs->file->bs);
717 if (size < 0) {
718 res->check_errors++;
719 return size;
720 }
721 + data_start_off = s->data_start << BDRV_SECTOR_BITS;
722
723 high_off = 0;
724 for (i = 0; i < s->bat_size; i++) {
725 off = bat2sect(s, i) << BDRV_SECTOR_BITS;
717 - if (off + s->cluster_size > size) {
726 + if (off == 0) {
727 + continue;
728 + }
729 + if (off < data_start_off || off + s->cluster_size > size) {
730 fprintf(stderr, "%s cluster %u is outside image\n",
731 fix & BDRV_FIX_ERRORS ? "Repairing" : "ERROR", i);
732 res->corruptions++;
@@ -1398,11 +1410,18 @@ static int parallels_open(BlockDriverState *bs, QDict *options, int flags,
1410
1411 for (i = 0; i < s->bat_size; i++) {
1412 sector = bat2sect(s, i);
1413 + if (sector == 0) {
1414 + continue; /* not allocated */
1415 + }
1416 + if (sector < data_start || sector + s->tracks > file_nb_sectors) {
1417 + /* Cluster is outside of the image file or overlaps the header. */
1418 + need_check = true;
1419 + continue;
1420 + }
1421 if (sector + s->tracks > s->data_end) {
1422 s->data_end = sector + s->tracks;
1423 }
1424 }
1405 - need_check = need_check || s->data_end > file_nb_sectors;
1425
1426 if (!need_check) {
1427 ret = parallels_fill_used_bitmap(bs);
tests/qemu-iotests/tests/parallels-checks
+58
@@ -222,6 +222,64 @@ _img_info
222 echo "== an unallocated cluster still reads as zeroes =="
223 { $QEMU_IO -r -c "read -P 0x00 0 $CLUSTER_SIZE" "$TEST_IMG"; } 2>&1 | _filter_qemu_io | _filter_testdir
224
225 +# Clear image
226 +_make_test_img $SIZE
227 +
228 +echo "== TEST BAT ENTRY POINTING OUTSIDE IMAGE =="
229 +
230 +echo "== corrupt image: point first cluster far outside the file =="
231 +poke_file_le "$TEST_IMG" $BAT_OFFSET 4 1000000
232 +
233 +echo "== read-only read must return zeroes, not an I/O error =="
234 +{ $QEMU_IO -r -c "read -P 0x00 0 $CLUSTER_SIZE" "$TEST_IMG"; } 2>&1 | _filter_qemu_io | _filter_testdir
235 +
236 +echo "== write must allocate a fresh cluster instead of trusting the entry =="
237 +{ $QEMU_IO -c "write -P 0x77 0 $CLUSTER_SIZE" "$TEST_IMG"; } 2>&1 | _filter_qemu_io | _filter_testdir
238 +
239 +echo "== file did not grow anywhere near the bogus offset =="
240 +file_size=`stat --printf="%s" "$TEST_IMG"`
241 +if [ "$file_size" -lt $((16 * 1024 * 1024)) ]; then
242 + echo "file size sane: yes"
243 +else
244 + echo "file size sane: no ($file_size bytes)"
245 +fi
246 +
247 +echo "== data reads back correctly =="
248 +{ $QEMU_IO -r -c "read -P 0x77 0 $CLUSTER_SIZE" "$TEST_IMG"; } 2>&1 | _filter_qemu_io | _filter_testdir
249 +
250 +# Clear image, with a small cluster size so the BAT table itself spans
251 +# more than one cluster and there is room to point before data_off.
252 +_make_test_img -o cluster_size=512 65536
253 +
254 +SMALL_CLUSTER_SIZE=$(peek_file_le $TEST_IMG $CLUSTER_SIZE_OFFSET 4)
255 +SMALL_CLUSTER_SIZE=$((SMALL_CLUSTER_SIZE * 512))
256 +DATA_OFF=$(peek_file_le $TEST_IMG $DATA_OFF_OFFSET 4)
257 +echo "cluster size: $SMALL_CLUSTER_SIZE, data offset (sectors): $DATA_OFF"
258 +
259 +# Cluster index 1 starts at this byte offset, which must be < data_off
260 +# in sectors * 512 for this test to actually exercise the bug.
261 +VICTIM_OFFSET=$SMALL_CLUSTER_SIZE
262 +
263 +echo "== TEST BAT ENTRY POINTING BEFORE DATA AREA =="
264 +
265 +echo "== corrupt image: point first cluster into the BAT table itself =="
266 +poke_file_le "$TEST_IMG" $BAT_OFFSET 4 1
267 +
268 +echo "== qemu-img check detects it without repairing =="
269 +_check_test_img
270 +
271 +echo "== bytes at the victim offset before write =="
272 +echo "$(peek_file_le "$TEST_IMG" $VICTIM_OFFSET 4)"
273 +
274 +echo "== write must allocate a fresh cluster instead of clobbering the BAT =="
275 +{ $QEMU_IO -c "write -P 0x88 0 $SMALL_CLUSTER_SIZE" "$TEST_IMG"; } 2>&1 | _filter_qemu_io | _filter_testdir
276 +
277 +echo "== bytes at the victim offset are unchanged =="
278 +echo "$(peek_file_le "$TEST_IMG" $VICTIM_OFFSET 4)"
279 +
280 +echo "== data reads back correctly =="
281 +{ $QEMU_IO -r -c "read -P 0x88 0 $SMALL_CLUSTER_SIZE" "$TEST_IMG"; } 2>&1 | _filter_qemu_io | _filter_testdir
282 +
283 # success, all done
284 echo "*** done"
285 rm -f $seq.full
tests/qemu-iotests/tests/parallels-checks.out
+35
@@ -140,4 +140,39 @@ virtual size: 4 MiB (4194304 bytes)
140 == an unallocated cluster still reads as zeroes ==
141 read 1048576/1048576 bytes at offset 0
142 1 MiB, X ops; XX:XX:XX.X (XXX YYY/sec and XXX ops/sec)
143 +Formatting 'TEST_DIR/t.IMGFMT', fmt=IMGFMT size=4194304
144 +== TEST BAT ENTRY POINTING OUTSIDE IMAGE ==
145 +== corrupt image: point first cluster far outside the file ==
146 +== read-only read must return zeroes, not an I/O error ==
147 +read 1048576/1048576 bytes at offset 0
148 +1 MiB, X ops; XX:XX:XX.X (XXX YYY/sec and XXX ops/sec)
149 +== write must allocate a fresh cluster instead of trusting the entry ==
150 +Repairing cluster 0 is outside image
151 +wrote 1048576/1048576 bytes at offset 0
152 +1 MiB, X ops; XX:XX:XX.X (XXX YYY/sec and XXX ops/sec)
153 +== file did not grow anywhere near the bogus offset ==
154 +file size sane: yes
155 +== data reads back correctly ==
156 +read 1048576/1048576 bytes at offset 0
157 +1 MiB, X ops; XX:XX:XX.X (XXX YYY/sec and XXX ops/sec)
158 +Formatting 'TEST_DIR/t.IMGFMT', fmt=IMGFMT size=65536
159 +cluster size: 512, data offset (sectors): 2
160 +== TEST BAT ENTRY POINTING BEFORE DATA AREA ==
161 +== corrupt image: point first cluster into the BAT table itself ==
162 +== qemu-img check detects it without repairing ==
163 +ERROR cluster 0 is outside image
164 +
165 +1 errors were found on the image.
166 +Data may be corrupted, or further writes to the image may corrupt it.
167 +== bytes at the victim offset before write ==
168 +0
169 +== write must allocate a fresh cluster instead of clobbering the BAT ==
170 +Repairing cluster 0 is outside image
171 +wrote 512/512 bytes at offset 0
172 +512 bytes, X ops; XX:XX:XX.X (XXX YYY/sec and XXX ops/sec)
173 +== bytes at the victim offset are unchanged ==
174 +0
175 +== data reads back correctly ==
176 +read 512/512 bytes at offset 0
177 +512 bytes, X ops; XX:XX:XX.X (XXX YYY/sec and XXX ops/sec)
178 *** done