1,055 karma · joined October 1, 2021
64K of actual text content in a single node could be reached in some documents, but it's not that small, more than a chapter of a typical book.
What was always a problem for segmented memory was graphics, at least if you wanted higher resolution than 320x200 at 256 colors. But you could have a segment pointer to each row of pixels instead of an entire image, as long as it would still fit within 1 MB (16 MB in the 286 protected mode).
9B5 BIST1 -> TMPD 0x0303 PASS2
9B6 SIGMA -> EDX
9B7 BIST2 -> TMPE TMPD XOR
9B8 SIGMA 0x3ddc0c2c XOR
9B9 SIGMA -> EAX BOOTUP_JUMP JFPUOK
0x303 = family 3, model 0, stepping id 3.Also browsers by default using a blocklist from some company, and showing a giant scary warning and contacting their server when the user deliberately navigates to an URL that is on that list. That should be opt-in as well, rather than something that just happens and is considered acceptable.
Besides the blocking, being able to see at the click of a button what kind of crap most sites want to load is really eye opening. And they would do so completely silently if you're using a "normie" browser created or financially supported by the largest advertising company in the world.
Instead the mainstream gets "security features" like Safe Browsing, where it connects to a Google server every day without most people's consent or even knowledge, downloading a list of hashes of "bad stuff" to block. Like open source software to download videos from YouTube (yt-dlp), which it flags as malware. Of course the tinfoil hat conspiracy theory that it's also sending every URL you visit to their server isn't true -- only the ones that match a hash, "to check for false positives". It's easy to see how this mechanism could be abused to log who is visiting particular URLs of interest, without alerting the user to it happening. As far as I see it, you would just have to trust them when they super-double-pinky-swear they would never do this. And of course the TLAs wouldn't allow them to disclose it if something like this happened on their orders.
On the Z80 and 8086, the code can be made one byte shorter by taking advantage of adjust-after-subtraction, which the 8080 didn't have (and on 6502 worked differently):
CP 10 / CMP AL,10 ;set carry if valid decimal digit
SBC A,69H / SBB AL,69H ;0..9 => 96h..9Fh (auxC=1), 10..15 => A1h..A6h (auxC=0)
DAA / DAS ;subtract 66h if auxC set, 60h if clear cmp dword [rax], 'XAUT'
jne .rxa_next
cmp dword [rax+4], 'HORI'
jne .rxa_next
cmp word [rax+8], 'TY'
jne .rxa_next
cmp byte [rax+10], '='
jne .rxa_next
; Found XAUTHORITY=path
Okay, this is setup code that only runs once at startup - but that would be a reason to optimize it for size and/or readability! REPE CMPSB exists, and may not be the fastest, but certainly the most compact and idiomatic way to compare strings. Or write a subroutine to do it!This pattern is used everywhere for copying or comparing strings, this was just one example of it.
There's a state variable that's used to keep track of whether the input is text to be displayed or part of a control sequence. It's a full 64 bits, probably not because we need 18 quintillion states? Here's how it is evaluated:
; Dispatch based on state
mov rcx, [vt_state]
cmp rcx, VT_ESC
je .vtp_esc
cmp rcx, VT_CSI
je .vtp_csi
cmp rcx, VT_CSI_PARAM
...
In total, there are 7 compares + conditional jumps, one after another. Compilers would generate a jump table for this, and a better option in assembly might be to make vt_state a pointer to the label we want to go to. Branch predictors nowadays can handle indirect jumps, and may actually have more trouble with such tightly clustered conditionals as seen in this code.This code is on the "slow" path, there's a faster one for 7-bit ASCII outside of control sequences, with a lengthy comment by Claude at the top on how it optimized this. Even this one starts with a bunch of conditionals though:
cmp qword [vt_state], 0 ; VT_NORMAL == 0
jne .vtp_loop_slow
cmp dword [utf8_remaining], 0
jne .vtp_loop_slow
cmp byte [pending_wrap], 0
jne .vtp_loop_slow
These could likely all be condensed into a single test or indirect jump via the state variable, by introducing just a few more states for UTF-8 decoding and wrap. Following this, here's a "useless use of TEST" (the subtraction already set the flags): mov rbx, [grid_cols]
sub rbx, [cursor_col] ; rbx = cells left on this row
test rbx, rbx
jle .vtp_loop_slow ; no room (or already past)
This also again shows the compulsive use of 64-bit registers and variables for values that should never be this big. It's not the "natural" data size on x86-64 at all, every such instruction requires an extra prefix byte.I freely confess that I'm a "Luddite", and was explicitly looking for bad (and obviously so) code, but this took me just a few minutes of scrolling through the nearly 20K lines in this file, so it should be somewhat representative of the whole.
That's been a feature since DOS 2.0, there was even an undocumented option AVAILDEV to make the prefix mandatory, instead of having device names present everywhere. But it broke the common trick used to detect if a directory exists ("if exist c:\some\path\nul").
Of course one could say that Windows 11 provides negative value, in which case running DOS 1.00 would also be better ;)
This may actually have been a bug not in the code, but in the assembler used to build it. The 8080 had mnemonics ROL and ROR that rotated through carry, and RLC/RRC (standing for "rotate circular", not "through carry"). Opposite meaning of the 8086 mnemonics! So I suspect this may have been switched up in the assembler, especially if it was running on an 8080 machine and developed by someone more familiar with its instruction set.
The STORE bug would have prevented using files over the size of 512 bytes, not just 64K. It's dividing by the sector size, and if DX was greater than that, it would have caused a "divide overflow" exception, since the result wouldn't fit in 16 bits.
(Also, by the Laws of Robotics you have to tell me if you're an LLM, or used one to generate this comment.)
EDIT: not an assembler bug, it seems. The printed listing shows that it produced the correct opcode for ROL (RCL would be D1 D1):
0A28 8A 0E D4 1B 1353 MOV CL,[RELOC]
0A2C D1 C1 1354 ROL CX
0A2E D1 C1 1355 ROL CX
0A30 88 0E D4 1B 1356 MOV [RELOC],CL
So I don't know why this version of the code wouldn't have worked. Maybe the penciled-in "fix" was to free up CL for some other purpose?Early versions of Microsoft Flight Simulator included a handler for the divide exception, which adjusted the result to +/- "infinity" (for a 16-bit signed integer, 32767 or -32768). The rest of the code relied on this in order to work correctly, and it was more efficient to take advantage of the processor's microcode doing this check rather than coding it explicitly before every division.
So even if it doesn't make mathematical sense, being able to continue after this type of exception is a useful feature to have.
>(and if you ignore SIGSEGV, it is considered perfectly acceptable that your program spins in a SIGSEGV loop until you kill it.)
This, on the other hand, shouldn't ever be acceptable. If a fatal-by-default signal is just ignored, it should always terminate the process.
What Windows 9x didn't have was security. A program could interfere with these mechanisms, but usually only if it was designed to do that, not as a result of a random bug (if the entire machine crashed, it was usually because of a buggy driver).
Merely pointing out that where both operations were available, there seems to have been a preference to use SUB instead, with some continuity from early business-oriented mainframes, to the 360, to the PC.
It could have been added to x86, even as a group of single-byte opcodes with the register encoded in three bits (as with PUSH, POP, and INC/DEC outside of long mode). But the XOR idiom was already established on the 8080 by that point.
(There was another, now deleted, comment somewhere in this thread that mentioned IBM's preference for SUB. Source of that statement was Claude, but it seems very likely to be correct. The BIOS code I've checked myself, lots of 'SUB AX,AX', no XOR)
There's also the TEST instruction, which does a logical AND but without storing the result (like CMP does for SUB). This can be used to test specific bits.
Testing a single register for zero can be done in several ways, in addition to CMP with 0:
TEST AX,AX
AND AX,AX
OR AX,AX
INC AX followed by DEC AX (or the other way around)
The 8080/Z80 didn't have TEST, but the other three were all in common use. Particularly INC/DEC, since it worked with all registers instead of just the accumulator.Also any arithmetic operation sets those flags, so you may not even need an explicit test. MOV doesn't set flags however, at least on x86 -- it does on some other architectures.
My test code looked like this:
mov ax,1 ;new MSW
mov bx,TestSel ;pointer to selector value into BX
mov dx,[bx] ;and load into DX
mov cl,31 ;shift count for delay
cli ;disable interrupts
lgdt [Gdtr]
lidt [Idtr]
jmp enter_pm ;flush queue now
align 2
enter_pm: ;go!
rol cl,cl ;delay while following instructions decode
lmsw ax ;set PE bit
mov es,[bx] ;should load selector 0x0010 into ES
mov ds,dx ;should set DS base to 0x00100 [NOPE]
str ax ;should trap because not allowed in real mode
ud2 ;trap anyway in case it didn't
On the 286, this always caused the processor to reset. Replacing one of the two segment load instructions with a same-length "mov ax,ax" didn't change that, but removing one of them did.In that case the "str ax" acted as the control transfer that flushed the queue (it was still decoded in real mode, so it went to the "invalid opcode" entry point). No clue as to what exactly happens to cause the reset when three instructions are run from the queue, some timing issue related to when the PE bit actually changes vs. what the decoder is doing at this point?
Some later documentation contradicted this, saying that instead this first jump had to be to the protected mode segment.
From the patent (US4442484), it is apparent that the processor decodes opcodes into a microcode entry point before they are executed, and the PE bit is one of the inputs for the entry point PLA. So that would be the obvious reason for flushing the prefetch queue - but it turns out that at least on the 80286, most instructions go to the same entry point regardless of the mode they are decoded in. So they should work the same without flushing the queue.
And yet for some reason, what I've seen in my experiments is that the system would reset if there were three instructions following the "LMSW" without a jump. Even something harmless like "NOP" or "MOV AX,AX", that couldn't be different between real and protected mode. Maybe there is some clock phase where the PE bit changing during the decoding of an instruction leads to an invalid entry point, that either causes a triple fault or resets the processor?
I've decoded the entry point PLA of the 80286 (not the actual microcode though). It also has separate entries for real and protected mode, but only for segment loads from a general purpose register, HLT, and for those opcodes that aren't allowed in real mode like ARPL.
Loading a segment register from memory on the 286 uses the same microcode in both modes, as does everything else that would certainly have to act differently, like jump/call far. That was a bit surprising, since it would have to decide at run time which mode it's in. Is this the same on the 386?
Tested on my 286 machine what happens when opcodes are decoded while in real mode but executed after PE is set: Segment load from memory works (using protected mode semantics), whereas the load from register only changes the visible selector and nothing else. The base in the descriptor cache keeps whatever was set there before -- I assume on the 386, SBRM would update the base the same way it does in real mode in that situation, because it's also used for V86 mode there. Illegal-in-real-mode instructions trap, but do so correctly using the protected mode IDT.
Also seems like executing three pre-decoded instructions without a jump after setting PE causes a triple fault for some reason.
What was patented was the technique of checking for a delay of about a second to separate the command from any data. It still had to be sent from the local side of the connection, so the exploit needed some way to get it echoed back (like ICMP).
More relevant to this bug: https://en.wikipedia.org/wiki/ANSI_bomb#Keyboard_remapping
DOS had a driver ANSI.SYS for interpreting terminal escape sequences, and it included a non-standard one for redefining keys. So if that driver was installed, 'type'ing a text file could potentially remap any key to something like "format C: <Return> Y <Return>".
I first assumed it was redirecting them to a library in user mode somehow, but actually the syscall is replaced with "int3", which also goes to the kernel. The whole reason why the "syscall" instruction was introduced in the first place was that it's faster than the old software interrupt mechanism which has to load segment descriptors.
So why not simply use KVM to intercept syscall (as well as int 80h), and then emulate its effect directly, instead of replacing the opcode with something else? Should be both faster and also less obviously detectable.
The broader problem with terminal control sequences didn't exist on Windows (until very recently at least), or before that DOS and OS/2. You had API calls to position the cursor, set color/background, etc. Or just write directly to a buffer of 80x25 characters+attribute bytes.
But Unix is what "serious" machines -a long time ago- used, so it has become the religion to insist that The Unix Way(TM) is superior in all things...
>It's user data in JSON in an HTTP stream in a TLS record in a TCP stream in an IP packet in an ethernet frame. Then it goes into a SQL query which goes into a B-tree node which goes into a filesystem extent which goes into a RAID stripe which goes into a logical block mapped to a physical block etc. All of those have control data in the same stream under the hood.
It's true that a lot of code out there has bugs with escape sequences or field lengths, and some protocols may be designed so badly that it may be impossible to avoid such bugs. But what you are suggesting is greatly exaggerated, especially when we get to the lower layers. There is almost certainly no way that writing a "magic" byte sequence to a file will cause the storage device to misinterpret it as control data and change the mapping of logical to physical blocks. They've figured out how to separate this information reliably back when we were using floppy disks.
That the bits which control the block mapping are stored on the same device as a record in an SQL database doesn't mean that both are "the same stream".
Not true. For most binary protocols, you have something like <Header> <Length of payload> <Payload>. On magnetic media, sector headers used a special pattern that couldn't be produced by regular data [1] -- and I'm sure SSDs don't interpret file contents as control information either!
There may be some broken protocols, but in most cases this kind of problem only happens when all the data is a stream of text that is simply concatenated together.
[1] e.g. https://en.wikipedia.org/wiki/Modified_frequency_modulation#...
Even if you disallow executing anything outside of the .text section, you still need the syscall trap to protect against adversarial code which hides the instruction inside an immediate value:
foo: mov eax, 0xc3050f ;return a perfectly harmless constant
ret
...
call foo+1
(this could be detected if the tracing went by control flow instead of linearly from the top, but what if it's called through a function pointer?)