Skip to content

Commit 89075c2

Browse files
CvvTjaybosamiya-ms
andauthored
Support TLS (#156)
This PR tries to address #66. Similar to how libc uses fs on x86_64 (or gs on x86) for thread-local storage, we could use the other one (i.e., gs on x86_64 and fs on x86) for litebox's TLS. Note that on x86_64, we can directly modify fs/gs base, while on x86, we have to call syscall `set_thread_area` to add an entry to GDT and update `fs` selector to point to the new entry. --------- Co-authored-by: Jay Bosamiya (Microsoft) <jayb@microsoft.com>
1 parent 5fdd6af commit 89075c2

File tree

4 files changed

+306
-18
lines changed

4 files changed

+306
-18
lines changed

litebox/src/platform/mod.rs

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -539,3 +539,33 @@ pub trait SystemInfoProvider {
539539
/// execution context and transfer control to the syscall handler.
540540
fn get_syscall_entry_point(&self) -> usize;
541541
}
542+
543+
/// A provider for thread-local storage.
544+
pub trait ThreadLocalStorageProvider {
545+
type ThreadLocalStorage;
546+
547+
/// Set a thread-local storage value for the current thread.
548+
///
549+
/// # Panics
550+
///
551+
/// Panics if TLS is set already.
552+
fn set_thread_local_storage(&self, value: Self::ThreadLocalStorage);
553+
554+
/// Invokes the provided callback function with the thread-local storage value for the current thread.
555+
///
556+
/// # Panics
557+
///
558+
/// Panics if TLS is not set yet.
559+
/// Panics if TLS is borrowed already (e.g., recursive call).
560+
fn with_thread_local_storage_mut<F, R>(&self, f: F) -> R
561+
where
562+
F: FnOnce(&mut Self::ThreadLocalStorage) -> R;
563+
564+
/// Release the thread-local storage value for the current thread
565+
///
566+
/// # Panics
567+
///
568+
/// Panics if TLS is not set yet.
569+
/// Panics if TLS is being used by [`Self::with_thread_local_storage_mut`].
570+
fn release_thread_local_storage(&self) -> Self::ThreadLocalStorage;
571+
}

litebox_common_linux/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ version = "0.1.0"
44
edition = "2024"
55

66
[dependencies]
7+
bitfield = "0.19.1"
78
bitflags = "2.9.0"
89
cfg-if = "1.0.0"
910
litebox = { path = "../litebox/", version = "0.1.0" }

litebox_common_linux/src/lib.rs

Lines changed: 95 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ use syscalls::Sysno;
1414

1515
pub mod errno;
1616

17+
extern crate alloc;
18+
1719
// TODO(jayb): Should errno::Errno be publicly re-exported?
1820

1921
bitflags::bitflags! {
@@ -681,13 +683,102 @@ pub unsafe fn wrfsbase(fs_base: usize) {
681683
}
682684
}
683685

686+
/// Reads the GS segment base address
687+
///
688+
/// ## Safety
689+
///
690+
/// If `CR4.FSGSBASE` is not set, this instruction will throw an `#UD`.
691+
#[cfg(target_arch = "x86_64")]
692+
pub unsafe fn rdgsbase() -> usize {
693+
let ret: usize;
694+
unsafe {
695+
core::arch::asm!(
696+
"rdgsbase {}",
697+
out(reg) ret,
698+
options(nostack, nomem)
699+
);
700+
}
701+
ret
702+
}
703+
704+
/// Writes the GS segment base address
705+
///
706+
/// ## Safety
707+
///
708+
/// If `CR4.FSGSBASE` is not set, this instruction will throw an `#UD`.
709+
///
710+
/// The caller must ensure that this write operation has no unsafe side
711+
/// effects, as the GS segment base address might be in use.
712+
#[cfg(target_arch = "x86_64")]
713+
pub unsafe fn wrgsbase(gs_base: usize) {
714+
unsafe {
715+
core::arch::asm!(
716+
"wrgsbase {}",
717+
in(reg) gs_base,
718+
options(nostack, nomem)
719+
);
720+
}
721+
}
722+
723+
/// Linux's `user_desc` struct used by the `set_thread_area` syscall.
684724
#[repr(C, packed)]
685725
#[derive(Debug, Clone)]
686726
pub struct UserDesc {
687-
pub entry_number: i32,
688-
pub base_addr: i32,
689-
pub limit: i32,
690-
pub flags: i32,
727+
pub entry_number: u32,
728+
pub base_addr: u32,
729+
pub limit: u32,
730+
pub flags: UserDescFlags,
731+
}
732+
733+
bitfield::bitfield! {
734+
/// Flags for the `user_desc` struct.
735+
#[derive(Clone, Copy)]
736+
pub struct UserDescFlags(u32);
737+
impl Debug;
738+
/// 1 if the segment is 32-bit
739+
pub seg_32bit, set_seg_32bit: 0;
740+
/// Contents of the segment
741+
pub contents, set_contents: 1, 2;
742+
/// Read-exec only
743+
pub read_exec_only, set_read_exec_only: 3;
744+
/// Limit in pages
745+
pub limit_in_pages, set_limit_in_pages: 4;
746+
/// Segment not present
747+
pub seg_not_present, set_seg_not_present: 5;
748+
/// Usable by userland
749+
pub useable, set_useable: 6;
750+
/// 1 if the segment is 64-bit (x86_64 only)
751+
pub lm, set_lm: 7;
752+
}
753+
754+
/// Struct for thread-local storage.
755+
pub struct ThreadLocalStorage<Platform: litebox::platform::RawPointerProvider> {
756+
/// Indicates whether the TLS is being borrowed.
757+
pub borrowed: bool,
758+
759+
#[cfg(target_arch = "x86")]
760+
pub self_ptr: *mut ThreadLocalStorage<Platform>,
761+
pub current_task: alloc::boxed::Box<Task>,
762+
763+
pub __phantom: core::marker::PhantomData<Platform>,
764+
}
765+
766+
impl<Platform: litebox::platform::RawPointerProvider> ThreadLocalStorage<Platform> {
767+
pub const fn new(task: alloc::boxed::Box<Task>) -> Self {
768+
Self {
769+
borrowed: false,
770+
#[cfg(target_arch = "x86")]
771+
self_ptr: core::ptr::null_mut(),
772+
current_task: task,
773+
__phantom: core::marker::PhantomData,
774+
}
775+
}
776+
}
777+
778+
/// A task associated with a thread in LiteBox.
779+
pub struct Task {
780+
/// Thread identifier
781+
pub tid: u32,
691782
}
692783

693784
#[repr(C)]

litebox_platform_linux_userland/src/lib.rs

Lines changed: 180 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -574,6 +574,23 @@ fn set_fs_base_arch_prctl(fs_base: usize) -> Result<usize, litebox_common_linux:
574574
})
575575
}
576576

