NOW LET US – AI RAG SaaS Studio TP.HCM
NOW LET US
Digital Product Studio
Back to news
DEV-TOOLS...6 min read

Hunting a 34 year old pointer bug in EtherSlip (DOS Networking)

Share
NOW LET US Article – Hunting a 34 year old pointer bug in EtherSlip (DOS Networking)

A deep dive into discovering a long-standing memory corruption bug in EtherSlip, a DOS packet driver, caused by a NULL pointer assignment and the complexities of x86 segmented memory.

[email protected]

Posted: 2026-04-19

Tags: DOS, Networking, Segmented memory is hard

A few weeks ago I was revisiting my instructions for running a SLIP connection between a DOS PC and Linux. If you are not familiar with SLIP it stands for Serial Line Internet Protocol, and it lets you run TCP/IP over a PC serial port. TCP/IP is much faster over Ethernet, but a serial port can work too.

There are several packet drivers for DOS that let you make SLIP connections. One that I use often is "EtherSLIP" which is handy because it emulates an Ethernet packet driver but it is really just SLIP over a serial port. The emulation allows you to use programs designed for Ethernet packet drivers unmodified; otherwise, you'd have to run programs that are designed specifically for SLIP packet drivers. All of the mTCP programs expect Ethernet, but they don't actually know what is happening "under the covers" so any packet driver that emulates Ethernet works too. (Besides EtherSLIP there is also a Token Ring packet driver that emulates Ethernet.) EtherSlip is included in the Crynwr packet driver collection, which covers most classic ISA Ethernet cards.

I used Telnet to do my testing and there was something wrong with my cabling; it was slow and dropping packets like crazy. (It turned out to be a hardware problem.) When Telnet exited it gave me this error message:

*** NULL assignment detected

Well, that doesn't sound good. The compiler I use (Open Watcom 1.9) checks the heap at the end of a program to let you know if there was heap corruption, but this is a different error message. I dug through the PDF documentation and I found an explanation in the "Watcom C/C++ Programmer's Guide." Here is a summary of the problem:

If you get the warning message then something clobbered those first 32 bytes in the data segment and you probably have a bug. Even if you don't get the warning message you might have a bug, but this trick can't detect that - a write outside of those 32 bytes will not be detected by this mechanism.

Ok, so here is the situation:

The compiler run-time is telling me that I'm writing using a NULL pointer, so all I need to do is add some trace points on the suspected path and write a warning if I see a NULL pointer being used. That is simple to do but somewhat tedious as I might have to add a trace point for every pointer I use. But the suspect code path (resending a lost packet) is not that complicated so I started with this approach. Here is a sample of what I did:

