diff --git a/src/arch/x86/gdt.rs b/src/arch/x86/gdt.rs
index f8b1e8eac09afc878302c5d638d2160e75d82d61..6e1eb58f97661bdc1ceb272ac9080ea0b4a88ef1 100644
--- a/src/arch/x86/gdt.rs
+++ b/src/arch/x86/gdt.rs
@@ -16,11 +16,12 @@ pub const GDT_NULL: usize = 0;
 pub const GDT_KERNEL_CODE: usize = 1;
 pub const GDT_KERNEL_DATA: usize = 2;
 pub const GDT_KERNEL_KPCR: usize = 3;
-pub const GDT_USER_CODE32_UNUSED: usize = 4;
+pub const GDT_USER_CODE: usize = 4;
 pub const GDT_USER_DATA: usize = 5;
-pub const GDT_USER_CODE: usize = 6;
-pub const GDT_TSS: usize = 7;
-pub const GDT_CPU_ID_CONTAINER: usize = 8;
+pub const GDT_USER_FS: usize = 6;
+pub const GDT_USER_GS: usize = 7;
+pub const GDT_TSS: usize = 8;
+pub const GDT_CPU_ID_CONTAINER: usize = 9;
 
 pub const GDT_A_PRESENT: u8 = 1 << 7;
 pub const GDT_A_RING_0: u8 = 0 << 5;
@@ -52,7 +53,7 @@ static mut INIT_GDT: [GdtEntry; 4] = [
 ];
 
 #[thread_local]
