Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,12 @@ jobs:
-v /opt/build-tools:/mnt/build-tools:ro \
-v $GITHUB_WORKSPACE:/mnt/project:rw my_docker_image /bin/bash -c "cd /mnt/project && make -B -C target/pc/linux/test_http_target test-integer-widths"

- name: Test G64 track bounds
run: |
docker run --rm \
-v /opt/build-tools:/mnt/build-tools:ro \
-v $GITHUB_WORKSPACE:/mnt/project:rw my_docker_image /bin/bash -c "cd /mnt/project && make -C software/drive/tests"

- name: Test UCI Palette Commands
run: |
docker run --rm \
Expand Down
7 changes: 7 additions & 0 deletions run-tests
Original file line number Diff line number Diff line change
Expand Up @@ -295,6 +295,13 @@ SUITES: Sequence[Suite] = (
# oversized D81 and proves the bounded layout leaves the device usable.
Suite("e2e", "d81-track-bounds", "tests/e2e/drive/d81_track_bounds_test.py",
"-H @HOST@ -p @PASS@ -t @TIMEOUT@ --test all", profile=profiles.QUICK),
# Mounts a G71 whose MFM-marked track is malformed -- one declaring a
# single byte where the metadata area alone is 162, and one declaring
# 0x4123, which is 16675 read as a length and 291 read as fourteen bits --
# and proves the GCR side of the same disk still reads afterwards. The
# first exercise of the mixed GCR/MFM path on real hardware.
Suite("e2e", "g71-mfm-bounds", "tests/e2e/drive/g71_mfm_bounds_test.py",
"-H @HOST@ -p @PASS@ -t @TIMEOUT@", profile=profiles.QUICK),
# Calls every endpoint the firmware registers, and fails when one is
# neither probed nor excluded with a reason. The lockup that prompted it
# shipped because no suite called that route at all, so the gate matters
Expand Down
61 changes: 34 additions & 27 deletions software/drive/c1541.cc
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
#include <sys/stat.h>
#include <ctype.h>
#include <errno.h>
#include "gcr_track_bounds.h"

#include "itu.h"
#include "dump_hex.h"
Expand Down Expand Up @@ -461,40 +462,30 @@ void C1541 :: insert_disk(bool protect, GcrImage *image)
uint32_t rotation_speed = (CLOCK_FREQ / 20); // 2 (half clocks) * 1/8 (bytes) * clocks per track. 300 RPM = 5 RPS. (5 * 8 / 2) = 20

// side 0
uint32_t bit_time;
uint32_t track_len;
volatile uint32_t *param = (volatile uint32_t *)&registers[C1541_PARAM_RAM];
for(int i=0; i < GCRIMAGE_FIRSTTRACKSIDE1; i++) {
GcrTrack *tr = &image->tracks[i];
if (tr->track_address) {
bit_time = rotation_speed / tr->track_length;
track_len = tr->track_length;
// printf("Side0: %2d %08x %08x %d\n", i, tr->track_address, track_len, bit_time);
*(param++) = (uint32_t)tr->track_address;
*(param++) = (track_len-1) | (bit_time << 16);
} else {
// printf("Side0: %2d %08x %08x %d\n", i, tr->track_address, track_len, bit_time);
*(param++) = (uint32_t)dummy_track;
*(param++) = (track_len-1) | (bit_time << 16);
}
uint32_t track_addr, track_param;
gcr_track_parameters((uint32_t)tr->track_address, tr->track_length,
(uint32_t)dummy_track, GCRIMAGE_DUMMYTRACKLEN,
rotation_speed, &track_addr, &track_param);
// printf("Side0: %2d %08x %08x\n", i, track_addr, track_param);
*(param++) = track_addr;
*(param++) = track_param;
registers[C1541_DIRTYFLAGS + i/2] = 0;
}
}