577+
#[cfg(target_arch = "x86")]
578+
fn set_thread_area(
579+
user_desc: litebox::platform::trivial_providers::TransparentMutPtr<
580+
litebox_common_linux::UserDesc,
581+
>,
582+
) -> Result<usize, litebox_common_linux::errno::Errno> {
583+
unsafe { syscalls::syscall1(syscalls::Sysno::set_thread_area, user_desc.as_usize()) }.map_err(
584+
|err| match err {
585+
syscalls::Errno::EFAULT => litebox_common_linux::errno::Errno::EFAULT,
586+
syscalls::Errno::EINVAL => litebox_common_linux::errno::Errno::EINVAL,
587+
syscalls::Errno::ENOSYS => litebox_common_linux::errno::Errno::ENOSYS,
588+
syscalls::Errno::ESRCH => litebox_common_linux::errno::Errno::ESRCH,
589+
_ => panic!("unexpected error {err}"),
590+
},
591+
)
592+
}
593+
577594
pub struct PunchthroughToken {
578595
punchthrough: PunchthroughSyscall<LinuxUserland>,
579596
}
@@ -674,19 +691,7 @@ impl litebox::platform::PunchthroughToken for PunchthroughToken {
674691
}
675692
#[cfg(target_arch = "x86")]
676693
PunchthroughSyscall::SetThreadArea { user_desc } => {
677-
use litebox::platform::RawConstPointer as _;
678-
unsafe {
679-
syscalls::syscall1(syscalls::Sysno::set_thread_area, user_desc.as_usize())
680-
}
681-
.map_err(|err| {
682-
litebox::platform::PunchthroughError::Failure(match err {
683-
syscalls::Errno::EFAULT => litebox_common_linux::errno::Errno::EFAULT,
684-
syscalls::Errno::EINVAL => litebox_common_linux::errno::Errno::EINVAL,
685-
syscalls::Errno::ENOSYS => litebox_common_linux::errno::Errno::ENOSYS,
686-
syscalls::Errno::ESRCH => litebox_common_linux::errno::Errno::ESRCH,
687-
_ => panic!("unexpected error {err}"),
688-
})
689-
})
694+
set_thread_area(user_desc).map_err(litebox::platform::PunchthroughError::Failure)
690695
}
691696
}
692697
}
@@ -1207,12 +1212,148 @@ impl litebox::platform::SystemInfoProvider for LinuxUserland {
12071212
}
12081213
}
12091214

