gamozolabs
https://raw.githubusercontent.com/MarginResearch/cannoli/mai...
You can see a massive improvement for shared hyperthreads, and on-CPU-socket messages.
In this case, about ~350 cycles local core, ~700 cycles remote core, ~90 cycles same hyperthread. Divide these by your clock rate as long as you're Skylake+ for the speed in seconds. Eg. about 87.5 nanoseconds for a 4 GHz processor for local core IPC.
See additional references for PDFs.
I’ve also played around with this in my research kernel, but never thoroughly enough to write up. Something I should revisit, I think it’d be a really fun thing to discuss and work on. Doing this level of timing requires doing BIOS configuration to make the CPU as deterministic as possible (turn off dynamic throttling, make sure you’re thermally keeping the CPU stable, etc).
I always highly encourage people to write advanced benchmarks. It’s a great way to learn how computers work!
For IPC, this effectively requires one thread writing to memory (thus, making that cache line modified to that core, and evicted from all other cores). Then, when the polling thread checks in on the line, it will have to demote that cache line from modified, to shared by flushing it out to memory (usually L2 or L3, but also writing out to memory). This causes some memory traffic, and constantly means that the cores are fighting over the same cache line. Since x86 is strongly ordered and caches are coherent, this traffic is extremely expensive. Think of a write as "tell all other cores that you modifed this memory, so they have to evict/invalidate their cache lines". And a read as "tell all cores to flush their cache lines if they're modified, then wait for them to tell me they're done, then I can read the memory". This effectively is a massively blocking operation. The simple act of reading the mailbox/ticket/whatever from another core to check if a message is ready will actually dramatically affect the speed the other core can write to it (as now that write is effectively full latency).
There are some tricks you can do to get extremely low latency between cores. One of them, is making sure you're on cores that are physically near each other (eg. on the same processor socket). This is only really relevant on servers, but it's a big thing. You can actually map out the physical processor layout, including on a single die, based on the latency between these cores. It's quite subtle and requires low noise, but it's really cool to map out the grid of cores on the actual silicon due to timing.
Another trick that you can do, is have both threads on the same core, thus, using hyperthreads. Hyperthreads share the same core and thus a lot of resources, and are able to actually skip some of the more expensive coherency traffic, as they share the same L1 cache (since L1 is per-core). The lowest latency you will be able to observe for IPC will be on the same core with hyperthreads, but that's often not really useful for _processing_ the data, since performance will not be great on two busy cores. But in theory, you can signal a hyperthread, the hyperthread can then go and raise some other signal, while the original hyperthread still continues doing some relevant work. As long as one of them is blocking/halted, the other won't really be affected by two things on the same thread.
Finally, the most reasonable trick, is making sure your tickets/buffers/mailboxes/whatever are _not_ sharing the same cache lines (unless they contain data which is passed at the same time). Once again, the CPU keeps things in sync at cache line levels. So having two pieces of data being hammered by two cores on the same cache line is asking for hundreds of cycles per trivial data access. This can be observed in an extreme case with many core systems, with multiple sockets, fighting over locks. I've done this on my 96C/192T system and I've been able to get single `lock inc [mem]` instructions to take over 15,000 cycles to complete. Which is unreal for a single instruction. But that's what happens when there's 200-500 cycles of overhead every single time that cache line is "stolen" back from other cores. So, effectively, keep in your head which state cache lines will be in. If they're going to be modified on one core, make sure they're not going to be read on another core while still being written. These transitions are expensive, you're only going to get your 3-4 cycle "random L1 data hit performance" if the cache line is being read, and it's in the exclusive, modified, or shared state, and if it's being written, it has to be exclusive or modified. Anything else and you're probably paying hundreds of cycles for the access, and thus, also probably hurting the other side.
Ultimately, what you're asking from the CPU is actually extremely complex. Think about how hard it would be for you to manage keeping a copy of a database in sync between hundreds of writers and reads (cores). The CPU is doing this automatically for you under the hood, on every single memory access. It is _not_ free. Thus, you really have to engineer around this problem, batch your operations, find a design that doesn't require as intense of IPC, etc. On more weakly ordered systems you can use some more tricks in page tables to get a bit more control over how cache coherency should be applied for various chunks of memory to get more explicit control.
This was specifically designed for one producer (the QEMU processor), and many consumers (processing the trace of instructions and memory accesses). I can't remember what specific tunings I did to help with this specific model. Data structures like this can always be tuned slightly based on your workload, by changing structure shapes, shared cache lines, etc.
With about 3-4 consumers QEMU was not really blocking on the reporting of every single instruction executed, which is really cool. This requires a low noise system, just having a browser open can almost half these numbers since there's just more cache coherency bus traffic occuring. If having a browser open doesn't affect your benchmark, it's probably not bottlenecking on the uarch yet ;)
https://github.com/MarginResearch/cannoli/blob/main/.assets/...
A little blog on Cannoli itself here: https://margin.re/2022/05/cannoli-the-fast-qemu-tracer/
Ultimately, mempipe is not really a big thing I've talked about, but it's actually what makes cannoli so good and enables the design in the first place.
I don't think they'd want it if it's specific to one target arch. Idk. In theory the hooks into QEMU are target agnostic, there's no assembly on the QEMU side, only in the Cannoli server side (shared library).
I had it working target agnostic if I did memory accesses from the JIT, but it "didn't work" when I tried to put my data in registers (eg. the only generic implementation I used caused 2 extra memory accesses per target instruction, which is brutal at these speeds)
One, is that having determinism makes it easier to triage bugs. If I find a crash while fuzzing, but there's no way to reproduce it, it's going to pretty much never get fixed (unless the call stack from the first crash is obvious enough to prepare a fix). For fuzzing systems, this is effectively mandatory. Imagine a memory allocation failure leading to a NULL-deref, the odds of hitting that same bug are so low because it requires going OOM at the same point in a subsequent execution. Snapshot fuzzing (each fuzz case starts from a snapshotted state of memory/registers/whatever) mitigates some of this, but there's still noise from context switches that will affect RNGs, which will affect heap layouts, which will affect everything on the system. That being said, most fuzzing out there publicly is not system's fuzzing, and thus this is typically not something people think about.
But two, is what most people use determinism for. In modern fuzzing, coverage guidance is pretty much mandatory. This means when new code is hit, to save off the input such that it can be built upon. At a very simple level, this means a problem which is 256^4, turns into a 256*4, as all requirements do not need to be satisfied simultaneously, as long as the previous requirements cause new code to get hit they can be built upon. Of course, if the program does not behave the same way every time, the noise can start to erode the value which is provided here, since you're not actually building upon the same thing.
I'm not sure if this answers the question.
But yes, I've done about 7 or 8 operating systems for fuzzing in the past and it's a massive performance (and cleanliness) cleanup. This one is going to be like an operating system I wrote 2-3 years ago for my vectorized emulation work.
To answer your QEMU questions, the goal is to effectively build QEMU with MUSL (just to make it static so I don't need a dynamic loader), and modify MUSL to turn all syscalls to `call` instructions. This means a "syscall" is just a call to another area, which will by my Rust Linux emulator. I'll implement the bare minimum syscalls (and enum variants to those syscalls) to get QEMU to work, nothing more. The goal is not to run Linux applications, but run a QEMU+MUSL combination which may be modified lightly if it means a lower emulation burden (eg. getting rid of threading in QEMU [if possible] so we can avoid fork())
The main point of this isn't performance, it's determinism, but that is a side effect. A normal syscall instruction involves a context switch to the kernel, potentially cr3 swaps depending on CPU mitigation configuration, and the same to return back. This can easily be hundreds of cycles. A `call` instruction to something that handles the syscall is on the order of 1-4 cycles.
While for syscalls this isn't a huge deal, it's even more emphasized when it comes to KVM hypercalls. Transitions to a hypervisor are very expensive, and in this case, the kernel, the hypervisor, and QEMU (eg. device emulation) will all be running at the same privilege level and there won't be a weird QEMU -> OS -> KVM -> other guest OS device -> KVM -> OS -> QEMU transition every device interaction.
But then again, it's mainly for determinism. By emulating Linux deterministically (eg. not providing entropy through times or other syscall returns), we can ensure that QEMU has no source of external entropy, and thus, will always do the same thing. Even if it uses a random-seeded hash table, the seed would be derived from syscalls, and thus, will be the same every time. This determinism means the guest always will do the same thing, to the instruction. Interrupts happen on the same instructions, context switches do, etc. This means any bug, regardless of how complex, will reproduce every time.
All of this syscall emulation + determinism I have also done before, in a tool called tkofuzz that I wrote for Microsoft. That used Linux emulation + Bochs, and it was written in userspace. This has proven incredibly successful and it's what most researchers are using at Microsoft now. That being said, Bochs is about 100x slower than native execution, and now that people have gotten a good hold of snapshot fuzzing (there's a steep learning curve), it's time to get a more performant implementation. With QEMU with get this with a JIT, which at least gets us a 2-5x improvement over Bochs while still "emulating", but even more value could be found if we get the KVM emulation working and can use a hypervisior. That being said, I do plan to support a "mode" where guests which do not touch devices (or more specifically, snapshots which are taken after device I/O has occurred) will be able to run without QEMU at all. We're really only using QEMU for device emulation + interrupt control, thus, if you take a snapshot to a function that just parses everything in one thread, without process IPC or device access (it's rare, when you "read" from a disk, you're likely just hitting OS RAM caches, and thus not devices), we can cut out all the "bloat" of QEMU and run in a very very thin hypervisor instead.
In fuzzing it's critical to have ways to quickly map and unmap memory as most fuzz cases last for hundreds of microseconds. This means after a few hundred microseconds, I want to restore all memory back to the state "before I handled user input" and continue again. This is extremely slow in every conventional operating system, and there's really no way around it. It's of course possible to make a driver or use CRIU, but these are still not exactly the solution that is needed here. I'd rather just make an OS that trivially runs in KVM/Hyper-V/Xen, and thus can run in a VM to get the cross-platform support, rather than writing a driver for every OS I plan to use this on.
Stay cute, ~gamozo
Fuzz Week 2020 – Come Learn the Basics to Advanced of Fuzzing
Nevertheless, I got unbanned and I still stream at: https://www.twitch.tv/gamozo I upload all the vods at: https://www.youtube.com/user/gamozolabs
Hope y'all enjoy the content <3
Kinda neat, but it's not a thing anymore.
Some more info here: https://stackoverflow.com/questions/14332848/intel-x86-0x2e-...
I'm sure we'll see more types of leaks, but unless they're actively fetching invalid data [1], there isn't much sensitive data to leak anymore.
I don't think there is much during speculation that can load _new_ data during that window.
[1]: So far almost every CPU bug has leaked something in an internal cache.
I'll be curious as to what there is to learn from this. It's more of a longshot goal for me to learn how things work, develop accurate uarch models, and then learn from those models better than I could guess and check hardware results.
Hard to say if it'll go well....
I used this kernel originally for my vectorized emulator, which is designed as a high-performance fuzzer/harness to find bugs (more info https://gamozolabs.github.io/fuzzing/2018/10/14/vectorized_e...). I used vectorized emulation on Windows DHCP to find multiple RCEs (which were disclosed earlier this year), as well as one of the Intel MDS vulnerabilities (such as RIDL and Fallout) disclosed earlier this year (specifically I found "MLPDS", https://nvd.nist.gov/vuln/detail/CVE-2018-12127).
I do most of my work in a personal kernel as it really gives me an edge with optimization. I'm able to use page tables directly (super fast fork()-like behavior), and write hypervisors that don't have to go through crazy call stacks to vmexit, use bleeding-edge CPU features, etc. Ultimately I just do it because it's fun, but I've found ways to justify it from time to time.
I'm happy enough with where I am in life, and I'd really like to just give back and teach others. I was fortunate enough to have a mentor at a very young age (and many other great teachers and leaders throughout life), and I recognize the huge advantage it gave me. If I can pass knowledge on to the next generation (and current generations of course), that's the best outcome I can hope for!
I have previous hypervisors and kernels I've written (in assembly, C, and Rust) [https://github.com/gamozolabs/falkervisor_beta and https://github.com/gamozolabs/falkervisor_grilled_cheese].
Memory corruption was my most common issue in these kernels, and when working with fuzzing and security research, my confidence in my own tooling got in the way of root causing bugs. I would find that sometimes bugs that I "detected" in the code I was fuzzing, was indeed due to corruption in my own code. This pushed me a bit towards finding a safer language to write my kernel in, not for security, but for code quality.
I was a pretty hardcore C fan and I never saw myself getting into a higher level language like Rust. However the cleanliness of the output code got me immediately hooked a few years ago. I do a lot of work on low-level development and optimizations, and having a compiler with predictable properties of emit code is really important to me (such that I can have a decent idea in my head what the emit code will be). Having allocators be scope based rather than garbage collected really helps with this, and helps with the usability of the language for kernel development.
Also you mentioned races as something Rust does not prevent, but it does prevent traditional "exploitable" race conditions, by enforcing that all types shared (passed via message passing, or via globals) must be "Sync", which means they must be proven safe to share between threads. Using atomics or wrapping things in Mutexes is one way to make things sharable. However, Rust does not prevent deadlocks, which are fairly common as just "bugs", especially in kernel development.
That being said, Rust has many things that I do not like, such as the clumsiness around working with generic arrays of >32 elements, and working with raw "plain old data". There's definitely a lot of research in modern Rust going into web assembly and other features that I have zero interest in personally, while some of the systems aspects can be a bit lacking. But, I am not personally contributing time to the Rust project, so I cannot complain too much. That being said, it is full featured enough to write bootloaders and kernels in, and I use it for all of my projects for the past few years.
TL;DR: Rust is fast, prevents many of the most common bugs (and many of the hardest to reproduce/fix bugs such as corruption/UaFs/etc), and has predictable codegen which is useful for optimization and systems development.
It's intended the kernel itself is multiprocessing (support for multiple cores, and multiple VMs). But currently the intent is only to run a single core per VM for now, as getting VT-x or SVM to be deterministic on a single core itself will be hard enough. If we accomplish this, then the next goal would be to maybe look at multiple cores.
It is important that I can run multiple single core VMs on a single machine as I plan to use this framework for fuzzing, and retrofitting multiprocessing to a kernel is always a source of many, many bugs.
Hope that clears it up a bit!
System level mutable time-travel debugging [video]
Vectorized Emulation: MMU Design
Finding 0day with the World's Worst Android Kernel Fuzzer
This is done however not due to a theoretical limitation of the tooling, but rather a practical one in that I haven't written SSE lifters and decoders yet. If I were to write these it would work just fine.