Skip to content

2026

I miei computer: cronologia hardware e audio (1988–2012)

Ho sistemato dei vecchi appunti, ed ho raccolto la successione delle macchine che mi hanno accompagnato nel tempo, tra floppy disk, megahertz ed audio digitale, dalle prime Sound Blaster fino alle interfacce da studio.

1988 — IBM PS/2 Modello 30

  • CPU: Intel 8086 (8 MHz)
  • RAM: 640 KB
  • Disco: Hard Disk 20 MB
  • Supporti: Floppy drive 3.5" da 720 KB (DD)
  • Grafica: MCGA (Multi-Color Graphics Array: il precursore del VGA, capace di 320x200 a 256 colori)
  • Monitor: IBM monocromatico da 12" (B/N)
  • Audio: PC Speaker interno (cicalino a 1 bit)
  • Software: MS-DOS 3.30, Windows 2.11 e Word 1.0

1992 — Il clone AMD 386/40

  • CPU: AMD 386DX 40 MHz
  • RAM: 4 MB
  • Disco: HD 120 MB
  • Supporti: Floppy 1.44 MB
  • Grafica: OAK VGA
  • Software: DOS 5.0, Windows 3.1
  • Audio: DAC R-2R artigianale su porta parallela (il leggendario "Covox" autocostruito con le resistenze: 8 bit mono per far suonare i MOD a 4 canali con ModPlay, ModEdit + ModRes ed infine con FastTracker II)

1996 — L'era Pentium e l'audio a 16-bit

  • CPU: Intel Pentium 150 MHz
  • RAM: 32 MB, portata a 64 MB a fine 1998 (upgrade da Jolly Computer, per gestire i banchi SoundFont con Vienna e le tracce audio su Cakewalk Pro Audio senza dropout)
  • Ottica: CD-ROM Asus 40x (aprile 1999, da Silicon Valley a Padova)
  • OS: Windows 95 OSR2
  • Audio: Creative Sound Blaster AWE64
  • Software Audio & MIDI:
  • Sequencing & DAW: Cakewalk Pro Audio e il bundle Creative (Voyetra MIDI Orchestrator Plus)
  • SoundFont: Creative Vienna SoundFont Studio
  • Tracker: FastTracker II in ambiente DOS

2000 — Il cambio di millennio con AMD

  • Data acquisto: 13 dicembre 2000
  • CPU: AMD Athlon "Thunderbird" 800 MHz (Socket A)
  • RAM: 128 MB SDRAM PC133 (o 256 MB, da verificare)
  • Monitor: CRT 17"
  • Audio: Creative Sound Blaster Live! (chip EMU10K1, l'era dell'EAX e del riverbero ambientale hardware)
  • OS: Windows 98 SE / Windows 2000
  • Note: Insieme al PC arrivò la scatola di Fuga da Monkey Island (Monkey Island 4), fresco di uscita a novembre 2000: il primo capitolo in 3D, che sul vecchio Pentium 150 sarebbe stato impensabile far girare!

