all repos — xyw @ 97b4e0ec67ae15319263f8c3633594369c0e228e

A minimal virtual machine and assembler for terminals.

Added memory checks and errors.
h3rald h3rald@h3rald.com
Tue, 28 Jul 2026 11:04:24 +0200
commit

97b4e0ec67ae15319263f8c3633594369c0e228e

parent

6d18bea4e92d336116011e843efdc3d2271a4778

7 files changed, 180 insertions(+), 23 deletions(-)

jump to
M .conver/history.txt.conver/history.txt

@@ -1,1 +1,2 @@

2026-07-27: +280D # Initial release. +2026-07-28: +3007 # Improved error management and debugging.
M .conver/release.txt.conver/release.txt

@@ -1,1 +1,1 @@

-280D+3007
M CHANGELOG.mdCHANGELOG.md

@@ -1,5 +1,17 @@

## Changelog +### v300-7 — 2026-07-28 + +#### Breaking Changes + +- Added more error codes and changed existing error code values. +- Debug output now written to stderr + +#### New Features + +- Enhanced debug messages +- Added more checks to avoid memory overflows. + ### v280-D — 2026-07-27 Initial release, featuring:
M devices/file.cdevices/file.c

@@ -43,6 +43,8 @@ #define FILE_ERROR_NOT_FOUND 0x31

#define FILE_ERROR_ACCESS_DENIED 0x32 #define FILE_ERROR_INVALID_OP 0x33 #define FILE_ERROR_IO_ERROR 0x34 +#define FILE_ERROR_PATH_TOO_LONG 0x35 +#define FILE_ERROR_BUFFER_OUT_OF_RANGE 0x36 // Maximum path length #define FILE_MAX_PATH 256

@@ -53,8 +55,8 @@ static char file_current_path[FILE_MAX_PATH] = {0};

static long file_cursor = 0; static int file_is_write_mode = 0; -// Get null-terminated string from memory at given address -static void get_path_string(xyw_word addr, char *buf, size_t bufsize) +// Get null-terminated string from memory at given address. +static int get_path_string(xyw_word addr, char *buf, size_t bufsize) { size_t i; for (i = 0; i < bufsize - 1 && xyw_memory[addr + i] != 0; i++)

@@ -62,6 +64,13 @@ {

buf[i] = xyw_memory[addr + i]; } buf[i] = '\0'; + return (xyw_memory[addr + i] != 0) ? -1 : 0; +} + +// Returns 1 if [addr, addr+length) fits entirely within xyw_memory, 0 otherwise. +static int file_buffer_in_range(xyw_word addr, xyw_word length) +{ + return ((unsigned int)addr + (unsigned int)length) <= XYW_MEMORY_SIZE; } // Close any open file

@@ -78,7 +87,7 @@ file_is_write_mode = 0;

} // Update file type and size based on path -static void update_file_stats(xyw_byte *data) +static void update_file_stats(xyw_byte *data, xyw_byte *error) { xyw_word path_addr = XYW_PEEKW(&data[FILE_PATH]); char path[FILE_MAX_PATH];

@@ -91,7 +100,13 @@ XYW_POKEW(&data[FILE_SIZE], 0);

return; } - get_path_string(path_addr, path, FILE_MAX_PATH); + if (get_path_string(path_addr, path, FILE_MAX_PATH) != 0) + { + *error = FILE_ERROR_PATH_TOO_LONG; + data[FILE_TYPE] = XYW_FILE_TYPE_UNKNOWN; + XYW_POKEW(&data[FILE_SIZE], 0); + return; + } if (stat(path, &st) != 0) {

@@ -137,7 +152,18 @@ *error = FILE_ERROR_INVALID_OP;

return; } - get_path_string(path_addr, path, FILE_MAX_PATH); + if (!file_buffer_in_range(buffer_addr, length)) + { + XYW_DBG(" Buffer out of range for file_read: addr=%04X length=%04X\n", buffer_addr, length); + *error = FILE_ERROR_BUFFER_OUT_OF_RANGE; + return; + } + + if (get_path_string(path_addr, path, FILE_MAX_PATH) != 0) + { + *error = FILE_ERROR_PATH_TOO_LONG; + return; + } XYW_DBG(" Reading file: %s, length=%u, cursor=%ld\n", path, length, file_cursor);

@@ -340,7 +366,18 @@ *error = FILE_ERROR_INVALID_OP;

