Skip to content

Commit

Permalink
[prefix] Fix use of writable code segment on 486 and earlier CPUs
Browse files Browse the repository at this point in the history
In real mode, code segments are always writable.  In protected mode,
code segments can never be writable.  The precise implementation of
this attribute differs between CPU generations, with subtly different
behaviour arising on the transitions from protected mode to real mode.

At the point of transition (when the PE bit is cleared in CR0) the
hidden portion of the %cs descriptor will retain whatever attributes
were in place for the protected-mode code segment, including the fact
that the segment is not writable.  The immediately following code will
perform a far control flow transfer (such as ljmp or lret) in order to
load a real-mode value into %cs.

On the Pentium and later CPUs, the retained protected-mode attributes
will be ignored for any accesses via %cs while the CPU is in real
mode.  A write via %cs will therefore be allowed even though the
hidden portion of the %cs descriptor still describes a non-writable
segment.

On the 486 and earlier CPUs, the retained protected-mode attributes
will not be ignored for accesses via %cs.  A write via %cs will
therefore cause a CPU fault.  To obtain normal real-mode behaviour
(i.e. a writable %cs descriptor), special logic is added to the ljmp
instruction that populates the hidden portion of the %cs descriptor
with real-mode attributes when a far jump is executed in real mode.
The result is that writes via %cs will cause a CPU fault until the
first ljmp instruction is executed, after which writes via %cs will be
allowed as expected in real mode.

The transition code in libprefix.S currently uses lret to load a
real-mode value into %cs after clearing the PE bit.  Experimentation
shows that only the ljmp instruction will work to load real-mode
attributes into the hidden portion of the %cs descriptor: other far
control flow transfers (such as lret, lcall, or int) do not do so.

When running on a 486 or earlier CPU, this results in code within
libprefix.S running with a non-writable code segment after a mode
transition, which in turn results in a CPU fault when real-mode code
in liba20.S attempts to write to %cs:enable_a20_method.

Fix by constructing and executing an ljmp instruction, to trigger the
relevant descriptor population logic on 486 and earlier CPUs.  This
ljmp instruction is constructed on the stack, since the .prefix
section may be executing directly from ROM (or from memory that the
BIOS has write-protected in order to emulate an ISA ROM region) and so
cannot be modified.

Reported-by: Nikolai Zhubr <[email protected]>
Signed-off-by: Michael Brown <[email protected]>
  • Loading branch information
mcb30 committed Feb 2, 2022
1 parent 6ba671a commit bc35b24
Showing 1 changed file with 10 additions and 4 deletions.
14 changes: 10 additions & 4 deletions src/arch/x86/prefix/libprefix.S
Original file line number Diff line number Diff line change
Expand Up @@ -380,6 +380,11 @@ process_bytes:
pushl %eax
pushl %ebp

/* Construct ljmp code on stack (since .prefix may not be writable) */
.equ LJMP_LEN, 0x06
pushw %cs /* "nop ; ljmp %cs, $2f" */
pushw $2f
pushw $0xea90
/* Construct GDT on stack (since .prefix may not be writable) */
.equ GDT_LEN, 0x20
.equ PM_DS, 0x18 /* Flat data segment */
Expand Down Expand Up @@ -410,8 +415,9 @@ process_bytes:
pushw %es
pushw %ds
pushw %ss
pushw %cs
pushw $2f
pushw %ss /* Far pointer to ljmp code on stack */
leaw (GDT_LEN + 1)(%bp), %ax
pushw %ax
cli
data32 lgdt (%bp)
movl %cr0, %eax
Expand All @@ -438,7 +444,7 @@ process_bytes:
popfw
movl %eax, %cr0
lret
2: /* lret will ljmp to here */
2: /* lret will ljmp to here (via constructed ljmp on stack) */
popw %ss
popw %ds
popw %es
Expand All @@ -461,7 +467,7 @@ process_bytes:

/* Restore GDT */
data32 lgdt -8(%bp)
leaw GDT_LEN(%bp), %sp
leaw (GDT_LEN + LJMP_LEN)(%bp), %sp

/* Restore registers and return */
popl %ebp
Expand Down

0 comments on commit bc35b24

Please sign in to comment.