#shkwve: an incomplete kernel/maybe bootloader/UEFI thing/maybe something else if i get bored again
1 messages ยท Page 5 of 1
hmm does virtio execute operations in order?
i dont think it guarantees ordering ๐ค
okay i have a semi working virtio driver!
time to make it async (๐๐๐)
okay lets figure out interrupts then ig
okay now i have a queue dpc
so hmm
should i write a shitty driver which submits at most one operation to the device
or should i write a good driver which can pipeline them
yeah no ill just put a lock around each virtqueue
i probably will have pipelined ops for xhci though
a good driver is not that much work if you use coroutines 
lol yes it is
but i wrote a bad driver that uses coroutines
and it works just fine
i'd probably have to use at least one or two more semaphores, and that sounds hard
okay now i have handling for multiple virtqueues maybe
okay now i have a horrible virtio-console """driver"""
okay lets abstract it all out
virtio-console, with a semi not shitty driver!
async, too
so now i guess i could do blk
or i could do net
okay, now i have a network device i think
but your old username wasn't constexpr
okay now i have my shitty buffer wrapper actually working
hmm, im not sure how to design my network stack
In a bootloader?
are you gonna do qr code booting
now that you said it
ive already figured out how to do it
Well unless you are the firmware uefi has helpers for that
i had code for limine
"scan this qr code and upload binary to boot"
i am in fact the firmware
and the uefi network stack is shit
โ ๏ธ
look at STREAMS
yeah im not sure if i like the streams-like design
the other thing is i only care about support for like 3 protocols total
udp, tcp, icmpv4/v6
okay i came up with a new way to do my network stack
Tell us all about it
i came up with this ultra cursed network packet building interface
net::Packet p = net::build([&] <net::BuildPhase phase> (net::PacketBuilder<phase>& b) {
// virtio
b.u8(0); // flags
b.u8(0); // gso type
b.le16(0); // hedaer length
b.le16(0); // gso size
b.le16(0); // checksum start
b.le16(0); // checksum offset
// ethernet
b.ether(net::EthAddr::broadcast);
b.ether(ether);
b.be16(cast<u16>(net::EtherType::IP));
// ip
IP ip{net::IPProto::UDP, net::IPv4Addr{255, 255, 255, 255}, net::IPv4Addr{0, 0, 0, 0}};
ip.write(b);
// udp
usize len = b.payladSize();
b.be16(68); // sport
b.be16(67); // dport
b.be16(len); // len
b.be16(0); // checksum
// test payload
b.cstring("hello, world!"_s);
});
something like this
and then i'll have higher-level interfaces for the higher-level protocols (udp, tcp, icmp), i think
then i do two passes, first to get the length
and then to write all the fields
truly revolutionary
but what if instead you allocated an mblk and set the b_wptr to base + maximum size of virtio, ethernet, ip, udp/tcp headers, and then sent it through a STREAMS stack such that each module subtracted its header's size from b_wptr?
hmm
the problem is calculating checksums
okay wtf somehow i have 160k of code
which is weird
hmm okay i think a lot of that is stack
its okay i think
thats my fdt::Node::is_compatible function
okay so some of it was -ffreestanding
now i have incompatible definitions of mem*
oh, oh no
my iterator wrappers have very bad codegen it seems
okay ill redo it then
to work less like rust and more like something approximating good codegen
i wonder how much code size i'd save if i compiled out all of the dchecks
like 4k
hmm i think i might be failing even before virtio-net
compiling out dchecks makes it crash earlier
hmm maybe i should do ubsan
okay i am hitting ubsan
trap: type mismatch
lmao what
also thats a wrong decode i think
so the first one is a function type check
wtf
specifically in the event claim function
oh fun!
llvm bug :^)
im just adding more and more checks and hardening at this point lol
i have SSP too now
doesn't trip
same global corruption bug
if this is another llvm bug istg
i think my allocator is fucked
Duality of man
now i'm getting PointerOverflow ubsan traps
it's from a vector push?
oh its a copy to null
shit
ah
zero length memcpies are broken
okay more memory corruption
okay now i hit a FunctionTypeMismatch
sounds like more heap corruption tbh
okay i added a vtable check
(inserted a magic cookie inside of the vtable and started checking it before doing calls)
maybe my promise type is broken?
vf->vmspace.ref()._authenticateVtable();
Promise<> _detached = [tp = vf->transport, vma = vf->vmspace] () -> Promise<> {
vma.ref()._authenticateVtable();
DMAMemory mem = vma->allocDMAMemory(4096).unwrap();
while (true) {
co_await tp->submit(0, {virtio::Element{mem.physAddr, 4096, virtio::Dir::DeviceToHost}});
parsePkt(Span{cast<const u8*>(mem.virtAddr), 4096});
}
}();
``` the second call to _authenticateVtable dchecks
yeah okay i can repro under addresssanitizer
stack uaf
okay i think i dont get c++ coroutines, part N+1
fucking hell
reads my own code again
what the actual fuck did i write
and why
oh, OH
what the actual fuck are these semantics lmao
wonderful i think i have to rewrite my promise type
and my blocking run
and add a detached run function too i think
okay i think im gonna try reading the old new thing and see if that helps me understand this crap
what's bad about deciding whether to delete self depending on if you suspend or not
huh what?
is that how it's meant to work?
also i think now my allocator is broken lmao
ugh i think i might have more memory corruption still
oh this time its in-place OOB?
im somewhat tempted to port KASAN
ah the magic option i really want is -mllvm -asan-mapping-offset
okay now i'm FARing 0x1fffa00008011704
okay now i have a sane asan base
still need to map it
ehh it sounds annoying
for fucks sake
i think i have uaf maybe?
okay i think my changes fixed the allocator
hopefully
maybe
It depends on initial_suspend and final_syspend
the latter decides who frees the coroutine afaik
wtf
i have a mildly cursed idea
what if i added go code to shkwve
okay maybe i wont go that cursed
๐
why not 
no i dont think i will that sounds a bit too cursed even for me
I mean I was planning to use it in pmOS for example, though the bad compiler kinda spoiled it
(though not in kernel)
ah okay
okay
im going back to shkwve
so far my progress is this
mapped into virtual memory in higher half
and full W^X
(there is a cpu bit to force enable w^x)
also i think my allocator is a bit fucked lol
probably info/warn/error
ah I guess ~ is debug now that i think about it
yeah
yep
my nvme is now nvmeing
and i need to think of a good way of implementing disk caches
Bootloader page cache 
of course!
i mean i already have a dma buffer allocator
and virtual memory
and a complement of synchronization primitives that would be acceptable for many full OSes
including something kinda like turnstiles ish
but more like parking_lot and go semaphores
including an entirely original and extremely unique API: ```rs
pub unsafe fn park(addr: usize) -> UnparkToken;
pub unsafe fn park_until(
addr: usize,
deadline: Instant,
) -> Result<UnparkToken, code!(Status::TIMEOUT)>;
pub unsafe fn unpark_one(
addr: usize,
callback: impl FnOnce(UnparkOneResult) -> UnparkToken,
) -> UnparkOneResult;
#[derive(Debug, Clone, Copy)]
pub struct UnparkOneResult {
pub unparked: bool,
pub be_fair: bool,
pub more_in_queue: bool,
}
and i have a one byte mutex based on that hmm, where have i heard of that idea before
semaphores, bounded queues, Event, sleeping mutexes with timeouts
and none of my sync primitives allocate, obviously
well ok thats not quite true
i use a third party queue library for my scheduling queue which does actually allocate
but i still have an allocation-free timer heap
What does this do?
the same as the functions with the same names in parking_lot_core
This library exposes a low-level API for creating your own efficient synchronization primitives.
Scheduler in bootloader
Wtf
Honestly just turn this into a kernel
yes its a great idea
yea i mean its pretty much a bad kernel at this point
except it also has a funky self relocating bootloader combined into its image
and i want to make it work as a real bootloader at some point
no
Or a lock backend
Alright thanks
its mostly a userspace futex
^
but thats boooring
plus it implies i have to care about things like actually having a userspace at some point
and it means i cant justify writing code with zero concern for OOM conditions under the guise of "yeah its fine its just a bootloader anyway"
Lol
also i have some cool ideas that dont make sense in a kernel
like for example, i had this idea to make an AML virtual machine which can be serialized back into functioning AML
unless i just say that my kernel has kexec support ๐
Damn
it would not be EASY
but it would be COOL
and i dont think it would actually be THAT hard
Like an AML interpreter in aml?
Hm
okay im resurrecting this
shkwve is backkkkkkk kinda
shkwve: an incomplete kernel/maybe bootloader/UEFI thing/maybe something else if i get bored again
is there a new repo?
no
i didnt do the whole version control thing yet
and i need to unhardcode some paths first
right now i have this hardcoded lmao
but i also have epic stacktraces (only for kasan errors, i didnt wire it up for panics lmao)
(this is a random crash i inserted on purpose to show how cool my stacktraces are)
oh yeah also i got usermode code executing
bro has usermode in a bootloader
no its not a bootloader anymore
im planning to maybe do kexec at some point but im going full userspace now
im gonna try microkerneling
the name "shkwve" is really just "whatever project im working on right now" ngl
apple has that too
and microsoft has that on the xbox one
well actually idk if its a thing in the xbox one bootloader, i know its a thing in the xbox one bootrom
like when u pick a boot drive
apple uefi literally does usermode
it looks like mac os ui
i think its just reconstructed
for what?
hardening
oooh KASAN
option roms run in usermode iirc
Nice
to date, it has found 0 bugs
doesnt uefi require all memory identity mapped
Is it proper compiler KASAN or just hardening features
yes its proper kasan
Yes but it's cool to have
I might try implementing it
Didn't know rust had it
except bugs in the kasan impl
why is it janky?
isn't rust supposed to be safe
no need for one since its memory safe 
isn't it just done by generic llvm passes