return; } - get_path_string(path_addr, path, FILE_MAX_PATH); + if (!file_buffer_in_range(buffer_addr, length)) + { + XYW_DBG(" Buffer out of range for file_write: addr=%04X length=%04X\n", buffer_addr, length); + *error = FILE_ERROR_BUFFER_OUT_OF_RANGE; + return; + } + + if (get_path_string(path_addr, path, FILE_MAX_PATH) != 0) + { + *error = FILE_ERROR_PATH_TOO_LONG; + return; + } XYW_DBG(" Writing to file: %s (append=%d), length=%u, cursor=%ld\n", path, append, length, file_cursor);

@@ -441,7 +478,11 @@ *error = FILE_ERROR_INVALID_OP;

return; } - get_path_string(path_addr, path, FILE_MAX_PATH); + if (get_path_string(path_addr, path, FILE_MAX_PATH) != 0) + { + *error = FILE_ERROR_PATH_TOO_LONG; + return; + } // Close file if it's the one being deleted if (file_handle && strcmp(file_current_path, path) == 0)

@@ -462,7 +503,7 @@ // Success = 1 for successful delete

XYW_POKEW(&data[FILE_SUCCESS], 1); // Update stats after delete - update_file_stats(data); + update_file_stats(data, error); } // Parse "source|destination" from path string

@@ -474,7 +515,10 @@

if (path_addr == 0) return -1; - get_path_string(path_addr, combined, sizeof(combined)); + if (get_path_string(path_addr, combined, sizeof(combined)) != 0) + { + return -1; + } char *sep = strchr(combined, '|'); if (!sep)

@@ -601,7 +645,7 @@ once for each byte), close current file, update stats, reset cursor */

if (addr == FILE_PATH + 1) { file_close(); - update_file_stats(data); + update_file_stats(data, error); } // When operation is written, execute it
M xyw.hxyw.h

@@ -29,7 +29,8 @@ #define XYW_SYSTEM_ERROR_SYSTEM_STACK_OVERFLOW 0x05

#define XYW_SYSTEM_ERROR_EXEC_DEPTH_EXCEEDED 0x06 #define XYW_SYSTEM_ERROR_IMAGE_NOT_FOUND 0x07 #define XYW_SYSTEM_ERROR_IMAGE_READ_ERROR 0x08 -#define XYW_SYSTEM_ERROR_INVALID_OPCODE 0x09 +#define XYW_SYSTEM_ERROR_IMAGE_TOO_LARGE 0x09 +#define XYW_SYSTEM_ERROR_INVALID_OPCODE 0x0A typedef unsigned char xyw_byte; typedef unsigned short xyw_word;
M xywasm.cxywasm.c

@@ -105,12 +105,15 @@ xyw_byte data[0x10000] = {};

// Current write pointer xyw_word ptr = 0; +static size_t bytes_written = 0; +static int bytecode_overflow = 0; // Flat single-string "dictionary" for symbols (macros and labels) // Removing 256 bytes to leave room for devices. // Actually following Uxn design here ;) char dict[0x8000 - 0x100]; char *dictnext = dict; +static int dict_overflow = 0; // List of symbols (macros and labels) xyw_symbol symbols[MAX_SYMBOLS];

@@ -125,6 +128,17 @@

// Stores a new string in the dictionary and returns its address static char *dict_store(char *str) { + if (dict_overflow) + { + return dict; + } + + size_t len = strlen(str); + if ((size_t)(dictnext - dict) + len + 1 > sizeof(dict)) + { + dict_overflow = 1; + return dict; + } char *o = dictnext; while (*str) {

@@ -262,6 +276,16 @@

// Append raw bytecode data static inline void append(xyw_word val) { + if (bytecode_overflow) + { + return; + } + unsigned int n = (val > 0xff) ? 2 : 1; + if (bytes_written + n > sizeof(data)) + { + bytecode_overflow = 1; + return; + } if (val > 0xff) { XYW_POKEW(&data[ptr], val);

@@ -272,6 +296,44 @@ {

data[ptr] = (xyw_byte)val; } ptr++; + bytes_written += n; +} + +// Writes a single raw byte, with overflow check +static inline void store_byte(xyw_byte val) +{ + if (bytecode_overflow) + { + return; + } + + if (bytes_written + 1 > sizeof(data)) + { + bytecode_overflow = 1; + return; + } + + data[ptr++] = val; + bytes_written++; +} + +// Writes a single raw word, with overflow check +static inline void store_word(xyw_word val) +{ + if (bytecode_overflow) + { + return; + } + + if (bytes_written + 2 > sizeof(data)) + { + bytecode_overflow = 1; + return; + } + + XYW_POKEW(&data[ptr], val); + ptr += 2; + bytes_written += 2; } // Encode a push instruction