-pub static mut GDT: [GdtEntry; 9] = [
+pub static mut GDT: [GdtEntry; 10] = [
     // Null
     GdtEntry::new(0, 0, 0, 0),
     // Kernel code
@@ -61,12 +62,14 @@ pub static mut GDT: [GdtEntry; 9] = [
     GdtEntry::new(0, 0, GDT_A_PRESENT | GDT_A_RING_0 | GDT_A_SYSTEM | GDT_A_PRIVILEGE, GDT_F_PROTECTED_MODE),
     // Kernel TLS
     GdtEntry::new(0, 0, GDT_A_PRESENT | GDT_A_RING_0 | GDT_A_SYSTEM | GDT_A_PRIVILEGE, GDT_F_PROTECTED_MODE),
-    // Dummy 32-bit user code - apparently necessary for SYSEXIT. We restrict it to ring 0 anyway.
-    GdtEntry::new(0, 0, GDT_A_PRESENT | GDT_A_RING_0 | GDT_A_SYSTEM | GDT_A_EXECUTABLE | GDT_A_PRIVILEGE, GDT_F_PROTECTED_MODE),
+    // User (32-bit) code
+    GdtEntry::new(0, 0, GDT_A_PRESENT | GDT_A_RING_3 | GDT_A_SYSTEM | GDT_A_EXECUTABLE | GDT_A_PRIVILEGE, GDT_F_PROTECTED_MODE),
     // User data
     GdtEntry::new(0, 0, GDT_A_PRESENT | GDT_A_RING_3 | GDT_A_SYSTEM | GDT_A_PRIVILEGE, GDT_F_PROTECTED_MODE),
-    // User (64-bit) code
-    GdtEntry::new(0, 0, GDT_A_PRESENT | GDT_A_RING_3 | GDT_A_SYSTEM | GDT_A_EXECUTABLE | GDT_A_PRIVILEGE, GDT_F_PROTECTED_MODE),
+    // User FS (for TLS)
+    GdtEntry::new(0, 0, GDT_A_PRESENT | GDT_A_RING_3 | GDT_A_SYSTEM | GDT_A_PRIVILEGE, GDT_F_PROTECTED_MODE),
+    // User GS (for TLS)
+    GdtEntry::new(0, 0, GDT_A_PRESENT | GDT_A_RING_3 | GDT_A_SYSTEM | GDT_A_PRIVILEGE, GDT_F_PROTECTED_MODE),
     // TSS
     GdtEntry::new(0, 0, GDT_A_PRESENT | GDT_A_RING_3 | GDT_A_TSS_AVAIL, 0),
     // Unused entry which stores the CPU ID. This is necessary for paranoid interrupts as they have
diff --git a/src/arch/x86/start.rs b/src/arch/x86/start.rs
index 84b063a8005a7b75fe22f0599857b4917e104fce..92d26cd7b466cb927b9d9ec144bc2793edc1bdb1 100644
--- a/src/arch/x86/start.rs
+++ b/src/arch/x86/start.rs
@@ -322,7 +322,9 @@ pub unsafe extern "C" fn usermode(_ip: usize, _sp: usize, _arg: usize, _is_singl
             mov eax, {user_data_seg_selector}
             mov ds, eax
             mov es, eax
+            mov eax, {user_fs_seg_selector}
             mov fs, eax
+            mov eax, {user_gs_seg_selector}
             mov gs, eax
 
             // Set up iret stack
@@ -353,6 +355,8 @@ pub unsafe extern "C" fn usermode(_ip: usize, _sp: usize, _arg: usize, _is_singl
         shift_singlestep = const(SHIFT_SINGLESTEP),
         user_data_seg_selector = const(gdt::GDT_USER_DATA << 3 | 3),
         user_code_seg_selector = const(gdt::GDT_USER_CODE << 3 | 3),
+        user_fs_seg_selector = const(gdt::GDT_USER_FS << 3 | 3),
+        user_gs_seg_selector = const(gdt::GDT_USER_GS << 3 | 3),
 
         options(noreturn),
     );
diff --git a/src/context/arch/x86.rs b/src/context/arch/x86.rs
index 8a5ce7bccf2dea4bbdcac631ef3f90b51519e573..ff541dfad2cfe570f28c713f3c90daccb5db7ca8 100644
--- a/src/context/arch/x86.rs
+++ b/src/context/arch/x86.rs
@@ -3,7 +3,7 @@ use core::sync::atomic::AtomicBool;
 
 use alloc::sync::Arc;
 
-use crate::gdt::{GDT, GDT_TSS};
+use crate::gdt::{GDT, GDT_USER_FS, GDT_USER_GS};
 use crate::paging::{RmmA, RmmArch, TableKind};
 use crate::syscall::FloatRegisters;
 
@@ -135,8 +135,10 @@ pub unsafe fn switch_to(prev: &mut super::Context, next: &mut super::Context) {
     );
 
     {
-        prev.arch.gsbase = GDT[GDT_TSS].offset() as usize;
-        GDT[GDT_TSS].set_offset(next.arch.gsbase as u32);
+        prev.arch.fsbase = GDT[GDT_USER_FS].offset() as usize;
+        GDT[GDT_USER_FS].set_offset(next.arch.fsbase as u32);
+        prev.arch.gsbase = GDT[GDT_USER_GS].offset() as usize;
+        GDT[GDT_USER_GS].set_offset(next.arch.gsbase as u32);
     }
 
     match next.addr_space {
diff --git a/src/scheme/proc.rs b/src/scheme/proc.rs
index 63d4167f3bac8d7cb540417f26cfebb6724ea384..a7c6efc844f6770c2f17977789b2d7e0ea5deaf1 100644
--- a/src/scheme/proc.rs
+++ b/src/scheme/proc.rs
@@ -404,7 +404,24 @@ impl ProcScheme {
         Err(Error::new(EINVAL))
     }
 
-    #[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
+    #[cfg(target_arch = "x86")]
+    fn read_env_regs(&self, info: &Info) -> Result<EnvRegisters> {
+        let (fsbase, gsbase) = if info.pid == context::context_id() {
+            unsafe {
+                (
+                    crate::gdt::GDT[crate::gdt::GDT_USER_FS].offset() as u64,
+                    crate::gdt::GDT[crate::gdt::GDT_USER_GS].offset() as u64
+                )
+            }
+        } else {
+            try_stop_context(info.pid, |context| {
+                Ok((context.arch.fsbase as u64, context.arch.gsbase as u64))
+            })?
+        };
+        Ok(EnvRegisters { fsbase: fsbase as _, gsbase: gsbase as _ })
+    }
+
+    #[cfg(target_arch = "x86_64")]
     fn read_env_regs(&self, info: &Info) -> Result<EnvRegisters> {
         let (fsbase, gsbase) = if info.pid == context::context_id() {
             #[cfg(not(feature = "x86_fsgsbase"))]
@@ -442,7 +459,37 @@ impl ProcScheme {
         Err(Error::new(EINVAL))
     }
 
-    #[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
+    #[cfg(target_arch = "x86")]
+    fn write_env_regs(&self, info: &Info, regs: EnvRegisters) -> Result<()> {
+        println!("{:?}", regs);
+
+        if !(RmmA::virt_is_valid(VirtualAddress::new(regs.fsbase as usize)) && RmmA::virt_is_valid(VirtualAddress::new(regs.gsbase as usize))) {
+            return Err(Error::new(EINVAL));
+        }
+
+        if info.pid == context::context_id() {
+            unsafe {
+                crate::gdt::GDT[crate::gdt::GDT_USER_FS].set_offset(regs.fsbase);
+                crate::gdt::GDT[crate::gdt::GDT_USER_GS].set_offset(regs.gsbase);
+
+                match context::contexts().current().ok_or(Error::new(ESRCH))?.write().arch {
+                    ref mut arch => {
+                        arch.fsbase = regs.fsbase as usize;
+                        arch.gsbase = regs.gsbase as usize;
+                    }
+                }
+            }
+        } else {
+            try_stop_context(info.pid, |context| {
+                context.arch.fsbase = regs.fsbase as usize;
+                context.arch.gsbase = regs.gsbase as usize;
+                Ok(())
+            })?;
+        }
+        Ok(())
+    }
+
+    #[cfg(target_arch = "x86_64")]
     fn write_env_regs(&self, info: &Info, regs: EnvRegisters) -> Result<()> {
         if !(RmmA::virt_is_valid(VirtualAddress::new(regs.fsbase as usize)) && RmmA::virt_is_valid(VirtualAddress::new(regs.gsbase as usize))) {
             return Err(Error::new(EINVAL));