// Side 1
param = (volatile uint32_t *)&registers[C1541_PARAM_RAM + 0x400]; // side 1
for(int i=GCRIMAGE_FIRSTTRACKSIDE1; i < GCRIMAGE_MAXHDRTRACKS; i++) {
GcrTrack *tr = &image->tracks[i];
if (tr->track_address) {
bit_time = rotation_speed / tr->track_length;
track_len = tr->track_length;
// printf("Side1: %2d %08x %08x %d\n", i, tr->track_address, tr->track_length, bit_time);
*(param++) = (uint32_t)tr->track_address;
*(param++) = (track_len-1) | (bit_time << 16);
} else {
// printf("Side1: %2d %08x %08x %d\n", i, tr->track_address, tr->track_length, bit_time);
*(param++) = (uint32_t)dummy_track;
*(param++) = (track_len-1) | (bit_time << 16);
}
uint32_t track_addr, track_param;
gcr_track_parameters((uint32_t)tr->track_address, tr->track_length,
(uint32_t)dummy_track, GCRIMAGE_DUMMYTRACKLEN,
rotation_speed, &track_addr, &track_param);
// printf("Side1: %2d %08x %08x\n", i, track_addr, track_param);
*(param++) = track_addr;
*(param++) = track_param;
registers[C1541_DIRTYFLAGS + i/2] = 0;
}

Expand Down Expand Up @@ -527,7 +518,14 @@ void C1541 :: map_gcr_image_to_mfm(void)
if (!gcr || !mtr) {
continue;
}
mtr->reservedSpace = gtr->track_length - MFM_TRACK_HEADER_SIZE;
// A track shorter than the metadata area cannot carry one, whatever
// its marker says, and there is no space behind an area that does
// not fit. Leave the entry as init() zeroed it: no sectors, no
// reserved space, so UpdateTrack() admits no write for this track.
if (!gcr_track_can_hold_mfm_header(gtr->track_length, MFM_TRACK_HEADER_SIZE)) {
continue;
}
mtr->reservedSpace = gcr_mfm_reserved_space(gtr->track_length, MFM_TRACK_HEADER_SIZE);
mtr->offsetInFile = (int)gcr - (int)gcr_image->gcr_data;
mtr->offsetInFile += MFM_TRACK_HEADER_SIZE;