@@ -422,13 +484,12 @@ // Use digit count to determine byte vs word, not value

if (token_is_word) { append(PSH | XYW_MODE_W); - XYW_POKEW(&data[ptr], value); - ptr += 2; + store_word(value); } else { append(PSH); - data[ptr++] = (xyw_byte)value; + store_byte((xyw_byte)value); } } return 0;

@@ -561,8 +622,7 @@ {

XYW_DBG("Label Ref:\t%s -> %04X\n", token, sym->address); // Push label address append(PSH | XYW_MODE_W); - XYW_POKEW(&data[ptr], sym->address); - ptr += 2; + store_word(sym->address); } else if (sym->type == SYMBOL_TYPE_REF) {

@@ -581,8 +641,8 @@ if (ref(ctx, token) != 0)

{ return -1; } - append(0x00); - append(0x00); + store_byte(0x00); + store_byte(0x00); } } return 0;

@@ -644,9 +704,9 @@ XYW_DBG("Directive:\t.string %s\n", token);

// Store string in memory (without quotes) for (unsigned int i = 1; i < token_length - 1; i++) { - data[ptr++] = (xyw_byte)token[i]; + store_byte((xyw_byte)token[i]); } - data[ptr++] = 0; + store_byte(0); return validate_single_directive_argument(ctx, r); }

@@ -692,12 +752,14 @@ }

if (token_is_word) { // Word value: emit high byte then low byte - data[ptr++] = (xyw_byte)(token_value >> 8); - data[ptr++] = (xyw_byte)(token_value & 0xFF); + store_byte((xyw_byte)(token_value >> 8)); + store_byte((xyw_byte)(token_value & 0xFF)); } else { data[ptr++] = (xyw_byte)token_value; + store_byte((xyw_byte)token_value); + } // Now break if we were at end of line/file if (r == TOKEN_END_EOL || r == TOKEN_END_EOF || r == TOKEN_END_COMMENT)

@@ -1126,9 +1188,12 @@ ctx.column = 1;

// Reset all assembler state so xyw_assemble can be called multiple times memset(data, 0, sizeof(data)); + bytes_written = 0; + bytecode_overflow = 0; ptr = 0; memset(dict, 0, sizeof(dict)); dictnext = dict; + dict_overflow = 0; memset(symbols, 0, sizeof(symbols)); symcount = 0; memset(refs, 0, sizeof(refs));

@@ -1145,6 +1210,19 @@ if (assemble_file(&ctx) != 0)

{ return -1; } + + if (bytecode_overflow) + { + fprintf(stderr, "Error - Assembled image exceeds %d-byte memory size [%s]\n", XYW_MEMORY_SIZE, input); + return -1; + } + + if (dict_overflow) + { + fprintf(stderr, "Error - Symbol dictionary full [%s]\n", input); + return -1; + } + FILE *fp = fopen(output, "wb"); if (!fp) {
M xywrun.cxywrun.c

@@ -430,6 +430,27 @@ xyw_memory[SYSTEM_ERROR] = XYW_SYSTEM_ERROR_IMAGE_NOT_FOUND;

xyw_memory[SYSTEM_STATE] &= ~XYW_STATE_RUNNING; return -1; } + fseek(fp, 0, SEEK_END); + long file_size = ftell(fp); + fseek(fp, 0, SEEK_SET); + + if (file_size < 0) + { + fprintf(stderr, "Error - Could not determine file size [%s]\n", path); + fclose(fp); + xyw_memory[SYSTEM_ERROR] = XYW_SYSTEM_ERROR_IMAGE_READ_ERROR; + xyw_memory[SYSTEM_STATE] &= ~XYW_STATE_RUNNING; + return -1; + } + + if ((size_t)file_size > XYW_MEMORY_SIZE) + { + fprintf(stderr, "Error - Image exceeds %d-byte memory size [%s]\n", XYW_MEMORY_SIZE, path); + fclose(fp); + xyw_memory[SYSTEM_ERROR] = XYW_SYSTEM_ERROR_IMAGE_TOO_LARGE; + xyw_memory[SYSTEM_STATE] &= ~XYW_STATE_RUNNING; + return -1; + } size_t bytes_read = fread(&xyw_memory[0x000], 1, XYW_MEMORY_SIZE - 0x000, fp); fclose(fp); if (bytes_read == 0)