Skip to contentSkip to conversation

i386 Losing fs gs to bios

PATCH In review Zachary Amsden 2 participants 2 messages Nov 8, 2005 Pnp support

lore
Share & tools
lore
#0 lore raw

PATCH 7/21 i386 Losing fs gs to bios

I discovered an even more subtle problem; the PnP BIOS code is saving the %fs and %gs segments in inline assembler, yet it also uses the same hack for patching in a fake real mode selector for the BIOS data area. Note that the protected mode selector 0x40 overlaps the user TLS area in the GDT; this means that badly timed PnP BIOS calls could come in, save %fs, come back, and restore %fs -- to point to the fake real mode selector in the GDT. This selector will remain cached despite the GDT update until the next task context switch, and could very well be responsible for causing random crashes and corruption in user space programs which make use of it (notably, Wine).

Rather than leave a half effort, I wrote an encapsulation function that saves and restores GDT state properly before attempting to call a potentially buggy BIOS. Note that saving and restoring %fs and %gs must be done after restoring the fake real mode GDT entry (0x40 >> 3), since they could possibly be referencing that segment. Also note that %cs, %ss, %ds, and %es need not be messed with, since in kernel mode, they never can point to a user TLS segment.

include/asm-i386/desc.h +26 -0
Index: linux-2.6.14-zach-work/include/asm-i386/desc.h
===================================================================
@@ -158,6 +158,32 @@ static inline unsigned long get_desc_bas
 	return base;
 }
 
+struct bios_segment_save {
+	struct desc_struct save_desc_40;
+	struct desc_struct *gdt;
+	unsigned short saved_fs;
+	unsigned short saved_gs;
+};
+
+static inline void prepare_bios_segments(struct bios_segment_save *save_area)
+{
+	int cpu = get_cpu();
+	struct desc_struct *gdt = get_cpu_gdt_table(cpu);
+	save_area->gdt = gdt;
+	savesegment(fs, save_area->saved_fs);
+	savesegment(gs, save_area->saved_gs);
+	save_area->save_desc_40 = gdt[GDT_ENTRY_BAD_BIOS];
+	gdt[GDT_ENTRY_BAD_BIOS] = gdt[GDT_ENTRY_BAD_BIOS_CACHE];
+}
+
+static inline void restore_bios_segments(struct bios_segment_save *save_area)
+{
+	save_area->gdt[GDT_ENTRY_BAD_BIOS] = save_area->save_desc_40;
+	loadsegment(fs, save_area->saved_fs);
+	loadsegment(gs, save_area->saved_gs);
+	put_cpu();
+}
+
 #endif /* !__ASSEMBLY__ */
 
 #endif
drivers/pnp/pnpbios/bioscalls.c +5 -12
Index: linux-2.6.14-zach-work/drivers/pnp/pnpbios/bioscalls.c
===================================================================
@@ -87,8 +87,7 @@ static inline u16 call_pnp_bios(u16 func
 {
 	unsigned long flags;
 	u16 status;
-	struct desc_struct save_desc_40;
-	int cpu;
+	struct bios_segment_save save_area;
 
 	/*
 	 * PnP BIOSes are generally not terribly re-entrant.
@@ -97,10 +96,8 @@ static inline u16 call_pnp_bios(u16 func
 	if(pnp_bios_is_utter_crap)
 		return PNP_FUNCTION_NOT_SUPPORTED;
 
-	cpu = get_cpu();
-	save_desc_40 = per_cpu(cpu_gdt_table,cpu)[0x40 / 8];
-	per_cpu(cpu_gdt_table,cpu)[0x40 / 8] = 
-		per_cpu(cpu_gdt_table,cpu)[GDT_ENTRY_BAD_BIOS_CACHE];
+	/* Save %fs, %gs and TLS segment which fakes real mode selector 0x40 */
+	prepare_bios_segments(&save_area);
 
 	/* On some boxes IRQ's during PnP BIOS calls are deadly.  */
 	spin_lock_irqsave(&pnp_bios_lock, flags);
@@ -117,15 +114,11 @@ static inline u16 call_pnp_bios(u16 func
 		"pushl %%esi\n\t"
 		"pushl %%ds\n\t"
 		"pushl %%es\n\t"
-		"pushl %%fs\n\t"
-		"pushl %%gs\n\t"
 		"pushfl\n\t"
 		"movl %%esp, pnp_bios_fault_esp\n\t"
 		"movl $1f, pnp_bios_fault_eip\n\t"
 		"lcall %5,%6\n\t"
 		"1:popfl\n\t"
-		"popl %%gs\n\t"
-		"popl %%fs\n\t"
 		"popl %%es\n\t"
 		"popl %%ds\n\t"
 	        "popl %%esi\n\t"
@@ -142,8 +135,8 @@ static inline u16 call_pnp_bios(u16 func
 	);
 	spin_unlock_irqrestore(&pnp_bios_lock, flags);
 
-	per_cpu(cpu_gdt_table,cpu)[0x40 / 8] = save_desc_40;
-	put_cpu();
+	/* Restore GDT segments and unpin the CPU */
+	restore_bios_segments(&save_area);
 
 	/* If we get here and this is set then the PnP BIOS faulted on us. */
 	if(pnp_bios_is_utter_crap)
arch/i386/kernel/apm.c +11 -44

87 lines not on this page · raw message

#1 lore raw
Quoted 11 lines · Zachary Amsden

I discovered an even more subtle problem; the PnP BIOS code is saving the %fs and %gs segments in inline assembler, yet it also uses the same hack for patching in a fake real mode selector for the BIOS data area. Note that the protected mode selector 0x40 overlaps the user TLS area in the GDT; this means that badly timed PnP BIOS calls could come in, save %fs, come back, and restore %fs -- to point to the fake real mode selector in the GDT. This selector will remain cached despite the GDT update until the next task context switch, and could very well be responsible for causing random crashes and corruption in user space programs which make use of it (notably, Wine).

Quoted 7 lines

Rather than leave a half effort, I wrote an encapsulation function that saves and restores GDT state properly before attempting to call a potentially buggy BIOS. Note that saving and restoring %fs and %gs must be done after restoring the fake real mode GDT entry (0x40 >> 3), since they could possibly be referencing that segment. Also note that %cs, %ss, %ds, and %es need not be messed with, since in kernel mode, they never can point to a user TLS segment.

as per my previous comment, wouldnt this patch mostly go away if we dedicated entry #8 to the 0x40 BIOS hack?

Ingo