it interacts very poorly with inlining
ur supposed to add a safety comment which makes it safe
#[cfg_attr(feature = "kasan", inline(never))]
#[cfg_attr(feature = "kasan", sanitize(address = "off"))]
pub unsafe fn write_noasan<T>(ptr: *mut T, value: T) {
if cfg!(feature = "kasan") {
let src = ManuallyDrop::new(value);
unsafe { memcpy(ptr as *mut (), &raw const src as *const (), size_of::<T>()) };
} else {
unsafe { *ptr = value };
}
}
wait, shit
i forgot
now its good
idk if this kind of write also breaks
but like a bunch of the pointer operations end up calling helpers
which ends up breaking
huh zig doesn't even have KASAN apparently
because the helpers dont have kasan disabled
it surely has normal asan
it's weird that you need inline(never), on Managarm we compile with LTO and we never ran into an issue where nosanitize was not respected
I think it does, but they do a lot of it through their debug allocator too
a c++ pointer load lowers to an llvm load
a rust pointer load lowers to a call to a helper function which does some checks
ah i see
that function is in libcore, and has sanitizers forced on
my kasan isnt actually kasan
its normal addresssanitizer
ah
built with this very simple set of flags
I know there's bespoke KASAN too
but that's essentially KASAN
i have -asan-mapping-scale=4 and a different offset
the difference to userspace ASAN is that the mapping offset is known a priori
what does that do?
other than that, I don't think there is a difference
it halves the size of the shadow mapping compared to the default
so the checks are more coarse?
ish
in practice no
with kasan enabled, i put 16 bytes between each zone (~normal heap) allocation anyway
and the compiler does the same thing for stack stuff
most of these dont really help much
I only do poisoning when enabled and I think it's probably more than enough lol
i dont think any of the -asan-opt-* flags do anything useful
my kasan runtime is quite complicated in general too
because it can print those awesome errors with inline-aware stacktraces
also its c++, its like the one(ish) piece of c++ in my kernel binary
Is that mostly dwarf
no
i have a custom format
thats easier to parse
which saves probably quite a lot over dwarf too
How many languages do you have
Like 3?
I've never seen a pitust project not be a cursed mix of languages and I like it tbh
3 + assembly + a tiny amount of shell script
how did u make clang output it
my binary is like literally 93% dwarf lmao
i didnt
i make rustc emit dwarf
and then i have an insane dwarf parser and converter in my build tooling
holy
actually a good solution
yes
that's how i'd also do it
if you dont want a binary thats 93% dwarf its basically the only viable approach i think
dwarf is pretty bad, especially if you want to use it on a hotpath (such as profiling)
have u checked how linux produces its orc stuff?
no
its unwind format
the build process is a lot more complicated than that
Even they don't use dwarf? Damn
i mean for unwinding i just use frame pointers, i might do something like orc later
theres also a guesser unwinder on x86
im guessing its just an interval list of frame sizes?
I'm probably gonna do dwarf because zig has a parser
what do u use your format for then?
ohhh
which is dwarf too
for symbolizing i just use kallsyms from linux
okay sure. but what if you want to demangle
rust demangling is not fun
You don't mangle :)
is that bad?
so id have to ship a demangler
well i wrote this when i wanted to do kasan
and the kasan runtime is very intentionally not written in rust
why?
you dont want to be using kasan-instrumented code anywhere within the kasan instrumentation impl
cant u just mark it as non instrumented
no
i mean yes i can mark my thing as not instrumented
but all of the things it calls will still get instrumented as normal
so it needs a separate compile session and flags
and you cant really mix two different rust static libraries and have them work, plus thats really bad for code size
the crate also doesnt expose any of the info, i actually do a bunch of postprocessing to hide frames that dont matter
and i also format some types specially
so for example all of core::ub_checks doesnt show up
damn rust really is a pain to work wth
and a whole bunch of my kernel's helper APIs dont show up either
so for example sk_kern::mmio::{read, write}{8, 16, 32, 64} will never be visible on a stacktrace because i check for that and hide them
why?
size + those functions are inline(always) and they take the address from the caller
and it looks ugly in a stacktrace tbh
so really the caller is at fault
if i want to know what exactly caused the issue i have a decompiler
isnt it useful to know it was mmio that caused a fault?
instead of having to read a disassembly
well it cant cause kasan errors so
ehh decompiler views are usually pretty good
and without a file name/line number its pretty hard to tell anyway
if it cant cause kasan errors then why hide it?
size
each entry takes a few bytes
how many symbols do u have lol
not a lot lmao
Infy one thing I wanna try is your fallible instructions
That's cool
pitust are you gonna do that too
yeah imo that shit is super convenient
90kb of unwind data total
less when its compressed (and my kernel is compressed, its self-decompressing)
which ones?
MSR ones I think?
i currently have abortable instructions for MSRs and try_memcpy
uhhhhh is that even reliable
the fuck do you have try_memcpy for
lol
currently only used in unwind code
oh
User as well?
here is a fun function inside of my usercopy region
usercopy needs special shit though
yeah its just a building block so ill totally use it for copy_to_user also
I'm not sure how it works
using that for copy to/from user is a bad idea
arm supremacy: i dont need to do validation
yeah thats basically that
why?
arm has a "load as if you did it from usermode" instruction lmao
ohh
You have to handle exceptions though? Do you have a list of fallible functions and on exceptions you check whether or not it was in that?
he probably has adhoc code to handle that specific one
No I mean you
if an exception is taken when PC is after arm64_usercopy_crit_start and before arm64_usercopy_crit_end, then ELR_EL1 (aka saved PC) is set to x8 and i return from the exception
yea x86 code calls into generic handle_abortable_instruction
thats what the adr x8, .kasan_fail is for
just read the code lol
Yeah ok fine 
# Save x0 and x1
sub sp, sp, 0x10
stp x0, x1, [sp, 0x00]
# Check if ELR_EL1 is in the usercopy region
mrs x0, ELR_EL1
adr x1, arm64_usercopy_crit_start
cmp x0, x1
b.lo 3f
adr x1, arm64_usercopy_crit_end
cmp x0, x1
b.hs 3f
# If so, fix stack and return to x8
add sp, sp, 0x10
msr ELR_EL1, x8
eret
``` i have special asm for this even in the exception path
Ah cool
does x86 guarantee that unknown MSRs fail with any specific exception
yeah, GPF
wait wtf does your code do lmao
i mean x86
if u try to wrmsr something bad u get a gpf
even if the msr is known
so GPF is both bad input and/or unknown msr
ah i see
although i think that ```c
if (ai->flags & ABORTABLE_INSTRUCTION_ENOSYS_ON_ERROR)
registers_set_return_value(regs, ENOSYS);
else if (ai->flags & ABORTABLE_INSTRUCTION_EFAULT_ON_ERROR)
registers_set_return_value(regs, EFAULT);
why
idk maybe it makes more sense in your impl
all it does is reduce code duplication
i just handle this outside of the asm helper
and the entire mechanism is private to the arm64 port
oh yeah i have something that vaguely approximates a strong separation of generic vs per-isa code
yeah i mean all it does is it makes it so u dont need a wrapper
i think its useful to have it as a generic thing
at least looking at how it scales in linux
in my case i need a wrapper anyway
which i stole the thing from
because rust?
no
because i want to handle dispatching faults to the right process manually
wdym by dispatching faults
imagine if there was a way for the arch to reject unsupported writes without an exception
like if you had a page fault
intel apparently can't imagine this
yeah imo it was a mistake to make it like that
intel has nothing, arm has ldtr, riscv has ? (ofc depending on your extensions)
wouldnt u still have high level code handle the e.g. copy_to_user failure?
ye but handling the two kinds of failure is extremely correlated
so its a bit of an architecture thing
like u handle copy to user errors and normal user faults in the same way?
no
it (riscv) can, it has write-any read-valid registers as well
i in general don't take [recoverable] kernel exceptions other than uaccess right now
ah ok
oh to system registers
and user exceptions have really special code
recoverable kernel exceptions except for very few cases are a mistake 
make a kernel where every kernel exception is recoverable
mine are only recoverable in uaccess
and in the smp trampoline
and i dont even have an error message otherwise
because arm moment
why does the smp trampoline need recoverable exceptions?
lol
well, memory isnt identity mapped
no, debug and single step from kernel mode should just cause an immediate boot drive erasure to make sure that developers don't do something stupid like that
and in general my translation is configured to writable implies execute never
and you cant execute from the direct map
you cant enable translation only for higher half
so if you don't take an exception, you are kinda stuck:
- you need to enable translation
- you need to be somewhere where after enabling translation, you will map to the same place in physmem
- you need to be somewhere which is not covered by any page table
- and the kernel is not identity mapped
i don't get it
thats annoying
in a specifically crafted address space
but yeah i guess that works too
so u manually fixup the pc of the cores when they start up or something?
you can also just intentionally take a prefetch abort
it's way less janky than catching a pf lol
no i set a special exception vector
crazy stuff
and then intentionally take either a prefetch abort or an undefined instruction
depending on how hard the core OoOs
we just treat it as a user page space except that we map it as a kernel page
the other annoying issue for me is that i dont really have a good allocator to use
allocator bringup occurs after i do smp
i have an early bootstrap bump allocator for smp, but that can't free
actually is it
what i do is definitely simpler
well you need the infrastructure anyway if you want to run userspace :^)
well no
the userspace stuff only works after you have the memory allocator initialized
true. but there's also no need to do smp before allocation
uhhh kinda
i need to know at least how many CPUs there are
because my allocator needs to allocate per-cpu structures
i could split it up
u can kinda parse that info and not start cpus at the same time lmao
we enumerate CPUs before allocation but launch them afterwards
yeah ofc. its more annoying tho
and there is zero benefit from doing so
okay and now im back to memory allocator hell
so i kinda want to allocate dma buffers
and like i could make them discontinous and say you cant allocate dma buffers larger than 1 page
hmm maybe i should just do that lol
ugh okay i decided to work on sync instead
and now im not sure how to implement sync lmao
i dont love the idea of taking a sleep lock
yeah this
not fsync lol im nowhere near disk drivers
what synchronization primitive do you want to implement?
you're writing in rust, right?
just do a block_on(future) :^)
lmao
that's what iretq did for thor-rs
futures are bad tho
they perform not great iirc
and they also dont match the rest of my model
well i was going for like a semaphore ๐
or an MPSC queue or something
a semaphore sounds like a good start
and can basically be used as a primitive for everything else
i mean go does that
okay ill just go steal more code and ideas from go
// Puts the current goroutine into a waiting state and calls unlockf on the
// system stack.
//
// If unlockf returns false, the goroutine is resumed.
//
// unlockf must not access this G's stack, as it may be moved between
// the call to gopark and the call to unlockf.
//
// Note that because unlockf is called after putting the G into a waiting
// state, the G may have already been readied by the time unlockf is called
// unless there is external synchronization preventing the G from being
// readied. If unlockf returns false, it must guarantee that the G cannot be
// externally readied.
//
// Reason explains why the goroutine has been parked. It is displayed in stack
// traces and heap dumps. Reasons should be unique and descriptive. Do not
// re-use reasons, add new ones.
awesome
so yeah go unlocks the semaphore bucket lock on the scheduler stack
for what?
go "semaphores" are basically implemented as userspace futexes
and go does its own userspace scheduling
for low leveling blocking i'd just add Thread::park(), Thread::unpark()
im not sure thats much better tbh
it's basically equivalent to a binary semaphore attached to each thread
it was the fashion in those days
it still is i think
yea
rust had green threads as well
so the first i saw of rust i took it to be basically a golike language and i still have the shadow of that prejudice in my mind
so now taking a lock means you take a semaphore, which acquires a lock, then you take from a binary semaphore, which acquires another lock
why does taking a semaphore take a lock?
well ig depends on the impl
oh wait no
yeah to enqueue yourself on the wait list you need to take a lock over the wait list
ah a spinlock
well yeah
or you do an "awesome" O(n^2) list traversal
ok i have something which may be a correct semaphore
use core::{
hint::{likely, unlikely},
sync::atomic::Ordering,
};
use crate::{
atomic::Atomic,
ex::sync::turnstile::{ParkToken, UnparkToken, park, unpark_one},
};
pub struct Semaphore {
value: Atomic<u64>,
}
impl Semaphore {
pub const fn new(value: u64) -> Semaphore {
assert!(value < 0x8000_0000_0000_0000);
Semaphore {
value: Atomic::new(value << 1),
}
}
unsafe fn put_slow(&self) {
unsafe {
let _ = unpark_one((&raw const self.value) as usize, |_, r| {
debug_assert_eq!(r.unparked_threads, 1);
if !r.have_more_threads {
self.value.fetch_sub(1, Ordering::Relaxed);
}
UnparkToken(0)
});
}
}
pub fn put(&self) {
let value = self.value.fetch_add(2, Ordering::Release);
if unlikely(value & 1 != 0) {
unsafe { self.put_slow() };
}
}
pub fn get(&self) {
loop {
let value = self.value.load(Ordering::Relaxed);
if value < 2 {
unsafe {
park((&raw const self.value) as usize, ParkToken(0), |_| {
if self.value.load(Ordering::Relaxed) < 2 {
self.value.fetch_or(1, Ordering::Relaxed);
true
} else {
false
}
})
};
continue;
}
if likely(
self.value
.compare_exchange_weak(value, value - 2, Ordering::Acquire, Ordering::Relaxed)
.is_ok(),
) {
return;
}
}
}
}
now is it a correct semaphore? lol idk
it feels right
dies immediately in miri when testing basically this impl against parking_lot_core
error: Undefined Behavior: Data race detected between (1) non-atomic write on thread `unnamed-2` and (2) non-atomic read on thread `unnamed-1` at alloc7
--> src/main.rs:80:44
|
80 | unsafe { VALUE.get().write(VALUE.get().read() + 1) };
| ^^^^^^^^^^^^^^^^^^ (2) just happened here
|
help: and (1) occurred earlier here
--> src/main.rs:80:26
|
80 | unsafe { VALUE.get().write(VALUE.get().read() + 1) };
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
= help: this indicates a bug in the program: it performed an invalid operation, and caused Undefined Behavior
= help: see https://doc.rust-lang.org/nightly/reference/behavior-considered-undefined.html for further information
= note: this is on thread `unnamed-1`
note: the current function got called indirectly due to this code
--> src/main.rs:77:9
|
77 | / thread::spawn(|| {
78 | | loop {
79 | | SEMA.get();
80 | | unsafe { VALUE.get().write(VALUE.get().read() + 1) };
... |
83 | | });
| |__________^
``` hmm yes
ok it turns out its just me dumb
lol
ok and now my turnstile/parkinglot knockoff is broken
Didn't you say that you don't want to do a parking lot knockoff? Or did you do it on top of semaphores as a basis?
idk did i say that
anyway i did that so
You did say that you dislike the idea of using park() and unpark() as primitive
i meant park() and unpark() as in rust std::thread::{park, Thread::unpark}
not as in parking_lot_core::{park, unpark_one, unpark_all}
okay ive got myself some one byte sleeping locks
ok after further thinking i might implement those lmao
lol
because they allow you to implement the other set of primitives without spraying spinlocks everywhere
and its a lot easier to test the primitives too
hmm does it even make sense to use a queued lock to protect my parkinglot meme's hash buckets
because disabling preemption is dirt cheap
depends how your per-cpu is implemented 
the fast path is this:```arm
ffffffff0000dd8c fe4fbfa9 stp lr, x19, [sp, #-0x10]! {__saved_lr} {__saved_x19}
ffffffff0000dd90 481640b9 ldr w8, [x18, #0x14]
ffffffff0000dd94 88030034 cbz w8, 0xffffffff0000de04
ffffffff0000dd98 08050051 sub w8, w8, #0x1
ffffffff0000dd9c 481600b9 str w8, [x18, #0x14]
ffffffff0000dda0 68000034 cbz w8, 0xffffffff0000ddac
ffffffff0000dda4 fe4fc1a8 ldp lr, x19, [sp], #0x10 {__saved_x19}
ffffffff0000dda8 c0035fd6 ret
pretend this got inlined
yeah im not qualified to judge arm per-cpu
you can count the number of instructions 
ill mark it inline(always) tbh
on x86 a good per-cpu would be inc gs:preempt_disable_count[rip]
plus a branch
why a branch?
you need to check for pending preemption
for enable
oh sorry this is the enable path
ah ok
disable does get inlined
its 3 instructions: ```arm
ffffffff0000e30c 481640b9 ldr w8, [x18, #0x14]
ffffffff0000e310 08050011 add w8, w8, #0x1
ffffffff0000e314 481600b9 str w8, [x18, #0x14]
ffffffff0000e318 c0035fd6 ret
read, add, write
whats in x18?
percpu data
kinda
but -ffixed-[reg] makes whatever you want reserved ๐
u pass that right?
yes ofc lmao
lol
its not reserved on linux
The role of register r18 is platform specific. If a platform ABI has need of a dedicated general-purpose register to carry inter-procedural state (for example, the thread context) then it should use this register for that purpose. If the platform ABI has no such requirements, then it should use r18 as an additional Caller-saved register. The platform ABI specification must document the usage for this register.
iirc linux stores per-cpu base in el0 sp
no they put the current task there
i have no idea
oh that might actually always do per_cpu_base[this_cpu_id]
its in tpidr_el1
base or just the id?
it does tpidr_el1 + offset_of_percpu_var
it lets you manipulate percpu data without reading from a register
i need a copy because i need to restore it after running userspace
oh ok
i also store the current CPU number in TPIDRRO_EL0
(which is readable by userspace)
makes sense
i guess i could set x18=current task or something but idk
this also requires writing per-arch asm
which is kinda annoying
im writing rust
also using seg_gs like that is definitely UB lol
how lol
actually maybe its not if you use an atomic
uhhh id have to think about the legality of that but it does not feel super robust lol
it's not allowed to cache gs: stuff
where does clang promise that lol
but yeah i havent read the docs, but iirc it was defined to not be cached
or so i heard somewhere
idk i cant find anything about that
and it would be super dumb if this was a generic rule
this is used on amdgpu and nvptx for like normal memory
ok but i dont care that much about definitions if this code always returns 1 lol ```c++
using u32 = uint32_t;
using u64 = uint64_t;
u32 test2(__seg_fs u32* p, u64* q) {
*p = 1;
*q = 2;
return *p;
}```
and thats the result on clang
(this is subject to normal strict aliasing btw)
i mean its ub to do per cpu writes/reads without preempt_disable
so this doesnt break anything
thats not the point
I think worrying about an extra indirection is most larp
It'd be surprised if this shows up in any meaningful benchmarks
this 100%
if you want even cheaper, a load followed by a store of 1 is even cheaper
instead of a counter
but everyone parrots linux including me and has a counter because ???
i do want to get rid of the counter (because its really dumb) but i didnt get around to it
also this is not even related to seg_gs lol
how
its just a general race-is-ub rule
no this is strict aliasing
sure, but whats your point?
you cant get it to emit an inc without asm
afaict
you can get a lock inc, which i guarantee is worse than a load+add+store
I think linux is the only mainstream OS with a preempt disable counter (?)
which has only 20 cycles of latency on alderlake!
yeah i think so
its a lot more common in hobby OSes though
yeah
do u just not allow nested preempt disable calls?
no you save the old value of the flag lol
you save the previous state
why would u do that if u can do less work with a counter?
because using a counter does more work
why would you use lock
well you cant get llvm to generate a non-lock op without asm
yeah
so you need per-isa porting work
if u want this optimized you will need asm
or you can just do a flag which only needs generic loads and stores
and an isa can override it
you can just use relaxed loads/stores instead of inc
there's 100% no measurable overhead
if you lock inc, there is
relaxed still uses a lock
yeah if you're lock, that's bad
well im looking at compiler exporer???
relaxed load/store, not relaxed fetch_add
this needs the preemption to be already disabled before that
no, why?
otherwise u increment preempt disable on a random cpu
well no
if you can get preempted then preempt_count == 0
what
the store is single-instruction atomic
cur_count = read(preempt_count)
cur_count += 1;
write(preempt_count, cur_count)
u can get preempted at any point before write
you cannot get preempted unless the disable flag is "committed"
doesnt matter, the preempt count is 0 on the target cpu
because u dont know if it was disabled before
yes you do, because context switch preserves the bit
or well i guess if u ended up on a different cpu u know it was enabled on that cpu
yeah i suppose it works if u have a boolean
it works with a counter too
so if u read and it returns 0, you might end up on a different cpu, but u know its 0 there to because u got switched to it in the first place
yes
it doesnt work with a counter
it does
oh ig same logic applies lol
a flag is still better though
a flag?
why
if it's non-zero already, you cannot change cpus
what no
and for the 0-1 transition, the same argument applies
oh i guess to save the old flag
yes
also lol wdym "everything"
every function you may call into
i mean im considering banning nested preempt_disable
sure
you only have to save it when you disable preemption
optmizing a load+store to an inc is 100% larp
you will just end up with
do_thing()
do_thing_with_preemption_already_disalbed()
It doesn't offer any real benefit but it has real costs
i doubt it makes any performance difference at all
namely that it becomes arch specific
inc has 3 uops
yeah, i doubt that you can show me a single benchmark where it makes a difference that is not within noise
so its just a rmw
yeah inc internally probably schedules the same sequence
on any competent CPU, the only difference is code size
its just less code bytes
oh wait no on zen5
i dont actually know if thats true
seems like on zen5 it might be a bit cheaper
but its not a measurable difference regardless its like 1 vs 3 cycles
and only a few things do that
my scheduler runs with irqs masked
fair ig lol
i dont want to take irqs mid context switch
you have to consider the latency of the entire code path though
its also not a hot code path
iret takes what, 1000 cycles?
less
eretu 
so if you reduce the preempt disable by 1 cycle you gain at most 0.1%
idk on m1 a linux syscall is like 300
in reality the irq does mode than just iret
yeah, a syscall is cheap
an irq is really expensive on x86
why? iret?
yeah
is it a property of x86 tho?
just iret using sysretq /j
Dont u have to do something similar to iret on other arches
#x86 message
an arm64 eret is <300 cycles
when i was testing thread register restore code paths, replaced sysretq by iretq for testing
so its not as bad
is there data on how expensive fred returns are?
and it turned out that iretq was more than twice as slow as the entire syscall
is that some sort of a no-op syscall
yes
oh ok lol
not just 2x slower than sysretq, 2x slower than kernel entry, syscall handler dispatch, ipl checks, preemption and migration checks, kernel exit combined
ok anyway going back to my earlier problem
should i just use a spinlock for this lol
i think thats probably a better idea than a complicated pushlock-style mechanism
What are you doing?
locking
Yes ok but what
like im doing generic locking infrastructure
But what is "this"
so basically im doing a ripoff of the wtf::parkinglot/turnstiles/futexes/go semaphores family
where you have a bunch of buckets with their own lock which you use to queue on (so that the inline sync primitive is smaller)
ah yeah typically you do have a spinlock over the buckets
if you mean the table hashed by the lock address
But you might not be able to do that if you implement your sleeping lock using that
you can make a second sleeping lock, that doesnt use the same infrastructure
which is larger
Yes but is it worth it?
lol i have no idea
how would you even measure that
my vibe is no though
not for a kernel
I'd say the lock likely won't experience that much contention
And it's a quick critical section
Yea
but you kinda have to disable preemption
Huh reading about parking lot it's basically turnstiles
And they also call not doing handoff barging
does parking lot / futex style locking make sense inside the kernel?
or are we talking about futexes for userspace?
yes, you do want small locks
Also with turnstiles you can do priority inheritance
But parking lot is not entirely like a turnstile, isn't it?
because it doesn't actually use the key as a pointer
I think BSD-style turnstiles make more sense
well i guess parking lot is just the blocking side of a turnstile lock
if you think of it that way
and then it makes sense, yeah
awesome, i started to test my impl in miri
that was fast lol
ok apparantly my lock is broken???
yeah i used a fucking relaxed store when unlocking
me dumb
How reliable is this software?
what is?
miri?
its by definition reliable for all rust programs nvm apparantly thats not the case
(well obviously it will have false negatives. but its guaranteed not to have false positives)
excluding compiler bugs ofc
does it work with barriers
it is reliable as long as all the code is written in rust
it cannot reason across FFI boundaries of course
i think it can somewhat do FFI now
but yeah obv its only useful if you have "normal" userspace code
it works by interpreting the rust program and tracking extra state per memory location
so it's rather slow / not usable for full programs
but it does detect races and other memory unsafety issues
it detects basically all violations of rust's memory access rules
it can be usable, if your program is simple. but yeah its very much not fast
yeah but it makes much more sense to run unit tests etc through miri than full programs
yeah
oh ok
okay im now trying to figure out a good ipc model
i still havent come up with a good one which isnt pain to implement
have you looked at existing ones?
a bit
so the big thing is that i want to make it work without dynamic allocation (i.e. allocation when opening a connection or setting up a server is fine, but anything else should be preallocated)
and ideally it would be asynchronous
asynchronous and no allocation kinda contracts each other, doesn't it?
well, lets call it "optimistically asynchronous"
unless you preallocate a bunch of inflight ipc data structures
my idea is to preallocate 1
and then you block if you cant allocate more
and if you can then you can obviously get more concurrency
im gonna look at managarm maybe
uhhhhh GCEs are not a good thing to rely on
they are more like a "this is getting removed possibly in a few months" kind of feature
not a "this is getting stabilized hopefully soonish" kind of feature
iirc our use of generic const expressions can be rewritten at the cost of making it more annoying / maybe using a proc macro
yea i see i see
assuming the only use is submit_async/Action, that looks easy enough to solve
yeah i think that's the only place
i think it's really only for the construction of the array
in let mut actions = [const { MaybeUninit::uninit() }; T::ACTION_COUNT];
also here is some functions in the managarm code i was just curious how you handle reading from shared memory
oop wrong paste
here is the correct 3rd picture
(you cannot just blind memcpy from random user-accessible pointers lmao)
this is probably not an issue but like
jfc
oh and also this is a controlled allocation primitive with whatever data you want on the only kernel zone, my favorite
at least my kernel attempts to do type isolation
even if it doesnt try super hard
the memcpy is from data shared with userspace
which you also cant just memcpy from safely
ah you mean because userspace could technically write and then its technically a data race?
yeah that's true
yeah
but there's no way around that in current c++
in rust i'd tell you to do by-value freeze
unless you hand roll and asm memcpy i guess
which some people regard as being sound
just do like asm volatile("" :: "r"(&the_object)); lol
that is sound within llvm
because data races are not llvm ub
but there is no TOCTOU here right? userspace cannot change the count parameter post hoc, neither can it invalidate the buffer
it can just influence the buffer contents
in the presence of inlining and llvm doing too much llvming you can actually get toctou here
imagine the compiler decided to remove the memcpy and read from the mapping directly (which is sound)
im not sure if you can prove that the stuff in the middle will have no side effects on memory, it could be enough of a barrier in practice
ah i see what you mean. that's true
and then it decided to remat the loads
i noted this down in https://github.com/managarm/managarm/issues/1331
there is a very funny strategy here i think
i THINK llvm might have intrinsics that let you do this
that arent surfaced
yeah i rolled this exact relaxed memcpy for my log ring lol
how did you implement it?
that probably has horrible codegen though
yeah
i dont know how else to solve it but yes
definitely worse than doing the mildly UB thing of memcpy then by-value freeze
what does that even mean
yeah\
ok so rust has a concept of a "byte" which has 257* values: 0-255, inclusive, and a special value called "uninit"
the simplest case of by-value freeze is when you go from a value of type "u8 | uninit" to a value of type "u8" by doing something like ```rs
fn by_value_freeze_u8(value: u8 | uninit) -> u8:
if is_uninint(value):
return unpredictable_byte()
else:
return value
(not real code ofc)
its called "freeze" because LLVM has an instruction called freeze which does exactly this
more generally, this works on any type where every bit pattern is valid
now the UB
in LLVM, data races are permitted: when two stores race, you store an LLVM undef value (which is similar to a rust uninit byte); when a load races with a store, you load an LLVM undef value
oh llvm has a real byte type now
instead of a weird and not used for good reason hack of <i1 x N>
ffffffff800041e4: 48 83 e2 fc and rdx,0xfffffffffffffffc
ffffffff800041e8: 31 c9 xor ecx,ecx
ffffffff800041ea: 66 0f 1f 44 00 00 nop WORD PTR [rax+rax*1+0x0]
ffffffff800041f0: 44 0f b6 04 0e movzx r8d,BYTE PTR [rsi+rcx*1]
ffffffff800041f5: 44 88 04 0f mov BYTE PTR [rdi+rcx*1],r8b
ffffffff800041f9: 44 0f b6 44 0e 01 movzx r8d,BYTE PTR [rsi+rcx*1+0x1]
ffffffff800041ff: 44 88 44 0f 01 mov BYTE PTR [rdi+rcx*1+0x1],r8b
ffffffff80004204: 44 0f b6 44 0e 02 movzx r8d,BYTE PTR [rsi+rcx*1+0x2]
ffffffff8000420a: 44 88 44 0f 02 mov BYTE PTR [rdi+rcx*1+0x2],r8b
ffffffff8000420f: 44 0f b6 44 0e 03 movzx r8d,BYTE PTR [rsi+rcx*1+0x3]
ffffffff80004215: 44 88 44 0f 03 mov BYTE PTR [rdi+rcx*1+0x3],r8b
ffffffff8000421a: 48 83 c1 04 add rcx,0x4
ffffffff8000421e: 48 39 ca cmp rdx,rcx
ffffffff80004221: 75 cd jne ffffffff800041f0 <atomic_src_memcpy+0x20>
ffffffff80004223: 48 85 c0 test rax,rax
ffffffff80004226: 74 17 je ffffffff8000423f <atomic_src_memcpy+0x6f>
ffffffff80004228: 48 01 cf add rdi,rcx
ffffffff8000422b: 48 01 ce add rsi,rcx
ffffffff8000422e: 31 c9 xor ecx,ecx
ffffffff80004230: 0f b6 14 0e movzx edx,BYTE PTR [rsi+rcx*1]
ffffffff80004234: 88 14 0f mov BYTE PTR [rdi+rcx*1],dl
ffffffff80004237: 48 ff c1 inc rcx
ffffffff8000423a: 48 39 c8 cmp rax,rcx
ffffffff8000423d: 75 f1 jne ffffffff80004230 <atomic_src_memcpy+0x60>
ffffffff8000423f: c3 ret
It did this at O3
if i force noinline
eugh
what is it even doing there
ah
im too retarded to understand why this helps
it helps in combination with the guarantee that loads which race load undef bytes
you know at the end if you had a race or not
so if you know you didnt, then you know you can safely read all of the bytes
actually you dont even need by value freeze lmao me dumb
well you need it in the managarm case you dont need it in the ringbuffer case
I think memcpy + asm volatile ("" : : : "memory") should be enough in the Managarm case, isn't it?
since that forces llvm to assume that the shared buffer has now changed in unpredictable ways
i mean it would make llvm not able to miscompile
but i'd also give it the pointer because i think that makes it also not llvm UB
(pointer to the copied buffer)
i guess that forces a "real" copy instead of reading into registers
reading into registers is fine though
can I do that too?
instead of atomic memcpy
i mean if you are fine with a little dose of technically kinda UB yes
well
its only a small dose, its not llvm ub
complain if gcc produces correct code that breaks you because "standards shit just do the """obvious""" thing dumbass"
lol
i mean if your standard makes it impossible to write a custom allocator it is kinda shit
you can you just cant write a good one without writing insane code
also you can do it in c++
(not that the C++ features that let you work properly afaict)
what did u have to do to make it ub free again