2004 — Il salto ai 64 bit

  • Data acquisto: 12 novembre 2004
  • CPU: AMD Athlon 64 3200+ (Socket 754, core ClawHammer)
  • RAM: 1 GB
  • Monitor: CRT 17" riciclato, successivamente un LCD
  • Disco: HD 160 GB
  • Audio: Creative Sound Blaster Audigy2 Platinum (con il pannello frontale da 5.25" pieno di ingressi jack, ottici e MIDI)

2012 — Dell XPS 8300 e l'home studio USB

  • CPU: Intel Core i7-2600 (Sandy Bridge, 3.40 GHz, 8 MB cache)
  • RAM: 8 GB DDR3 (4x2 GB) a 1333 MHz
  • Disco: HD 1 TB SATA 7200 rpm, successivamente SSD512GB
  • Monitor: LCD 17" riciclato, successivamente LCD 27"
  • Grafica: NVIDIA GeForce GT 530 (1 GB)
  • OS: Windows 7 Home Premium SP1 (64 bit)
  • Audio: Roland Quad-Capture (il salto di qualità: stop alle schede audio PCI interne, ma senza gestione SF2 nativa)

MODRES: reverse engineering di un TSR audio negli anni '90

MODRES era la libreria audio di MODEDIT, un tracker per MOD molto diffuso in ambiente DOS. Non aveva documentazione pubblica, l'ho quindi ricostruita a suo tempo con il Turbo Debugger ed inviata a Ralf Brown, ed oggi nel 2026 tali voci sono ancora presenti nella Interrupt List.

Cos'è MODRES

MODRES era un programma per DOS di tipo TSR (Terminate and Stay Resident) che permetteva di riprodurre in background i moduli musicali in formato MOD quattro tracce dei tracker Amiga.

Il suo uso principale era all'interno di MODEDIT: MODRES era la libreria audio che permetteva al tracker di ascoltare i moduli ed i campioni mentre li si editava, e di continuare a suonarli anche quando il tracker era in background.

Supportava:

  • PC speaker
  • D/A converter su porte parallele LPT1-LPT4
  • Sound Blaster (porta 02x0h)
  • Disney Sound Source
  • Configurazioni stereo su due porte parallele
  • Stereo-on-1

Esponeva le sue funzioni attraverso l'interrupt INT 2F, con AX=8220h come base.

Il reverse engineering

MODRES non aveva documentazione pubblica. Tramite il Turbo Debugger della Borland ne ho seguito l'esecuzione istruzione per istruzione, mettendo breakpoint ed esaminando registri e memoria. Con molta pazienza, ho ricostruito le sue logiche interne di funzionamento.

Le funzioni che avevo documentato sono sette. Le strutture dati due: MODPARM e SAMPARM, più una tabella di output device e una dei pitch.

La documentazione

MODRES - PLAY MODULE

AX = 8220h
DX:CX -> MODPARM structure (see #2646)

Return:
AX = status
  5722h successful
  2000h parameters out of range
  other MODRES not installed

See Also: AX=8221h - AX=8223h - AX=8225h - AX=8227h - AX=8200h"RESPLAY"

Format of MODPARM Structure (Table 2646)

Offset  Size    Description
00h     WORD    signature 504Dh ("MP" = Modparm)
02h     BYTE    output device (see #2648 at INT 2F/AX=8221h)
03h     WORD    segment of start of main module (pattern) data
05h  31 WORDs   segment of start of sample numbers 1-31
43h     BYTE    pattern at which to start playing (00h to 7Fh)
44h     BYTE    function
                00h play from pattern [offset 43h] until end of the song
                01h play indicated pattern [offset 43h] only
45h     BYTE    Machine speed
                00h 10-12Mhz
                01h 12-25Mhz (default)
                02h 25Mhz+
                03h mix speed 10kHz (fast 8Mhz machines)
                04h mix speed 12kHz (10Mhz machines)
                05h mix speed 13kHz
                06h mix speed 8kHz (test for 8Mhz machines)
46h     BYTE    allow >64k sample playing
                80h MOD has samples >64k in it
                else all samples in MOD are <64k

Notes: Main module data and all samples must start on segment
boundaries. In version 2.00 (ONLY) this function carries on
playing (works in the background).

See Also: #2647

MODRES - INSTALLATION CHECK

AX = 8221h

Return:
AX = status
  5722h successful
  other MODRES not installed
BX = BCD version number (BH = major, BL = minor)
DX:CX -> Output Device structure (read-only) (see #2647)

See Also: AX=8220h - AX=8222h - AX=8225h - AX=8227h

Format of Output Device structure [array] (Table 2647)

Offset  Size    Description
00h 20 BYTEs   ASCIZ name of the output device
               (end of list if first char is FFh)
14h    WORD    apparently always FFFFh
16h    WORD    0000h if output device not available
               else first I/O port for the output device
18h    WORD    second I/O port for the output device (for example
               if it is stereo)
               0000h if only one port used or device is not available
1Ah  7 BYTEs   ???

See Also: #2646 - #2648

Values for MODRES v1.52 output device index (Table 2648)

00h    PC speaker
01h    D/A Converter on LPT1
02h    D/A Converter on LPT2
03h    D/A Converter on LPT3
04h    D/A Converter on LPT4
05h    D/A Converter on LPT1&LPT2 (stereo)
06h    D/A Converter on LPT1&LPT2 (mono)
07h    Sound Blaster (port 02x0h)
08h    User Defined D/A (mono)
09h    User Defined D/A (stereo)
0Ah    Stereo-on-1
0Bh    Disney SS su LPT1
0Ch    Disney SS su LPT2
0Dh    Disney SS su LPT3
0Eh    Disney SS su LPT4

Note: This list may vary between versions of MODRES

MODRES - UNINSTALL

AX = 8222h

Return:
AX = code segment of the program

Note: This function does not release the TSRs memory; the caller
must do so

See Also: AX=8220h - AX=8221h - AX=8223h

MODRES - PLAY SAMPLE

AX = 8223h
DX:CX -> SAMPARM structure (see #2649)

Return:
AX = status
  5722h successful
  2000h parameters out of range
  other MODRES not installed

See Also: AX=8221h - AX=8224h - AX=8225h - AX=8226h

Format of SAMPARM Structure (Table 2649)

Offset  Size    Description
00h     WORD    signature 5053h ("SP" = SAMPARM)
02h     WORD    segment of start of sample to play
04h     WORD    length of sample (IN WORD)
06h     BYTE    output device (see #2648 at INT 2F/AX=8221h)
07h     WORD    pitch to play (see #2650)
09h     BYTE    volume (from 00h to 40h)
0Ah     WORD    loop start
0Ch     WORD    loop length
0Eh     BYTE    machine speed (see INT 2F/AX=8220h)

See Also: #2646

Values for Pitch to play (Table 2650)

C 0 is 06B0h
C#0 is 06B0h / 2^(1/12)
D 0 is (06B0h / 2^(1/12)) / 2^(1/12)
...

Note: C 1 is 06B0h / 2. C 2 is 06B0h / 4. Etc.

See Also: #2649

MODRES - ???

AX = 8224h
DX:CX -> ???

Return:
???

See Also: AX=8221h - AX=8223h - AX=8224h

MODRES v2.00+ - GET LOCATION IN MOD

AX = 8225h

Return:
AL = status
  00h playing
  01h reached end or stopped
AH = speed of MOD
BX = position within pattern 0000h-0400h
CL = position within the song (track number)

See Also: AX=8220h - AX=8221h - AX=8223h - AX=8226h

MODRES v2.00+ - STOP PLAYING

AX = 8226h

Return:
AX = status
  5722h successful
  other MODRES not installed

Desc: Stops playing the MOD file before performing critical
operations such as disk accesses

See Also: AX=8220h - AX=8221h - AX=8223h - AX=8225h - AX=8227h

MODRES - CONFIGURE

AX = 8227h
BX = function
  0001h set default playing speed (06h)
  0002h select output device
    CL = output device (see #2648 at INT 2F/AX=8221h)

Return:
AX = status
  5722h successful
  2000h parameters out of range
  other MODRES not installed

Note: Function 0001h should be called every time a new module
is loaded

See Also: AX=8220h - AX=8221h - AX=8222h - AX=8223h

Note sulla documentazione

Alcune cose che vale la pena notare, rileggendola oggi:

  • La MODPARM inizia con una signature (504Dh = "MP"), come quasi tutte le strutture dati di quei tempi. Serviva a verificare che il puntatore passato alla funzione fosse davvero una struttura valida.
  • Il campo machine speed non è un valore in MHz, ma un indice che il TSR usa per scegliere la frequenza di mixaggio. La voce 06h è "test for 8MHz machines": il programma si adattava alla macchina su cui girava.
  • La tabella dei pitch parte da C 0 = 06B0h e calcola le note successive come divisioni per 2^(1/12). È il temperamento equabile applicato ai registri del timer. Quasi certamente però il programma utilizzava una tabella precompilata per evitare di calcolare i valori a runtime, che però non sono riuscito a recuperare.
  • Non sono riuscito a capire il significato della funzione AX=8224h che è rimasta non documentata.

Dove è finita

La documentazione è nella Ralf Brown's Interrupt List, alla voce INT 2F/AX=8220h e seguenti. È ancora consultabile su ctyme.com/rbrown.htm.

Il riconoscimento

Nella sezione CREDITS della Release 55 (28 settembre 1997) della Ralf Brown's Interrupt List, c'è questa riga:

12/96 A Federico Thiella fthiella@stud32.math.unipd.it  MODRES

Dicembre 1996. Non ho più quella casella di posta ovviamente!

filtersql v1.2.6: il bug banale che ha rivelato un abisso architetturale

Quando ho pubblicato la versione 1.2.6 di filtersql ero convinto di aver raggiunto un buon livello di sicurezza. Poi, puntuale come un ordigno svizzero, è saltato fuori un nuovo bug.

Era la classica svista minuscola, la riga di codice che guardi e pensi di risolvere in pochi minuti.

Ma quando ho messo le mani nella correzione mi sono reso conto che la patch banale risolveva in effetti il bug, ma non il problema che si è rivelato essere più grande.

Il bug non era nella logica: era architetturale.


Premessa 1: La giungla dei nomi di colonna nei database reali

Vero che dipende dal DBMS, dalla versione e dalla configurazione, ma in linea generale i database consentono di definire i nomi delle colonne con grande ed eroica anarchia. Una colonna può contenere spazi, accenti, caratteri speciali e simboli Unicode. Si può addirittura quotare il carattere di quoting.

Ed in produzione in effetti non c'è limite all'ineleganza: da colonne come "qtà totale" o "n° ordine" che pur non essendo una idea grandiosa sono tutto sommato legittime, fino a casi limite ma inaspettatamente validi come "totale " (con due spazi in coda, o magari con tab, newlines, ecc.).

Per garantire una quotazione avanzata e sicura senza impazzire, ho scelto di applicare una semplificazione: rimuovere eventuali spazi bianchi accidentali agli estremi (trimming). La colonna "totale " veniva ripulita in "totale". Questo avrebbe ridotto la copertura dei casi più assurdi, ma i casi più dignitosi sarebbero rimasti salvi.


Premessa 2: L'incubo dei conflitti di scope (Multi-Tenancy)

filtersql gestisce il concetto di scope: una mappa di condizioni fisse (come tenant_id = 42) applicata lato server a tutte le operazioni (select, insert, update, delete). Non sostituisce davvero i permessi del database, ma offre comunque una sua utilità nel blindare le applicazioni web multi-tenant.

Ma cosa succede se un client invia un payload che prova a modificare proprio la colonna riservata allo scope?

A parte la rottura di un equilibrio estetico, nelle letture e nelle cancellazioni non si pone davvero un problema, dato che le condizioni finiscono tutte in AND e al massimo non esce e non si cancella nulla, ma in scrittura la faccenda si fa interessante:

Caso insert:

insert into users (tenant_id, tenant_id, name) values (400, 500, 'Mario');
A seconda del DBMS, questa istruzione può assegnare il primo valore (400), prendere l'ultimo (500), comportarsi in modo imprevedibile o fallire miseramente.

Caso update (molto peggio):

update users set tenant_id = 500 where tenant_id = 400;
Questa singola riga di codice trasferisce uno o più record dal proprio tenant a un altro. Ed a parte perdere i propri dati che spariscono dal controllo, un utente malevolo potrebbe sfruttare questo comportamento per spostare un account admin dal proprio tenant al tenant di un nemico per prenderne il controllo.

La soluzione logica: il problema sembrava risolvibile con banale controllo di collisione che ho implementato facilmente.

Se una colonna appartiene allo scope, deve essere vietata nei campi di insert o update.

Facile come bere una bottiglia d'acqua.


Il Bug della v1.2.6

Rileggi le due premesse, inserisci un minuscolo errore logico, ed ecco che l'ingranaggio salta.

Se lo scope protegge la colonna "tenant_id" e un client invia in scrittura la chiave "tenant_id " (con uno spazio in fondo), cosa succede?

  1. purtroppo il controllo dei conflitti confrontava immediatamente le due stringhe grezze: "tenant_id" contro "tenant_id ";
  2. le stringhe risultavano diverse ed il controllo dava il via libera;
  3. solo successivamente, la funzione di quoting sanificava l'identificatore applicando il trim().
  4. risultato: la query veniva compilata ed eseguita modificando proprio tenant_id!

Risolvere questo specifico passaggio non era poi complicato, bastava anticipare il trim() al momento giusto, prima del controllo di collisione. Il bug sarebbe stato risolto senza tanta fatica. Ma è stato lì che mi si è aperto l'abisso.


Il problema architetturale: Uguaglianza Stringa vs Equivalenza Semantica

Due stringhe identiche sono per forza uguali, il che non è particolarmente sorprendente.

Ma due stringhe diverse possono avere lo stesso identico significato logico per il database.

Cosa succede se il server imposta scope: {"tenant_id": 400} e il client invia un payload con values: {"users.tenant_id": 500}?

Un controllo basato su stringhe non rileva alcun conflitto ("tenant_id" != "users.tenant_id"). Il sistema genera allegramente questo codice SQL:

update users
set
  users.tenant_id = 500
where
  tenant_id = 400;

Mentre diversi motori, quali PostgreSQL o SQLite, trovavano questa sintassi indigesta e la rifiutavano giustamente con un errore, MySQL la accettava con grande entusiasmo, eseguendo lo spostamento temuto.

Ma non solo!

Cosa succede se le colonne sono case-insensitive? Cosa succede se a livello di database è definito qualche alias? Cosa sarebbe successo con le decomposizioni Unicode (NFC vs NFD), dove la lettera à può essere rappresentata da un singolo codice o da due codici separati (a + accento)?

Le stringhe in byte erano diverse, ma per il database puntavano alla stessa identica colonna.

Riconoscere tutte le possibili varianti semantiche che un utente o un database potevano inventarsi per identificare la stessa colonna era una corsa alle armi persa in partenza. Chi sa davvero se due nomi sono equivalenti è il motore del database; ma filtersql genera solo stringhe e non è, per filosofia, collegato al database.


La Svolta: L'Asimmetria Consapevole

Per uscirne vittorioso ho capito che non dovevo rendere più "intelligente" il confronto tra stringhe, ma dovevo cambiare il contratto all'ingresso.

Ho introdotto un'architettura asimmetrica:

  1. in lettura (select, where): Il sistema rimane flessibile. Può accettare identificatori qualificati (users.tenant_id) o percorsi JSONB (data->>key), perché quotati e sanificati separatamente.
  2. in scrittura (insert, update, delete): nuova regola: tutte le chiavi in values o id devono superare la validazione _validate_bare_identifier.

Un identificatore di scrittura valido dev'essere un bare identifier puro:

  • nessuno spazio iniziale o finale (il trim() non pulisce più il dato: se c'è uno spazio, la query viene rifiutata con InvalidIdentifierError).
  • nessun punto (vietata la qualificazione di tabella come users.tenant_id).
  • nessuna sintassi JSONB o carattere di controllo.
  • forma canonica Unicode NFC obbligatoria.

Normalizzando sia lo scope che i campi di scrittura in NFC e applicando casefold(), l'area di confronto per le collisioni si è ridotta a uno spazio unidimensionale in cui nessuna ambiguità semantica può più nascondersi.

Nota: nella 1.2.8 ho poi esteso la stessa regola anche a _quote(), così che l'incoerenza non possa ripresentarsi in nessun altro punto del codice.


E la Allowed_Columns?

Ci pensavo da tempo, ma avrei dovuto implementarla prima.

Il collision check risolve il caso in cui il client prova a scrivere su una colonna di scope. Ma non risolve il caso in cui il client prova a scrivere o leggere su colonne pericolose. Per quello serve una whitelist.

In un'applicazione reale (e a maggior ragione in una pipeline con gli LLM), lasciare che un client o un'IA interroghino o filtrino su qualsiasi colonna esista a DB senza filtro è un rischio enorme: rischi di esporre password_hash, credit_card_token o filtrare su stipendio_netto.

Il prossimo passo sarà quindi introdurre la possibilità di limitare le colonne coinvolte a quanto previsto nella allowed_columns.

In questo modo i ruoli rimangono ben distinti e puliti:

  • il developer definisce a mano la whitelist applicativa dei campi esposti per quello specifico endpoint o utente.

  • filtersql fa il lavoro sporco da compilatore: sanifica l'input, quota gli identificatori, applica lo scope multi-tenant senza possibilità di bypass e difende il database dai DoS.

Tutto questo sarà pubblicato in una delle prossime versioni.

Tanti saluti ed al prossimo bug!

filtersql.org — github.com/fthiella/filtersql