1215+
impl LinuxUserland {
1216+
#[cfg(target_arch = "x86_64")]
1217+
fn get_thread_local_storage() -> *mut litebox_common_linux::ThreadLocalStorage<LinuxUserland> {
1218+
let tls = unsafe { litebox_common_linux::rdgsbase() };
1219+
if tls == 0 {
1220+
return core::ptr::null_mut();
1221+
}
1222+
tls as *mut litebox_common_linux::ThreadLocalStorage<LinuxUserland>
1223+
}
1224+
1225+
#[cfg(target_arch = "x86")]
1226+
fn get_thread_local_storage() -> *mut litebox_common_linux::ThreadLocalStorage<LinuxUserland> {
1227+
let mut fs_selector: u16;
1228+
unsafe {
1229+
core::arch::asm!(
1230+
"mov {0:x}, fs",
1231+
out(reg) fs_selector,
1232+
options(nostack, preserves_flags)
1233+
);
1234+
}
1235+
if fs_selector == 0 {
1236+
return core::ptr::null_mut();
1237+
}
1238+
1239+
let mut addr: usize;
1240+
unsafe {
1241+
core::arch::asm!(
1242+
"mov {0}, fs:{offset}",
1243+
out(reg) addr,
1244+
offset = const core::mem::offset_of!(litebox_common_linux::ThreadLocalStorage<LinuxUserland>, self_ptr),
1245+
options(nostack, preserves_flags)
1246+
);
1247+
}
1248+
addr as *mut litebox_common_linux::ThreadLocalStorage<LinuxUserland>
1249+
}
1250+
}
1251+
1252+
/// Similar to libc, we use fs/gs registers to store thread-local storage (TLS).
1253+
/// To avoid conflicts with libc's TLS, we choose to use gs on x86_64 and fs on x86
1254+
/// as libc uses fs on x86_64 and gs on x86.
1255+
impl litebox::platform::ThreadLocalStorageProvider for LinuxUserland {
1256+
type ThreadLocalStorage = litebox_common_linux::ThreadLocalStorage<LinuxUserland>;
1257+
1258+
#[cfg(target_arch = "x86_64")]
1259+
fn set_thread_local_storage(&self, tls: Self::ThreadLocalStorage) {
1260+
let old_gs_base = unsafe { litebox_common_linux::rdgsbase() };
1261+
assert!(old_gs_base == 0, "TLS already set for this thread");
1262+
let tls = Box::new(tls);
1263+
unsafe { litebox_common_linux::wrgsbase(Box::into_raw(tls) as usize) };
1264+
}
1265+
1266+
#[cfg(target_arch = "x86")]
1267+
fn set_thread_local_storage(&self, tls: Self::ThreadLocalStorage) {
1268+
let mut old_fs_selector: u16;
1269+
unsafe {
1270+
core::arch::asm!(
1271+
"mov {0:x}, fs",
1272+
out(reg) old_fs_selector,
1273+
options(nostack, preserves_flags)
1274+
);
1275+
}
1276+
assert!(old_fs_selector == 0, "TLS already set for this thread");
1277+
1278+
let mut tls = Box::new(tls);
1279+
tls.self_ptr = tls.as_mut();
1280+
1281+
let mut flags = litebox_common_linux::UserDescFlags(0);
1282+
flags.set_seg_32bit(true);
1283+
flags.set_useable(true);
1284+
let mut user_desc = litebox_common_linux::UserDesc {
1285+
entry_number: u32::MAX,
1286+
base_addr: Box::into_raw(tls) as u32,
1287+
limit: u32::try_from(core::mem::size_of::<Self::ThreadLocalStorage>()).unwrap() - 1,
1288+
flags,
1289+
};
1290+
let user_desc_ptr = litebox::platform::trivial_providers::TransparentMutPtr {
1291+
inner: &raw mut user_desc,
1292+
};
1293+
set_thread_area(user_desc_ptr).expect("Failed to set thread area for TLS");
1294+
1295+
let new_fs_selector = ((user_desc.entry_number & 0xfff) << 3) | 0x3; // user mode
1296+
// set fs selector
1297+
unsafe {
1298+
core::arch::asm!(
1299+
"mov fs, {0:x}",
1300+
in(reg) new_fs_selector,
1301+
options(nostack, preserves_flags)
1302+
);
1303+
}
1304+
}
1305+
1306+
#[cfg(target_arch = "x86_64")]
1307+
fn release_thread_local_storage(&self) -> Self::ThreadLocalStorage {
1308+
let tls = Self::get_thread_local_storage();
1309+
assert!(!tls.is_null(), "TLS must be set before releasing it");
1310+
unsafe {
1311+
litebox_common_linux::wrgsbase(0);
1312+
}
1313+
1314+
let tls = unsafe { Box::from_raw(tls) };
1315+
assert!(!tls.borrowed, "TLS must not be borrowed when releasing it");
1316+
*tls
1317+
}
1318+
1319+
#[cfg(target_arch = "x86")]
1320+
fn release_thread_local_storage(&self) -> Self::ThreadLocalStorage {
1321+
let tls = Self::get_thread_local_storage();
1322+
assert!(!tls.is_null(), "TLS must be set before releasing it");
1323+
unsafe {
1324+
core::arch::asm!(
1325+
"mov fs, {0}",
1326+
in(reg) 0,
1327+
options(nostack, preserves_flags)
1328+
);
1329+
}
1330+
1331+
let tls = unsafe { Box::from_raw(tls) };
1332+
assert!(!tls.borrowed, "TLS must not be borrowed when releasing it");
1333+
*tls
1334+
}
1335+
1336+
fn with_thread_local_storage_mut<F, R>(&self, f: F) -> R
1337+
where
1338+
F: FnOnce(&mut Self::ThreadLocalStorage) -> R,
1339+
{
1340+
let tls = Self::get_thread_local_storage();
1341+
assert!(!tls.is_null(), "TLS must be set before accessing it");
1342+
let tls = unsafe { &mut *tls };
1343+
assert!(!tls.borrowed, "TLS is already borrowed");
1344+
tls.borrowed = true; // mark as borrowed
1345+
let ret = f(tls);
1346+
tls.borrowed = false; // mark as not borrowed anymore
1347+
ret
1348+
}
1349+
}
1350+
12101351
#[cfg(test)]
12111352
mod tests {
12121353
use core::sync::atomic::AtomicU32;
12131354
use std::thread::sleep;
12141355

1215-
use litebox::platform::RawMutex;
1356+
use litebox::platform::{RawMutex, ThreadLocalStorageProvider as _};
12161357

12171358
use crate::LinuxUserland;
12181359
use litebox::platform::PageManagementProvider;
@@ -1249,4 +1390,29 @@ mod tests {
12491390
prev = page.end;
12501391
}
12511392
}
1393+
1394+
#[test]
1395+
fn test_tls() {
1396+
let platform = LinuxUserland::new(None);
1397+
let tls = LinuxUserland::get_thread_local_storage();
1398+
assert!(tls.is_null(), "TLS should be null in the main thread");
1399+
platform.set_thread_local_storage(litebox_common_linux::ThreadLocalStorage::new(Box::new(
1400+
litebox_common_linux::Task { tid: 0xffff },
1401+
)));
1402+
platform.with_thread_local_storage_mut(|tls| {
1403+
assert_eq!(
1404+
tls.current_task.tid, 0xffff,
1405+
"TLS should have the correct task ID"
1406+
);
1407+
tls.current_task.tid = 0x1234; // Change the task ID
1408+
});
1409+
let tls = platform.release_thread_local_storage();
1410+
assert_eq!(
1411+
tls.current_task.tid, 0x1234,
1412+
"TLS should have the correct task ID"
1413+
);
1414+
1415+
let tls = LinuxUserland::get_thread_local_storage();
1416+
assert!(tls.is_null(), "TLS should be null after releasing it");
1417+
}
12521418
}

0 commit comments

Comments
 (0)