void near TcpSocket::resendPacket( TcpBuffer buf ) {if ( buf == NULL ) { TRACE_WARN(("Whoops: resendPacket tried to reference a NULL pointer.")); return; }TcpPacket_t packetPtr = &buf->headers; ...

I kept running the code and recreating the problem, but I never got my warning message. So I kept adding trace points in my code until I eventually determined that this approach was not working and I would need to try something different.

The compiler can detect the corruption, but it only runs the check when the program exits. To get closer to the problem I can do the same check and do it while the program is running, hopefully narrowing down when and where it happens.

To get started I first looked at the compiler source code to see exactly what it was doing, and I found what I needed in bld/clib/startup/a/cstrt086.asm. (I've slightly simplified and reformatted it here for clarity.)

This is where the 32 bytes of reserved storage are defined. (It is allocated as 16 words of 0x0101.)

assume ds:DGROUP INIT_VAL equ 0101h NUM_VAL equ 16 _NULL segment para public 'BEGDATA' __nullarea label word dw NUM_VAL dup(INIT_VAL) public __nullarea _NULL ends

Here is the error message that I was seeing:

; ; miscellaneous code-segment messages ; NullAssign db '*** NULL assignment detected',0

And here is the code that checks the storage for changes at the end of the program:

__exit proc near public "C",__exit push ax mov dx,DGROUP mov ds,dx cld ; check lower region for altered values lea di,__nullarea ; set es:di for scan mov es,dx mov cx,NUM_VAL mov ax,INIT_VAL repe scasw pop ax ; restore return code je ok ; ; low memory has been altered ; mov bx,ax ; get exit code mov ax,offset NullAssign ; point to msg mov dx,cs ; . . . ...

That code defines 32 bytes of 0x01 at the beginning of the data segment, and they can be addressed using the variable name "__nullarea". The bytes are present and initialized before the program starts. At the end of the program the __exit routine will be called and it will check to see that those 32 bytes are still 0x01. If they are not, you will get an error message.

I created a callable function in C that does the same thing:

extern "C" uint8_t _nullarea; uint8_t *_nullareap = &_nullarea; bool failed = false; extern "C" void nullCheck( const char *loc ) { // Only generate a trace message the first time it is detected. if (failed == true) return; int good = true; for ( int i=0; i < 32; i++ ) { if ( _nullareap[i] != 0x01 ) { good = false; break; } } if ( good == false ) { TRACE_WARN(("Null check failed at %s\n", loc)); Utils::dumpBytes( Trace_Stream, _nullareap, 32 ); failed = true; } }

And then, I inserted a call to this code in various places in my Telnet code to try to narrow down where the problem was happening.

Eventually I got to the code that calls the packet driver to send a packet on the wire:

nullCheck("Packet_send_pkt sendattempt"); int86x( Packet_int, &inregs, &outregs, &segregs); nullCheck("Packet_send_pkt after soft int");

The second call to nullCheck was tripping. So it was not a problem in my Telnet code, it was something in the packet driver which is why my if-checks for NULL pointers never showed anything.

The trace showed me the following:

2026-04-17 16:41:13.76 Nullarea is at 318a:0000 . . . 2026-04-17 16:41:28.59 W Null check failed at Packet_send_pkt after soft int Buffer address: 318a:0000 01 0100 02 12 00 56 3401 01 01 01 01 01 01 01 ......V4........ 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 ................

Interestingly, only six bytes were corrupted and the contents of the six bytes were the same in every trace that I looked at. Sometimes the six bytes would move around but that was probably due to the changes I was making to Telnet to add the code or the trace points.

Knowing that this was only happening on the packet send path I looked for those six bytes in the trace and found this right at the very top:

2026-04-17 16:41:13.22 mTCP telnet Version: Apr 17 2026 2026-04-17 16:41:13.27 PACKETINT=0x60 MAC=00.02.12.00.56.34MTU=1400 2026-04-17 16:41:13.27 IPADDR=192.168.2.122 NETMASK=255.255.255.255 GATEWAY=192.168.2.121 2026-04-17 16:41:13.33 Debug level: 0xff, DOS Version: 6.00 2026-04-17 16:41:13.33 Tcp: Allocated 1 sockets, MTU is 1400, My MSS is 1360 2026-04-17 16:41:13.33 NAMESERVER=192.168.2.1 2026-04-17 16:41:13.38 DOS Sleep calls enabled: int 0x28:1 int 0x2f,1680:0

So the six bytes of corruption in the _nullarea are the MAC address of the simulated Ethernet device that EtherSLIP is providing. This is a very powerful clue - I now knew to look in EtherSlip on the send path, specifically where it might be copying a MAC address.

Before we pick apart the packet driver code and expose the bug, we should review some x86 architecture.

Classic x86 architecture has 16 bit registers and a segmented memory model that allows you to address up to 1MB of memory. A segment is a region of memory that starts on a 16 byte boundary (a paragraph); on a classic IBM PC there are 64K possible segments, each spaced 16 bytes apart. Segment values are stored in special registers called segment registers.

To address a single byte of memory you combine a segment register and a 16 bit offset to construct a pointer. The segment defines the start of the memory region (always at a paragraph boundary) and the offset lets you reach any byte in that region, up to the range of the offset. Since the offset is a 16 bit value, that lets you address up to 64KB. To

© 2026 Now Let Us. All rights reserved.

Source: Hacker News

Advertisement
Ad slot ready: 5887729102

More in this category

EXPLORE TOPICS

Discover All Categories

Deep dive into the specific technology sectors that matter most to you.