Expand Down Expand Up @@ -615,7 +613,16 @@ void C1541 :: mount_g64(bool protect, File *file)
return;
}
printf("Loading...");
gcr_image->load(file);
if (!gcr_image->load(file)) {
// load() has already invalidated the image, so its track table holds
// nothing that may be programmed. remove_disk() ran above, so an empty
// drive is the state the machine is already in.
printf("Failed. Not a G64 image, or its track table does not fit.\n");
fm->fclose(mount_file);
mount_file = NULL;
drive_reset(0); // do not reset, but restore the freeze
return;
}
printf("Inserting...");
insert_disk(protect, gcr_image);
printf("Done\n");
Expand Down
36 changes: 23 additions & 13 deletions software/drive/disk_image.cc
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ extern "C" {
#include "user_file_interaction.h"
#include "blockdev_file.h"
#include "endianness.h"
#include "gcr_track_bounds.h"

#define HARDWARE_ENCODING 1

Expand Down Expand Up @@ -709,23 +710,32 @@ bool GcrImage :: load(File *f)
// track offsets start at 0x000c
for(int i=0;i<max_tracks;i++) {
offset = le_to_cpu_32(pul[i+3]);
if(offset > GCRIMAGE_MAXSIZE) {
if(offset >= GCRIMAGE_MAXSIZE) {
printf("Error. Track pointer outside GCR memory range.\n");
return false;
}
// Offset zero means the track is absent. A non-zero one still has to
// point at two bytes the file actually delivered, since reading the
// length word is itself an access into the image buffer.
if(!gcr_track_header_is_readable(offset, bytes_read)) {
continue;
}
tr = gcr_data + offset;
if(offset) {
w = tr[0] | (uint16_t(tr[1]) << 8);
tracks[i].track_address = tr + 2;
tracks[i].track_length = (int)(w & 0x3FFF);
tracks[i].track_used = true;
tracks[i].in_image_file = true;
tracks[i].track_is_mfm = (w & 0x8000);
// printf("Set track %d.%d to 0x%6x / 0x%4x.\n", (i>>1)+1, (i&1)?5:0, tracks[i].track_address, w);
if (i >= GCRIMAGE_FIRSTTRACKSIDE1) {
double_sided = true;
}
}
w = tr[0] | (uint16_t(tr[1]) << 8);
int length = gcr_validated_track_length(w, offset, bytes_read, GCRIMAGE_MAXTRACKLEN);
Comment thread
chrisgleissner marked this conversation as resolved.
if(!length) {
printf("Track %d: declared length %d was not delivered by the file. Track skipped.\n",
i, (int)(w & 0x7FFF));
continue; // invalidate() left this entry unused; leave it that way
}
tracks[i].track_address = tr + 2;
tracks[i].track_length = length;
tracks[i].track_used = true;
tracks[i].in_image_file = true;
tracks[i].track_is_mfm = (w & 0x8000);
if (i >= GCRIMAGE_FIRSTTRACKSIDE1) {
double_sided = true;
}
}
// Track speed zone info starts after the track offsets
uint8_t reported_tracks = gcr_data[9];
Expand Down
10 changes: 9 additions & 1 deletion software/drive/disk_image.h
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
#include "menu.h"
#include "filemanager.h"
#include "subsys.h"
#include "mfmdisk.h" // WD_MAX_SECTORS_PER_TRACK, used by MFM_TRACK_HEADER_SIZE below

#define GCR_DECODER_GCR_IN (*(volatile uint8_t *)(GCR_CODER_BASE + 0x00))
#define GCR_DECODER_BIN_OUT0 (*(volatile uint8_t *)(GCR_CODER_BASE + 0x00))
Expand Down Expand Up @@ -48,7 +49,14 @@
#define C1541_MIN_D71_SIZE (2*C1541_MAX_D64_35_NO_ERRORS)
#define C1541_D71_SIZE_WITH_ERRORS (C1541_MIN_D71_SIZE + 1366)

#define MFM_TRACK_HEADER_SIZE 2 + (WD_MAX_SECTORS_PER_TRACK * 5)
/* Parenthesised because it is subtracted: without the outer brackets
* "x - MFM_TRACK_HEADER_SIZE" expanded to "x - 2 + 160", which is x + 158
* rather than x - 162, and map_gcr_image_to_mfm() reserved 320 bytes more
* than the track had. The static_assert below fails if they are lost again;
* comparing the macro against 162 would not, since 2 + 160 equals 162. */
#define MFM_TRACK_HEADER_SIZE (2 + (WD_MAX_SECTORS_PER_TRACK * 5))
static_assert(0 - MFM_TRACK_HEADER_SIZE == -162,
"MFM_TRACK_HEADER_SIZE must survive being subtracted");

class BinImage;

Expand Down
145 changes: 145 additions & 0 deletions software/drive/gcr_track_bounds.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
/*
* gcr_track_bounds.h -- what a G64 says about its tracks, checked.
*
* The track lengths in a G64 file are not decoration: insert_disk() programs
* them into the drive engine's parameter RAM, and floppy_mem.vhd both reads
* and writes memory bounded by them. So a length taken from the file decides
* how far the hardware reaches into a buffer, and a track that is not in the
* image at all still has to be given a parameter pair the engine can survive.
*
* These are free functions over plain integers, deliberately: none of the
* drive headers can be compiled on a build host, and this is the part that is
* worth testing there. See software/drive/tests/.
*/
#ifndef DRIVE_GCR_TRACK_BOUNDS_H
#define DRIVE_GCR_TRACK_BOUNDS_H

#include <stdint.h>

/* The low half of a parameter word is the last valid offset in the track, so
* this is a 257-byte empty track. init() and remove_disk() already program it
* for a drive with nothing in it. */
#define GCR_EMPTY_TRACK_PARAM 0x100

/* A track table entry points at a two-byte length word followed by the track
* data. Reading that word is itself an access into the image buffer, so it has
* to lie inside the part of the buffer the file actually filled. Offset zero
* means the track is absent. */
static inline bool gcr_track_header_is_readable(uint32_t offset, uint32_t bytes_read)
{
if (offset == 0 || offset > bytes_read) {
return false;
}
return (bytes_read - offset) >= 2;
}

/* Returns the track length to use, or 0 when the track cannot be used and has
* to be left marked absent.
*
* declared the 16-bit word from the image, flag bits included
* offset byte offset of that word within the buffer
* bytes_read how much of the buffer the file actually filled
* max_length longest track the format allows
*
* The bound is what was read, not how large the buffer is. A truncated image
* can declare a track whose header is present but whose data never arrived,
* and the buffer behind it holds whatever the previous mount left there. So
* the whole track, header word included, has to lie inside what was read:
* offset + 2 + length <= bytes_read, written as subtractions so that no sum
* can wrap.
*
* The length is taken from fifteen bits, not fourteen. Bit 15 is the MFM
* marker this firmware writes and Denise reads; nothing gives bit 14 a
* meaning, so it belongs to the length. write_track() and save() already
* write the length unmasked and set bit 15 on top of it, so reading fourteen
* bits meant the image could not be read back as it was written: a declared
* 0x4123 came out as 0x0123, small enough to pass every check below and be
* programmed as a 291 byte track.
*
* The width is not arbitrary -- the drive's own parameter field is fourteen
* bits -- but the file and the engine are two different things, and masking
* at parse time conflates them. The drive engine's own ceiling is 16383:
* max_offset in floppy_param_mem.vhd is fourteen bits wide, and
* floppy_mem.vhd wraps offset_count at it for reads and writes alike. That
* ceiling never comes into play here, because max_length -- 7928 for a G64,
* GCRIMAGE_MAXTRACKLEN -- is less than half of it and is checked below. So
* the parse stays faithful to the file and the bound does the rejecting.
*/
static inline int gcr_validated_track_length(uint16_t declared, uint32_t offset,
uint32_t bytes_read, int max_length)
{
int length = (int)(declared & 0x7FFF);

if (length <= 0 || length > max_length) {
return 0; /* zero would also divide by zero in insert_disk() */
}
if (offset > bytes_read || (bytes_read - offset) < 2) {
return 0;
}
if ((uint32_t)length > (bytes_read - offset - 2)) {
return 0;
}
return length;
}

/* An MFM track carries a metadata area at its front: a sector count, a version
* byte, and five bytes for each of up to WD_MAX_SECTORS_PER_TRACK sectors.
* map_gcr_image_to_mfm() reads that area whole for a track marked MFM, and
* reserves what lies behind it for sector data. Neither exists unless the
* track is at least as long as the area itself.
*
* Length and MFM marker share one word in a G64, so 0x8001 -- a one-byte track
* marked MFM -- is a legal thing to put in an image. The declared extent then
* checks out: one byte was delivered. The mapping used to believe the marker
* anyway, read the full metadata area from that one byte, and take the space
* behind it as a subtraction in an unsigned field, where it cannot come out
* negative.
*
* track_length the validated length of the track
* header_size bytes of metadata at the front of an MFM track
*/
static inline bool gcr_track_can_hold_mfm_header(int track_length, int header_size)
{
if (header_size <= 0 || track_length <= 0) {
return false;
}
return track_length >= header_size;
}

/* Bytes behind the metadata area, or 0 for a track with no room for one. The
* caller keeps this in an unsigned field that gates every track write, so it
* must never be reached by underflow. */
static inline uint32_t gcr_mfm_reserved_space(int track_length, int header_size)
{
if (!gcr_track_can_hold_mfm_header(track_length, header_size)) {
return 0;
}
return (uint32_t)(track_length - header_size);
}

/* Builds one entry of the drive's track parameter RAM: the address the engine
* works at, and a word carrying the last valid offset in the low half and the
* bit time in the high half.
*
* A track that is not in the image gets the dummy track together with the same
* short, safe length init() and remove_disk() use -- not whatever length the
* previous track happened to leave in a local.
*/
static inline void gcr_track_parameters(uint32_t track_address, int track_length,
uint32_t dummy_address, int dummy_length,
uint32_t rotation_speed,
uint32_t *out_address, uint32_t *out_param)
{
if (track_address != 0 && track_length > 0) {
uint32_t bit_time = rotation_speed / (uint32_t)track_length;
*out_address = track_address;
*out_param = (uint32_t)(track_length - 1) | (bit_time << 16);
return;
}

uint32_t bit_time = (dummy_length > 0) ? (rotation_speed / (uint32_t)dummy_length) : 0;
*out_address = dummy_address;
*out_param = GCR_EMPTY_TRACK_PARAM | (bit_time << 16);
}

#endif /* DRIVE_GCR_TRACK_BOUNDS_H */
15 changes: 15 additions & 0 deletions software/drive/tests/Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
CXX ?= g++
CXXFLAGS := -std=c++14 -g -Wall -Wextra -I. -I.. -I../../io/usb/tests
OUTPUT_DIR := output
GCR_TRACK_BOUNDS_TEST := $(OUTPUT_DIR)/gcrTrackBoundsTest

.PHONY: all gcr-track-bounds clean

all: gcr-track-bounds

gcr-track-bounds:
mkdir -p $(OUTPUT_DIR)
$(CXX) $(CXXFLAGS) gcr_track_bounds_test.cpp ../../io/usb/tests/host_test_main.cpp -lpthread -o $(GCR_TRACK_BOUNDS_TEST) && $(GCR_TRACK_BOUNDS_TEST)

clean:
rm -rf $(OUTPUT_DIR)
Loading