{ "version": "https://jsonfeed.org/version/1.1", "title": "Val Packett's Blog", "home_page_url": "https://val.packett.cool/", "feed_url": "https://val.packett.cool/feed.json", "icon": "https://val.packett.cool/x/val.1.png", "favicon": "https://val.packett.cool/x/val.1.png", "authors": [ { "name": "Val Packett", "url": "https://val.packett.cool/", "avatar": "https://val.packett.cool/x/val.1.png" } ], "language": "en", "items": [ { "id": "https://val.packett.cool/blog/use-openat/", "url": "https://val.packett.cool/blog/use-openat/", "title": "path.join Considered Harmful, or openat() All The Things", "summary": "Say goodbye to path traversal attacks by using modern kernel facilities and get ready for the capabilities-secure future at the same time!", "content_html": "
Soooo… isn’t it absurd that we have this hierarchical and dynamic structure called the file system, and the way we use it is by pretty much always traversing it from the root, by path, constructing paths using string manipulation tricks to hopefully try to be safe against both attacks and accidents?
\nYes. Yes, it is. Very much absurd and ridiculous. Just think about it. And despair. Or actually, don’t despair; instead, I want YOU to start caring about this and to join the fight for changing it! As we’ll see, the system interfaces have actually been evolving to make this situation better.
\nIn this article we’ll learn the history of how Unix-like systems got a way to make stable references to filesystem subtrees and perform operations relative to those, explore the current state of filesystem APIs in various programming languages, and—hopefully—get motivated to improve that.
\nAs it is with many good things in this life, the *at
family of system calls was invented at Sun Microsystems.\nSpecifically back in the early 00s, when Solaris 10 has introduced the openat
/fchownat
/fstatat
/futimesat
/renameat
/unlinkat
/utimensat
calls as a method to avoid race conditions / time of check vs time of use issues.\nNamely, this kind:
#define STATE_DIR "/var/db/thing/"\nif (stat(STATE_DIR "ok", &sb) != 0) return false;\n// in between these calls some other process screws with what /var/db/thing means\n// e.g. overwriting what a symlink points to\nint db_fd = open(STATE_DIR "data", O_RDWR);\n// we ended up referring to one thing at time of check and another at time of use\n
\nBefore, it was only possible to avoid these by changing the current working directory and using relative paths from there (!), which was quite an ugly hack, especially bad because the working directory is per-process state, so it wasn’t even thread-safe. That’s actually kinda really funny, isn’t it? “You can have ONE stable reference to a directory, as a treat,” said Unix.
\nThe solution they came up with was using directory file descriptors and adding operations relative to them, hence *at
:
int state_dir = open("/var/db/thing", O_DIRECTORY /* checks that it is a dir */);\nif (fstatat(state_dir, "ok", &sb, 0) != 0) return false;\nint db_fd = openat(state_dir, "data", O_RDWR);\n
\nFrom there, it has found its way into Linux, initially as userspace-only procfs trickery but finally available as a set of real system calls in version 2.6.16 from 2006.\nThen the POSIX.1-2008 standard has included these *at
calls and they were picked up by all the BSDs: first DragonFly and FreeBSD around 2008-09, OpenBSD a bit later in 2011, and the holdouts were NetBSD until late 2014 and macOS until late 2015.
Suspiciously soon after these calls were added to FreeBSD, the Capsicum project at Cambridge came up with a way to build capability-based security for Unix.\nFreeBSD was the main target for prototyping, and the work got upstreamed for 9.0 and enabled by default in 10.0.\nThere also was a Linux port sponsored by Google, with an implementation of the consumer side in Chromium, but all that sadly didn’t go anywhere.
\nSo, what is the idea there, anyway?
\nTurns out, exactly these *at
calls are at the core of it.\nYou see, while the original use case for kernel-level handles to directories was race condition avoidance, the researchers realized the great sandboxing potential of them.\nThe capability model has turned out to map very well onto the POSIX API: file descriptors are already unforgeable handles that indicate access to a resource.\nThey can be passed between processes, inherited by child processes… and openat
is exactly how you go from a more-privileged filesystem capability (e.g. a descriptor to /etc
signifying access to the whole subtree under /etc
) to a less-privileged one (a descriptor to /etc/passwd
signifying access to only that file).
Well, provided you restrict it to actually only do the lookup beneath the directory and never escape it.\nCapability mode does exactly that, along with disabling all access to global namespaces, i.e. the ability to just open()
whatever by a global path, to reference a process by PID, that kind of thing.\nThe idea is that you open the directories and other resources you anticipate working with, and then sandbox yourself to the resources you have and those derived from them.\nFor example, roughly like this:
int web_root = open("/var/www/site", O_DIRECTORY);\nint tcp_sock = socket(PF_INET6, SOCK_STREAM, 0);\n// bind, listen etc. omitted\ncap_enter(); // also check the return code lol\n// here we can accept client connections and talk to them,\n// access files under /var/www/site, and nothing more\nwhile (fd = event_loop_thing_poll(pfds)) {\n if (fd == tcp_sock) {\n event_loop_thing_add(pfds, accept(tcp_sock, NULL, NULL));\n } else {\n char *requested_path = read_request(fd);\n int ffd = openat(web_root, requested_path, O_RDONLY);\n sendfile(ffd, fd, /* … */);\n close(ffd);\n }\n}\n
\nThere are additional features like fine-grained limits called “rights” that are inherited by everything derived from a capability and can only be reduced, never expanded—so like, you can have a directory handle so damn read-only that files opened below it using openat
could never be read-write—but that is the core idea.\nAs a result, we have one of the strongest process sandboxes out there, based on a principled thought-out model instead of arbitrary deny/allow lists.\nThe tradeoff is however that it’s not as easy to sandbox existing software into such a paradigm-shifting system, so e.g. OpenBSD gets to tout a bigger quick practical impact of their pledge+unveil system :)\nHowever some great research has been done since into mechanisms for retrofitting existing applications, and I’m sometimes trying to continue that line of reseach myself.
So quite a few years later, during the explosion of various “cloud services” (advanced ways of running your code on someone else’s computer), this idea was extended further.\nCloudABI (2016) was an attempt to define a sandboxed application format for “the cloud” based on a simple idea:\nwhat if we just have a new OS-neutral ABI that already starts in Capsicum capability mode and doesn’t include any syscalls inappropriate for that, i.e. ones that use global namespaces?
\nThe design was very clever.\nResources would be injected before program start by a launcher that would make that easy.\nThe system call interface was entirely vDSO-based, one of the implications of which was that it was possible to run CloudABI binaries—without the sandboxing—on unmodified Linux and macOS using the launcher, allowing for developers to get on board easily.
\nSadly, native (secure) support only got upstreamed to FreeBSD and nowhere else, and the project fizzled out due to lack of industry interest.\nNot without leaving a huge influence behind though!
\nAt the same time, WebAssembly was taking off as a solution for another level of sandboxing and abstraction, the machine level.\nBorn out of previous efforts to compile existing C/C++ projects (as big as game engines) to the browser, Wasm has quickly expanded into a huge array of other use cases as well, because of what it actually ended up being: the most lightweight and neutral low-level abstract machine.
\nCurrently it seems to be catching on in the “cloud” industry, which has been looking for alternatives to heavyweight full-OS virtualization as the security boundary.\nThat was helped by the “next big thing” being “edge” instead of “cloud”, which mostly seems to mean running customer code all across a worldwide CDN instead of in just a few huge datacenters.\nBut the hype was all around a secure abstract machine when in reality the need for a common secure ABI was arguably even bigger there – assuming your goal isn’t explicitly doing vendor lock-in by providing a custom interface instead of a standard one :D\n(Hm, would it have helped if CloudABI was instead named EdgeABI?)
\nAnyway, thankfully, what’s fulfilling the need for an ABI is WASI, the WebAssembly System Interface, which is… basically kinda sorta just a wasm32-cloudabi
target if you look at it! (Well, with the whole Component Model thing that’s only going to be one aspect of it but still.)\nThe WASI overview explicitly references CloudABI and Capsicum.\nAnd even the aforementioned research into Capsicumizing existing software in the form of libpreopen.\nIn a way, we have won after all! :)\nThe industry-hyped, Wasm-workgroup-blessed, by-all-compilers-supported ABI for POSIX-y applications is based on exactly these ideas.
Meanwhile, other Things have been Happening with the system call API design all this time, right in the normal Unix-like kernels of “the present” that we actually run on hardware.
\nWay back in 2011, Linux 2.6.39 introduced the O_PATH
flag which allows opening, well, “only the path”—getting a stable reference to an inode—without actually opening the contents. These descriptors can only be used with operations that don’t care about the contents, like the *at()
calls we’re discussing. In most cases, this is just an optimization: not opening what we don’t need opened. It’s fine to “fully” open a directory and then openat()
below. However the difference in semantics comes into play especially with symbolic links. To support these semantics as required by xdg-document-portal, FreeBSD got O_PATH
merged in 2021, a whole decade later.
But the most important feature making directory descriptors attractive? The ability to do strictly-beneath lookups, like all lookups are under Capsicum, but no matter if your whole process is in a sandbox mode or not. Having an explicit flag in the API enables the developer to just pass a reference to a directory and tell the kernel to open a path definitely under that subtree in the file system, without worrying about processing the path carefully to avoid escapes.
\nThis was first proposed for FreeBSD back in 2015 as O_BENEATH
and landed in late 2018 in the development branch. Then Linux 5.6 in 2020 shipped with the openat2() syscall that adds a bunch of controls over path resolution behavior, including the RESOLVE_BENEATH
flag implementing the Capsicum-style behavior. Almost immediately after FreeBSD has added O_RESOLVE_BENEATH
with the correct behavior since it was discovered that the original O_BENEATH
one actually wasn’t working as intended (oops); then the original O_BENEATH
was removed. For FreeBSD 14 there was also a fix landed to avoid the “/..
is /
” behavior when opening beneath a descriptor pointing to the root directory, to make the behavior equivalent to Linux, at the request of the author of a library that I’ve added FreeBSD support to which finally landed just recently, speaking of which…
So. We have arrived at the current point in time, where we have decent kernel support for holding references to directories in the file system and doing useful things with them, like safely opening files strictly beneath the directory. Now what’s needed the most is: adoption, adoption, adoption!
\nIn Rust, use the cap-std crate! It’s an excellent library that provides an appropriate high-level API for directory references:
\n// somewhere in initialization\nlet mut root = Dir::open_ambient_dir("/var/www/memes", ambient_authority()).unwrap();\n\n// fn do_the_work(/* user input */ name: &str)\nlet img = root.open(format!("out-{}.avif.tmp", name))?;\nlet log = root.open_with(\n format!("log-{}.txt", name),\n OpenOptions::new().create(true).write(true)\n)?;\nroot.rename(\n format!("out-{}.avif.tmp", name),\n root,\n format!("out-{}.avif", name)\n)?; // etc.\n
\nAnd it takes advantage of modern Linux and FreeBSD system call API features to make this fast, while still supporting other platforms with a fallback method (essentially doing a system call per path component which should honestly be fine in most cases).
\nIn other languages, I’m not yet aware of cap-std equivalents; please do start working on your own! Or sponsor me to work on one for your favorite language I guess! :)
\nThe basic idea of what such a library would be is this:
\nint open_beneath(int dirfd, const char *pathname, int flags, mode_t mode) {\n#ifdef __FreeBSD__\n\t// TODO: validate flags/mode to match Linux behavior\n\treturn openat(dirfd, pathname, flags | O_RESOLVE_BENEATH, mode);\n#elif defined(__linux__)\n\tstruct open_how how = {\n\t\t.flags = flags,\n\t\t.mode = mode,\n\t\t.resolve = RESOLVE_BENEATH,\n\t};\n\treturn (int)syscall(SYS_openat2, dirfd, pathname, &how, sizeof(how));\n#else\n#error "TODO: the whole fallback algorithm from cap-std"\n#endif\n}\n\n// same with other operations\n
\nFor young or evolving languages, advocate for this paradigm to be incorporated into the standard library or “blessed” external libraries! I’ve noticed some awareness of the aforementioned ideas in this space:
\nDir
type is an fd wrapper with *at
ops but sadly as of right now there’s no attempt at providing the RESOLVE_BENEATH
behavior;eio
has a sandboxed dir type that for now uses realpath
+ string trickery and doesn’t hold an fd, but the authors are aware that it probably should and RESOLVE_BENEATH
exists.It would be really awesome if we could push for a cap-std-like API to be the recommended default one everywhere.
", "date_published": "2024-01-08T00:00:00Z" }, { "id": "https://val.packett.cool/blog/tiddlypwa/", "url": "https://val.packett.cool/blog/tiddlypwa/", "title": "Introducing TiddlyPWA: putting TiddlyWiki on modern web app steroids", "summary": "Oops, I think I just turned the legendary personal wiki system into an offline-first, installable, encrypted, synchronized Progressive Web App", "content_html": "tl;dr: go play with the new thing there, report issues there and support me there :3 but please do read the full story below!
\nTiddlyWiki is quite a famous note-taking system.\nDescribing itself as “a non-linear personal web notebook”, it’s actually quite a bit more than that.\nThanks to a plugin system, it’s basically an app platform, and plenty of cool stuff has been made for it.
\nAnd it exists as a unique thing: a self-contained web application—a single HTML file with both the app and the content—that you can take around on a USB flash drive.\nOh wait… who even uses those anymore for anything other than OS installers and bringing files to a print shop?
\nSo of course, a bunch of storage solutions have sprung up.\nThere’s an official node.js based server mode where it becomes more like a traditional web app,\nthere are browser extensions and even modules inside of TW itself for saving the updated single file to a server,\nthere are mobile and desktop apps that try to combine that saving with some kind of offline support,\nthere are people using a file sync solution like Syncthing – I’ve even heard of some people using Syncthing with the server version,\nsyncing the .tid
files between devices and running the server in Termux on their phones to access them on mobile.\nOof. While I’m nerdy enough to be able to do that, I’m also enough of a spoiled basic consumer to know that that’s not the user experience I want.
What I want is something that works like an app. Fully working offline, syncing efficiently (not by POSTing a multi-megabyte HTML file), quickly and reliably when online.\nAnd with client-side encryption, because there’s just no reason to let the server side read the contents here.
\nThere has actually been one good attempt at bringing this kind of sync to TiddlyWiki: NoteSelf, which integrated PouchDB for storage.\nI liked the sound of it, but in practice it wasn’t up to my standards.\nIt’s a heavyweight modification of TiddlyWiki that doesn’t keep up with latest core updates, PouchDB/CouchDB feel a bit heavy themselves, there’s no encryption, and the offline support is just “run it from your hard drive”.
\nSo… this is where I come in.\nLast year, looking to try TiddlyWiki once again, looking through the options and getting frustrated with the aforementioned everything, one thing inspired me.\nI stumbled upon a basic IndexedDB plugin which made me realize that there is an API for storage backends inside TiddlyWiki.\nI realized that there’s no need for core modifications, that I could just start with this and—knowing what I know about the web platform—take it to the next level.\nMake a plugin that combines IndexedDB, encryption, a sync engine (with a custom super-lightweight server counterpart for it), and adds a Service Worker and a Web Manifest\nto turn TW into a Progressive Web App that works offline and installs on mobile with “add to home screen”.\nThat was a whole Vision. And I knew it had to be done. It just had to exist. So I got to work, putting aside my typical FreeBSD system-level work to get back to good old web dev.
\nNow, a whole year after that inspiring moment, having gone through huge personal life changes and a reverse putting-aside in favor of my other work again…\nit’s finally ready for the public to see.
\nSo.
\n\n\n\nIt’s still rough around the edges, but I’ve put a lot of effort into the setup experience, hiding all the complexity of the flexibility available with how the system is structured\nand presenting a very simple “happy path” where if you open the app from a sync server it’s already preconfigured to sync with that server and so on.
\nI’ve also tried to document it, though I have to say that detailing the security model was a lot easier than trying to explain the whole synchronization/server/content-versus-app thing.\nHopefully at least for those familiar with TiddlyWiki it’s not too hard to understand that there’s the “app wiki” with the core/plugins/themes and the separate encrypted content,\nand that the sync server can work with both (differently).
\nNow, to justify the whole existence of this blog post, let me also list some random fun facts and whatnot – everything that didn’t fit in the documentation itself :)
\nBecause of how browsers work, I’ve had to take extra care to make the storage work across multiple running tabs without corrupting anything.\nMoreover, I made it all explicitly work together well and it’s kind of a hidden feature now.\nThanks to a combination of BroadcastChannels and Web Locks, you can enjoy a multi-tab experience.\nBrowse the same wiki simultaneously in two tabs, the navigation will be independent but any changes will be visible everywhere.
\nThis whole argon2ian thing you might’ve seen was indeed created for TiddlyPWA!\nI’ve started out using PBKDF2 because it’s available in the Web Crypto API, but eventually decided to upgrade to a modern password hash rather than cranking out iteration counts.\nI wasn’t satisfied with the state of existing Argon2 WASM+JS wrappers, so I set out to make my own, code-golfed to the tiniest size possible.\nThe yak shaving stack got even deeper during that subproject.\nAlso, I have used the very new Compression Streams API to be able to bundle the Wasm binary as compressed while not having to bunlde a decompressor.\nAnd this has led to the creation of a very funny bug…
\n…so since that API was already used there I started using it to compress long-enough tiddler contents as well.\nStream APIs are kind of annoying when you don’t want to stream, so I went with the whole “a stream is an async iterator” thing.\nWhen I first gave TiddlyPWA to an external tester I got a report of not being able to save a particular tiddler.\nTurns out, that iterator thing is only available in Firefox as of right now.\nI accidentally made it so that tiddlers longer than 240 bytes would only work in Firefox. :D
\nAnother really funny bug is something I bugzilled here for Firefox Android.\nWhen using FlorisBoard, pressing “enter” would result in the password mask (bullet ••• characters) becoming the actual value of the input.\nThis is something that I have discovered while typing passwords into my TiddlyPWA password prompt!\nI also ran into an already reported prompt()
bug in Firefox on iOS.
The server was initially written with SQLite as it is now, but when Deno KV was announced I got excited and rewrote it to use KV, hoping to leverage Deno Deploy for an easy server hosting experience.\nI quickly ran into a 64KiB limit per entry when trying to do the whole “save the HTML page” thing, but I found kv-toolbox, a module that would do chunking for me.\nThen, after everything was ready, already with the aforementioned other person testing it, I discovered (and ranted about) the undocumented 10 mutations in a transaction limitation.\nI wasn’t about to give up on atomicity, so I just rewrote everything back to SQLite in a burst of anger and now I hold a bit of a grudge against Deno KV >_<\nEven though I recognize that, obviously, it’s just that my use case is absolutely not the intended one.
\nThis TypeScript thing that I referenced on fedi was actually for the SQLite part of the TiddlyPWA server.
\nThere was a very last minute backwards-incompatible change I suddenly managed to do right before release.
\nI probably could’ve tried to make it as a commercial product with a centrally hosted server, but that’s not what I made.\nTiddlyPWA is fully free (both “as in beer” and “as in freedom”) because I believe in software that empowers everyone to own their data and the software itself, that is never gonna give you up doesn’t die for business reasons.
Right now TiddlyPWA is still early in its development, still missing a lot of things, but hopefully is solid enough to make you check it out.\nIf you like what you see, please consider helping me continue working on it:
\nThanks!!
", "date_published": "2023-07-26T00:00:00Z" }, { "id": "https://val.packett.cool/blog/ergonice/", "url": "https://val.packett.cool/blog/ergonice/", "title": "Yet another keyboard post, or, introducing ErgoNICE", "summary": "I made a custom split mechanical keyboard! Because it's me, this involved things like contributing to a PCB design tool and discovering a ridiculously optimized way to read a keyboard matrix.", "content_html": "A whole decade ago (wow!) I read a blog post called “A Modern Space Cadet”\nand… well, started caring about keyboards.\nI’ve adopted quite a few of the software tricks right there and then: shifts-as-parens and\ncapslock-as-control-and-escape.
\nThe hardware part—the whole mechanical keyboard thing—took me a couple years to get to.\nI was only using a MacBook Air when the post came out, but eventually as I got a desktop,\nI had purchased my first mech which was a Leopold with original Cherry MX Brown switches.\nI’ve used it for several years, but eventually I found myself wanting more.\nSpecifically, clicky switches and the ergonomics of the split form factor.\nIn 2020 I purchased this AliExpress split keyboard from a manufacturer\ncalled “Spider Island”. I’ve ported QMK to it, because of course.\nIt felt like a nice upgrade at the time, though the clone MX Blue switches weren’t the highest quality (as in, a couple\nstarted having weird phantom actuations) and the SATA cable connection between the halves being flaky was annoying.
\nAround the end of 2021 I was looking at higher quality split options like the Dygma Raise\nand the ZSA Moonlander and… decided that I was too weird for either of them\nand would rather spend money on fabrication and parts for something fully custom.\nI think this video might have been the final inspiration.
\nSo, I started thinking about what I actually wanted and came up with this list of requirements:
\nAnd a list of extra “wants”:
\nSpoiler alert: I didn’t successfully implement all the experimental stuff :)\nBut I’m really happy with the result!
\n\nSo, here’s how we got there…
\nThe first thing you need to make a PCB-based keyboard is, well, being able to design PCBs!\nSo I watched a KiCad tutorial on YouTube to learn and…\ndid not start using KiCad. Because you see, I have a terminal case of hipster-ism :)\nSo I started to play around with the far less popular Horizon EDA instead and it was an absolute dream.\nWhat a beautiful, coherent, smart piece of software.
\nBefore actually going for the full keyboard, I’ve decided to make a proof-of-concept: a little dev board\nbasically containing what would be the brain of the keyboard (I picked the STM32L1 family because it was\nless affected by the chip shortage and, you know, power-saving sounds good) and pins.\nTiny boards are a lot cheaper to manufacture, so if something went wrong, it wouldn’t be that much of a loss.\nSo I designed it, uploaded it to JLCPCB, paid some money and… success! It worked perfectly!
\nI called this board Ferrispark as a reference to Ferris the Rust mascot\nand Digispark, the board whose form factor (18×29 mm) this one fits in.\nWith that little thingy in hand, it was possible to experiment with firmware stuff while waiting for the keyboard PCB to be manufactured and shipped.\nBut first…
\nUnlike “normal” keyboards that come with boring standard ANSI/ISO layouts, enthusiast keyboards are very diverse,\nand with a fully custom build possibilities are truly endless. One can try to get away with very few keys\nor make an unhinged meme board or whatever.\nBut I was after something practical, tailored to my habits, based on ergonomic innovations but not too different from standard layouts.
\nSo I opened everyone’s favorite non-FOSS (boo) layout editor web app, loaded the Ergodox preset,\nremoved all the unassigned keys surrounding the core clusters, and started adding keys that made sense to me.
\nThe first thing I added was two columns on the right side that kinda just bring back the punctuation as it is on ANSI.\nThis felt really important to me because it’s not just about keeping the habits regarding the {[<:;'">]}
stuff.\nSome non-Latin scripts such as Cyrillic have a lot of letters, so those two columns have actual letters on them when typing those.\nComing up with alternatives (like chords) for that seemed like more of a nightmare than just for punctuation.\nBy the way, I added these columns without shifting them down, so they ended up as a 3-wide ortholinear grid cluster on the right end of the layout —\nI quickly realized that it would be a good fit for a numpad on an alternate layer too!
Then I started adding modifiers and other miscellaneous keys. I’ve added shifts where my fingers expect them to be\n(since I use shifts-as-parentheses, the “just one shift key” idea from minimalist boards is really not for me).\nFor the thumb clusters, I’ve added the most important text actions—Space, Tab, Backspace, Return—as large keys.\nI’ve added a dedicated actual Compose key because I use one. Then I started noticing that the key count was climbing up.\nDangerously close to a funny number, even. So I’ve added four extra “macro” keys, some really extra stuff like “menu”,\nand ended up with exactly 69 keys. That instantly solved the hard problem of naming things: the board was called “ErgoNICE” from that point on! :)\nHere’s how the layout looks:
\n\n(This is the physical layout, so you see QWERTY here. I actually type on Colemak though!)
\nBefore actually designing the PCB, I started looking at existing keyboard designs (nearly all in KiCad), importing various parts\ninto Horizon, purchasing extra components like the rotary volume knob and headphone jacks on AliExpress and modeling them in Horizon,\nand otherwise doing various preparations.
\nKeyboard PCBs aren’t rocket science. There are basically two ways to connect the keyswitches to the MCU: in a matrix\nand directly. With a compact split keyboard, direct is more feasible than ever, you don’t even need that huge of a microcontroller,\nso I actually saw that solution in one of the designs I was looking at. But my choice of MCU was somewhat limited by the chip shortage,\nand my board wasn’t that compact, and I wasn’t looking forward to routing all the direct connections with just 2 layers\n(which was the limit for non-green boards on JLCPCB and I just waaaanted a black one even though it won’t be visible),\nso the obvious decision was to go with a matrix with diodes on every switch.
\nThe other decision I made early on was that the left half would contain the microcontroller and would be assembled by JLCPCB with a ton of SMD parts,\nwhile the right one I would entirely hand-solder at home, using parts I already owned when possible.\nAnd that would be… through-hole diodes and the MCP23017-E/SP\ninput/output expander. Yeah, the DIP package variant. Because that’s what I purchased about 10 years ago (!) from\nan Arduino stuff store when I was first experimenting with electronics. How convenient!
\nSpeaking of diodes, something I saw and really liked was this footprint for either a surface-mount or through-hole diode.\nHowever you can’t just import that straightforwardly into Horizon. KiCad is fine with multiple pads/holes sharing a name, and\nwill just collapse them into one pad. Horizon is a lot more strict: each pad must have its own unique name, and if you want to construct something fancy like that,\nyou need to do it inside of a padstack. Thankfully, Horizon’s parametric padstack system is extremely capable. It’s based on a little\nstack-based scripting language for repositioning everything based on whatever logic you want. However it was missing the ability\nto reposition holes from the script, so I’ve had to add it, and then:
\n\nSo. Anyway. Actual keyboard design time. You can view the full schematic as a PDF here.\nThere’s not that much to say: it contains the aforementioned matrix (well, two of them, left and right), the microcontroller and everything it requires,\nthe USB-C connector with correctly separated CC resistors,\nTRRS audio connectors, various pin connectors (debug header, extras like external buttons),\nthe rotary knob with required circuitry… oh, and a bunch of various protection. Even though I’ve noticed that various\nDIY keyboard designs don’t seem to do much of it, I was really into the idea of extra safety, so I put resistors on all the external-facing pins,\ntransistors for reverse polarity protection on power inputs, a USBLC6 IC on the USB lines, and so on.
\n\nLayout started with feeding the JSON output of the keyboard-layout-editor website to another website, Keyboard CAD Assistant.\nIt produces DXF files that are supposed to be used for cutting a plate on a CNC router, but I actually needed it for the PCB.\nI’ve imported the DXF into Horizon EDA, drew polygons with diagonals over each key square, and got precise centers of each key —\nexactly what’s necessary to position the keyswitches!
\nThen I drew the outlines of the halves, positioned all the other components, routed the tracks… everything as expected.\nRouting can be quite fun, especially when the EDA tool looks this nice (this is the “Rust” color scheme, ha):
\n\nAfter getting pretty confident that the board was correct (the design rules check in Horizon is pretty helpful!) I’ve sent it off to manufacturing.\nA couple weeks later, I received the exciting notification from the post office. The long-awaited package from China!
\nHere’s a comparison with the 3D preview in Horizon. The real thing always looks amazing!
\nIf you want to play around with the PCB design files in Horizon EDA yourself, it’s in the pcb-ergonice
directory in the repo.\nAnd in the release downloads, there is an export with Gerber files and BOM/CPL for assembly.\nDisclaimer: the revision 1 which is published there is slightly different from the revision 0 which I physically have built. Keep reading to see the bug that I fixed there!
Analog keys are a pretty fun thing for gaming, allowing you to move slowly in CS for example.\nI stumbled upon these people here that were trying to commercially sell add-on flex PCBs\nfor adding the capability to an otherwise normal keyboard and began wondering if it’s possible to just DIY it.\nIt seems to be a bit of a scary topic because they have a patent in some jurisdictions, but who would go after a non-commercial personal project?\nSo, their technique is just using a Texas Instruments inductance to digital converter with a PCB coil.\nLuckily, these chips were easily available on JLCPCB’s assembly service (though not cheap), so I just went ahead with the experiment.\nThat is, I designed a little “evaluation board”, suspiciously shaped to fit under the keyboard PCB’s WASD cluster and connect to it using 2.54mm headers :)
\nThe interesting part of the design process is of course the PCB coils. TI provides an online tool for generating them.\nIt supports some export… into a couple commercial EDA tools. But I found a way get the results into Horizon. Get this:\nexport as an EAGLE project, open that in KiCad, export the coil as SVG, clean it up in Inkscape (merge into one SVG path), export as DXF R12,\nand finally import into Horizon with a downscaling factor of 10000 because reasons. Oof, it’s there! But we can’t connect anything to it,\nbecause it’s just lines, not tracks. And somehow they’re not even all connected.
\nNaturally, this was an opportunity to dig into Horizon EDA’s codebase and add some new tools!\nThis was a very enjoyable experience, and with some quick feedback from the author of Horizon I split one of the tools into two, and here they are:\n“select connected lines”, “merge duplicate junctions”, and “lines to tracks”. With Horizon becoming this much better, the board was easy to make.\nThis is how it looked:
\nTo try it out, I wrote a Rust embedded-hal driver for the LDC1x1x chips and\nan embedded-hal implementation for FreeBSD userspace APIs\nso that I could test it directly on my PC with a CP2112 USB-to-I²C adapter, just piping the output from a demo program into\nploot.
\nWhat I’ve found is that while the stream of numbers was indeed correlated with how far the key was pushed down,\nit was not good. Whether I was catching the movement of the finger or the spring was pretty confusing, especially\nwhen under the actual keyboard PCB. The fact that the switches of my choice have a click bar might’ve been a negative impact,\nthe distance to the switch from behind of the PCB was probably a problem, and the large 4-layer coil probably wasn’t quite compensating\nfor that (or was it actually just bad?).
\nEither way, I couldn’t attach it to the keyboard because it turns out I’ve made a silly mistake in the schematic:\nI forgot to connect one of the columns to the microcontroller :D\nSo I ended up running a bodge wire to one of the holes intended for the LDC board:
\n\nThere are many ways to make a keyboard enclosure, but as I was into 3D printing, that question was already answered.\nHow convenient that a split keyboard fits well into the dimentions of an Ender 3 build plate!\nI’ve been using realthunder’s FreeCAD fork for modeling 3D printed parts,\nso that’s what I used for this one as well.\nOf course FreeCAD’s UI is clunky and its core is crashy,\nbut I’d rather not involve proprietary software in this project.\nHaving CAD files in an open format is that valuable to me.\nAnd before someone starts preaching code-CAD like cadquery to me:\nsorry, I love sketching with a mouse and hate school-style math too much :D
\nI started out with exporting a 3D model of the boards and components on them as a STEP file from Horizon and importing it into FreeCAD.\nIn the file, everything was cleanly separated out, i.e. each component was its own body. However due to format limitations\nevery instance of the same part (e.g. every switch) is its own independent body, which takes up a lot of space on disk.\nI wrote a Python script in FreeCAD that would take the currently selected bodies and replace all of them except the first one\nwith a link to the first one, removing the duplication of actual 3D model data. (Sadly I lost that script by now, but it was tiny.)
\nWith a model of the boards, it wasn’t too hard to make an enclosure around them. The enclosure has two main parts.\nThe plate is a flat extrusion that gets permanently attached between top of the PCB and the bottom halves of the switches.\nThe tray is the rest of the enclosure, attached to the plate with screws.\nIn addition there are tenting wedges that attach to the bottom of the tray, and the actual knob that goes on the rotary encoder.\nThis is how it all looks in FreeCAD:
\nHere’s a look into the tray from the above. I’ve added these supports underneath each key for extra rigidity.\nOtherwise it’s… not that remarkable?
\n\nI printed the parts on a heavily modified Ender 3 Pro out of dark gray sparkly PETG.\nIt turned out pretty well! Everything fit together easily, the tolerances for things like the space for the key switches\nwere exactly right, it looks very decent for a DIY object.\nAgain, this is how it looks:
\n\nIf you want to play around with the source model, it’s all of that XML in the case
directory\nstored in the repo, saved using the version-control-friendly “save as directory” functionality of realthunder’s FreeCAD.\nAnd in the release downloads, there’s an archive with STL and STEP exports.
Now that everything is put together physically, we need to put software on the tiny little computer that runs the keyboard\n(it’s computers all the way down!).
\nThe most common way to get some keyboard firmware going is to use a popular project like QMK (a classic\nthat started on AVR and added Arm later) or ZMK (popular with Bluetooth, based on a whole RTOS called Zephyr and\nconfigured with flattened device trees).\nBut of course, “common way” means it’s not what I’m going to do.
\nI like the Rust programming language quite a lot and it’s, like, good for embedded so of course I’m going to use it here.\nNaturally, I’m not the only one doing so: I was quite happy to discover that there was already a library called\nKeyberon for handling all the uhh… keyboarding.
\nNow, how does the Rust ecosystem for STM32 look like?\nUnlike the C world where you interact with one monolithic SDK (either the vendor-provided one or libopencm3)\nthere is a lot more code sharing and integration due to the magic of package management.\nThe “libopencm3” of Rust is spread over a variety of crates: the center of that vague “SDK” is\ninterface crates like embedded-hal and usb-device,\nand there are both drivers that use those interfaces and microcontroller support crates such as the stm32-rs ones that implement them.\nThere is however the “parallel ecosystems” thing too, but not because of vendors: the rather standalone world here is\nEmbassy, an async
embedded framework.
And in fact, because stm32-rs’s stm32l1xx-hal
is not actively maintained, I’ve considered using Embassy for the project.\nI played around with it, fixing some STM32L1 initialization code and stuff,\nbut ultimately ended up just forking stm32l1xx-hal
, adding USB support and I²C timeouts and of course fixing bugs.
With a reasonable HAL crate and Keyberon in hand, it’s pretty straightforward to put the pieces together.\nBut I didn’t do anything in the straightforward way because I can get kinda obsessed with efficiency :)
\nThe cool thing to do for efficiency in embedded development is to let hardware do things as much as possible instead of software.
\nThe first opportunity to leverage nice STM32 peripherals is of course the volume knob: the hardware timers have the\nability to read a rotary encoder instead of, well, counting time.\nAs long as you connect the encoder to a pair of pins that do correspond to a timer, which I did as\nI knew this beforehand, at the PCB design stage.\nSo in the code, grabbing the timer for this purpose is as simple as:
\nlet knob_cnt = cx.device.TIM2.qei((gpioa.pa0, gpioa.pa1), &mut rcc);\n
\nAnd when polling all the things, we simply check if the count has increased or decreased since the previous\ntime we did that, and by how much.\nDepending on that, we press-and-release a designated key position in the layout,\nwhere we place volume up / volume down keys on the main layer and other fun stuff on other layers\n(e.g. I made it so that Fn+knob is scroll up / scroll down, just for fun).
\nlet knob_now = cx.local.knob_cnt.count() as i16;\nlet knob_row = if knob_now > *cx.local.knob_last { 1 } else { 0 };\nfor _ in 0..(knob_now - *cx.local.knob_last).abs() / 2 {\n handle_event::spawn(layout::Event::Press(knob_row, (LCOLS + RCOLS) as u8)).unwrap();\n handle_event::spawn(layout::Event::Release(knob_row, (LCOLS + RCOLS) as u8)).unwrap();\n}\n*cx.local.knob_last = knob_now;\n
\nNow, how about something more advanced? But what is left there to automate?\nWell, of course, the actual reading of the keyboard matrix! It’s a common thing, there should be hardware solutions!\nAnd there are. There are ICs that do keyboard matrix scanning (usually I/O expanders with that functionality),\nthe Programmable I/O peripheral of the RP2040 might be promising for this application,\nof course cool things can be done with FPGAs (you can make a microcontroller with your own key scanner peripheral\nlike in the icekeeb project).\nBut it turns out our little STM32 already has a great tool for the job!
\nBecause… it can DMA between memory and banks of GPIO pins, triggered by a timer.\nWhen I was thinking about the DMA capabilities, I was wondering if someone has already done what I wanted to do and yes!\nThis 2016 blog post describes\nexactly how to do it. (With some bonus big-brain thoughts on key debouncing.)\nThis part of the post sounded somewhat worrying:
\n\n\nAllocate all row output pins on one GPIO port and all column input pins on another GPIO port
\n
as I have not done that on my PCB. But well, I quickly realized that I could extend this idea to work\nwith pins arbitrarily scattered across both of the two pin banks, just by using more timers.\nBut my job was slightly easier as only columns (outputs in my design) were in both banks, A and B,\nwhile rows (inputs) only were in bank B.
\nTo generate the bit patterns, I used a const fn
:
const fn gen_gpio<const N: usize>(pins: [i32; N]) -> [u32; N] {\n let mut result: [u32; N] = [0; N];\n let mut p = 0;\n while p < N {\n if pins[p] >= 0 { result[p] = 1 << pins[p]; }\n p += 1;\n }\n result\n}\n\nstatic mut IN_GPIOB: [[u32; LCOLS]; 2 * BOUNCES] = [[69; LCOLS]; 2 * BOUNCES];\nstatic OUT_GPIOA: [u32; LCOLS] = gen_gpio([-1, -1, -1, -1, -1, 8, -1]);\nstatic OUT_GPIOB: [u32; LCOLS] = gen_gpio([ 1, 12, 13, 14, 15, -1, 0]);\n
\nOkay, not that cool, it’s a rather verbose way to avoid writing 1 <<
everywhere, but I’m a big fan of\ncompile-time function execution.\nBut more to the point, the memory layout already explains how the DMA setup works.\nOn every tick, the next output configuration will be selected across both GPIO ports (activating the next column)\nand the next readout of the inputs (well, of all the inputs on bank B) will be put into memory.
For configuring the DMA engines, which don’t have friendly wrappers in the HAL crate,\nI wrote a macro\nwrapping the raw hardware register writes to make it look decent like so:
\ndma_chan! { dma1: // TIM4_CH1 -> DMA1_CH1\n cndtr1 [LCOLS] cmar1 [&OUT_GPIOB as *const _]\n cpar1 [gpiob_odr] ccr1 [mem2per nointr]\n};\ndma_chan! { dma1: // TIM3_CH3 -> DMA1_CH2\n cndtr2 [LCOLS] cmar2 [&OUT_GPIOA as *const _]\n cpar2 [gpioa_odr] ccr2 [mem2per nointr]\n};\ndma_chan! { dma1: // TIM3_UP -> DMA1_CH3\n cndtr3 [LCOLS * BOUNCES * 2] cmar3 [&mut IN_GPIOB as *mut _]\n cpar3 [gpiob_idr] ccr3 [per2mem intr]\n};\n
\nSo, the output channels just cycle through the very short 7-element arrays, enabling only one column at a time.\nThe input reading channel is the interesting one.\nThe * 2
in the array length is related to DMA double buffering.\nSee, the engine can interrupt the CPU both when it goes through the entire array and wraps around,\nand right in the middle of the array. This allows the CPU and the DMA engine to avoid stepping on each other’s feet:\non the half interrupt we read the first half of the memory, and on the full interrupt we read the second one.\nAnd yeah, each half contains a few readouts in a row, to be fed into a debouncer.\nThis is how it’s done, converting the raw GPIO readout bytes into a matrix for Keyberon:
static ROW_GPIOS: [u32; ROWS] = gen_gpio([4, 5, 6, 7, 2]);\nlet is_half = dma1.isr.read().htif3().bit();\nlet scans = unsafe {\n &IN_GPIOB[if is_half {\n 0..BOUNCES\n } else {\n BOUNCES..2 * BOUNCES\n }]\n};\nfor scan in scans {\n let mut mat = [[false; LCOLS]; ROWS];\n for (c, colscan) in scan.iter().enumerate() {\n for (r, rowmask) in ROW_GPIOS.iter().enumerate() {\n mat[r][c] = (colscan & rowmask) != 0;\n }\n }\n for ev in cx.local.debouncer_l.events(matrix::PressedKeys(mat)) {\n handle_event::spawn(ev.transform(left2global)).unwrap();\n }\n}\n
\nAnd… it works! Now, of course I haven’t measured any efficiency gains versus doing everything the basic software way :D\nBut having fun with the implementation was the point more than anything.
\nThe entire source of the firmware is in the fw
directory in the repo.
Of course the end result ended up less ambitious than the initial project.\nOut of the initial list of ideas, the analog input fell out at the PCB stage, while\nthe extra peripheral (trackball/etc.) support kinda fell out at the case design stage.\nThe “teletype mode” via the output-only serial port header only fell out here at the firmware stage though,\nwith potential to come back and add it someday.
\nHowever there’s another thing I was rather interested in but didn’t get to try in the firmware, and that is\nUSB Selective Suspend\nwhich should allow the microcontroller to go to sleep for idle periods.\nThis isn’t easy to accomplish, but it seems like resuming the USB connection after sleep on the STM32L1 series might be possible.
", "date_published": "2023-01-10T00:00:00Z" }, { "id": "https://val.packett.cool/blog/pixelbook/", "url": "https://val.packett.cool/blog/pixelbook/", "title": "FreeBSD and custom firmware on the Google Pixelbook", "summary": "A search for a new thin-and-light laptop, a journey through the Chromebook firmware trust architecture, some FreeBSD kernel development, and finally, something about actually customizing open source firmware.", "content_html": "Back in 2015, I jumped on the ThinkPad bandwagon by getting an X240 to run FreeBSD on.\nUnlike most people in the ThinkPad crowd, I actually liked the clickpad and didn’t use the trackpoint much.\nBut this summer I’ve decided that it was time for something newer.\nI wanted something…
\nThe Qualcomm aarch64 laptops were out because embedded GPU drivers like freedreno\n(and UFS storage drivers, and Qualcomm Wi-Fi…) are not ported to FreeBSD.\nAnd because Qualcomm firmware is very cursed.
\nSamsung’s RK3399 Chromebook\nor the new Pinebook Pro would’ve been awesome… if I were a Linux user.\nNo embedded GPU drivers on FreeBSD, again. No one has added the stuff needed for FDT/OFW attachment to LinuxKPI.\nIt’s rather tedious work, so we only support PCIe right now.\n(Can someone please make an ARM laptop with a PCIe GPU, say with an MXM slot?)
\nSo it’s still gonna be amd64 (x86) then.
\nI really liked the design of the Microsoft Surface Book, but the\niFixit score of 1 (one)\nand especially the Marvell Wi-Fi chip that doesn’t have a driver in FreeBSD are dealbreakers.
\nI was considering a ThinkPad X1 Carbon from an old generation - the one from the same year as the X230 is corebootable, so that’s fun.\nBut going back in processor generations just doesn’t feel great.\nI want something more efficient, not less!
\nAnd then I discovered the Pixelbook.\nOther than the big huge large bezels around the screen, I liked everything about it.\nThin aluminum design, a 3:2 HiDPI screen, rubber palm rests (why isn’t every laptop ever doing that?!),\nthe “convertibleness” (flip the screen around to turn it into… something rather big for a tablet, but it is useful actually),\na Wacom touchscreen that supports a pen, mostly reasonable hardware (Intel Wi-Fi),\nand that famous coreboot support (Chromebooks’ stock firmware is coreboot + depthcharge).
\nSo here it is, my new laptop, a Google Pixelbook.
\nThe write protect screw is kind of a meme.\nAll these years later, it’s That Thing everyone on various developer forums associates with Chromebooks.\nBut times have moved on.\nAs a reaction to glued devices and stuff, the Chrome firmware team has discovered a new innovative way of asserting\nphysical presence: sitting around for a few minutes, pressing the power button when asked.\nIs actually pretty clever though, it is more secure than… not doing that.
\nWait, what was that about?
\nLet’s go back to the beginning and look at firmware security in Chromebooks and other laptops.
\nThese devices are designed for the mass market first.\nYour average consumer trusts the vendor and (because they’ve read a lot of scary news) might be afraid of scary attackers\nout to install a stealthy rootkit right into their firmware.\nBusinesses are even more afraid of that, and they push for boot security on company laptops even more.\nThis is why Intel Boot Guard is a thing\nthat the vast majority of laptops have these days.\nIt’s a thing that makes sure only the vendor can update firmware.\nEvil rootkits are out. Unfortunately, the user is also out.
\nGoogle is not like most laptop vendors.
\nYes, Google is kind of a surveillance capitalism / advertising monster, but that’s not what I’m talking about here.\nLarge parts of Google are very much driven by FOSS enthusiasts.\nOr something. Anyway, the point is that Chromebooks are based on FOSS firmware and support\nuser control as much as possible.\n(Without compromising regular-user security, but turns out these are not conflicting goals and we can all be happy.)
\nInstead of Boot Guard, Google has its own way of securing the boot process.\nThe root of trust in modern (>=2017) devices is a special\nGoogle Security Chip, which in normal circumstances\nalso ensures that only Google firmware runs on the machine, but:
\nRW_LEGACY
slot right from Chrome OS, reboot, press a key\nand you’re booting that payload!Okay, okay, let’s go.\nI didn’t even want to write an introduction to Chromebooks but here we are.\nAnyway, while waiting for the debug cable to arrive, I’ve done a lot of work on FreeBSD,\nusing the first method above (RW_LEGACY
).
SeaBIOS does not have display output working in OSes that don’t specifically support the Coreboot framebuffer\n(OpenBSD does, FreeBSD doesn’t), and I really just hate legacy BIOS, so\nI’ve had to install a UEFI implementation into RW_LEGACY
since I didn’t have the cable yet.\nMy own EDK2 build did not work (now I see that it’s probably because it was a debug build and that has failing assertions).\nSo I’ve downloaded MrChromebox’s full ROM image, extracted the payload using cbfstool
\nand flashed that. Boom. Here we go, press Ctrl-L for UEFI. Nice. Let’s install FreeBSD.
The live USB booted fine. With the EFI framebuffer, an NVMe SSD and a PS/2 keyboard it was a working basic system.\nI’ve resized the Chrome OS data partition (Chrome OS recovers from that fine, without touching custom partitions),\nfound that there’s already an EFI system partition (with a GRUB2 setup to boot Chrome OS, which didn’t boot like that o_0),\ninstalled everything and went on with configuration and developing support for more hardware.
\n(note: I’m leaving out the desktop configuration part here, it’s mostly a development post; I use Wayfire as my display server if you’re curious.)
\nSo how’s the hardware?
\nWell, that was easy.\nThe Pixelbook has an Intel 7265.\nThe exact same wireless chip that was in my ThinkPad.\nSo, Wi-Fi works great with iwm
.
Bluetooth… if this was the newer 8265, would’ve already just worked :D
\nThese Intel devices present a “normal” ubt
USB Bluetooth adapter, except it only becomes normal if you upload firmware\ninto it, otherwise it’s kinda dead.\n(And in that dead state, it spews interrupts, raising the idle power consumption by preventing the system\nfrom going into package C7 state! So usbconfig -d 0.3 power_off
that stuff.)\nFreeBSD now has a firmware uploader for the 8260/8265, but it does not support the older protocol used by the 7260/7265.\nIt wouldn’t be that hard to add that, but no one has done it yet.
Google kept the keyboard as good old PS/2, which is great for ensuring that you can get started with a custom OS\nwith a for-sure working keyboard.
\nAbout the only interesting thing with the keyboard was the Google Assistant key, where the Win key usually is.\nIt was not recognized as anything at all.\nI used DTrace to detect the scancode without adding prints into the kernel and rebooting:
\ndtrace -n 'fbt::*scancode2key:entry { printf("[st %x] %x?\\n", *(int*)arg0, arg1); } \\\n fbt::*scancode2key:return { printf("%x\\n", arg1); }'\n
\nAnd wrote a patch to interpret it as a useful key\n(right meta, couldn’t think of anything better).
\nThe touchpad and touchscreen are HID-over-I²C, like on many other modern laptops.\nI don’t know why this cursed bus from the 80s is gaining popularity, but it is.\nAt least FreeBSD has a driver for Intel (Synopsys DesignWare really) I²C controllers.
\n(Meanwhile Apple MacBooks now use SPI for even the keyboard.\nFreeBSD has an Intel SPI driver but right now it only supports ACPI attachment for Atoms and such, not PCIe yet.)
\nThe even better news is that there is a nice HID-over-I²C driver in development as well.\n(note: the corresponding patch for configuring the devices via ACPI\nis pretty much a requirement, uncomment -DHAVE_ACPI_IICBUS
in the iichid makefile too to get that to work.\nAlso, upcoming Intel I²C improvement patch.)
The touchscreen started working with that driver instantly.
\nThe touchpad was… a lot more “fun”.\nThe I²C bus it was on would just appear dead.\nAfter some debugging, it turned out that the in-progress\niichid driver was sending a wrong extra out-of-spec command, which was causing Google’s touchpad firmware\nto throw up and lock up the whole bus.
\nBut hey, nice bug discovery, if any other device turns out to be as strict in accepting input, no one else would have that problem.
\nAnother touchpad thing: by default, you have to touch it with a lot of pressure.\nEasily fixed in libinput:
\n% cat /usr/local/etc/libinput/local-overrides.quirks\n[Eve touchpad]\nMatchUdevType=touchpad\nAttrPressureRange=12:6\n
\nThe touchscreen in the Pixelbook is made by Wacom, and supports stylus input like the usual Wacom tablets.\nFor USB ones, on FreeBSD you can just use webcamd
to run the Linux driver in userspace.\nCan’t exactly do that with I²C.
But! Thankfully, it exposes generic HID stylus reports, zero Wacom specifics required.\nI’ve been able to write a driver for that quite easily.\nNow it works. With pressure, tilt, the button, all the things :)
\nThis was another “fun” debugging experience.\nThe intel_backlight
console utility (which was still the thing to use on FreeBSD) did nothing.
I knew that the i915 driver on Chrome OS could adjust the brightness, so I made it work here too, and all it took is:
\ncompat.linuxkpi.i915_enable_dpcd_backlight="1"
in /boot/loader.conf
;Turns out there was a patch sent to Linux to add a “prefer DPCD” toggle,\nbut for some reason it was not merged.\nThe patch does not apply cleanly so I just did a simpler hack version:
\n--- i/drivers/gpu/drm/i915/intel_dp_aux_backlight.c\n+++ w/drivers/gpu/drm/i915/intel_dp_aux_backlight.c\n@@ -252,8 +252,12 @@ intel_dp_aux_display_control_capable(struct intel_connector *connector)\n * the panel can support backlight control over the aux channel\n */\n if (intel_dp->edp_dpcd[1] & DP_EDP_TCON_BACKLIGHT_ADJUSTMENT_CAP &&\n- (intel_dp->edp_dpcd[2] & DP_EDP_BACKLIGHT_BRIGHTNESS_AUX_SET_CAP) &&\n- !(intel_dp->edp_dpcd[2] & DP_EDP_BACKLIGHT_BRIGHTNESS_PWM_PIN_CAP)) {\n+ (intel_dp->edp_dpcd[2] & DP_EDP_BACKLIGHT_BRIGHTNESS_AUX_SET_CAP)\n+/* for Pixelbook (eve), simpler version of https://patchwork.kernel.org/patch/9618065/ */\n+#if 0\n+ && !(intel_dp->edp_dpcd[2] & DP_EDP_BACKLIGHT_BRIGHTNESS_PWM_PIN_CAP)\n+#endif\n+ ) {\n DRM_DEBUG_KMS("AUX Backlight Control Supported!\\n");\n return true;\n }\n
\nAnd with that, it works, with 65536 steps of brightness adjustment even.
\nThe Pixelbook uses regular old ACPI S3 sleep, not the fancy new S0ix thing,\nso that’s good.
\nOn every machine with a TPM though,\nyou have to tell the TPM to save state before suspending, otherwise you get a reset on resume.\nI already knew this because I’ve experienced that on the ThinkPad.
\nThe Google Security Chip runs an open-source TPM 2.0 implementation (fun fact, written by Microsoft)\nand it’s connected via… *drum roll* I²C. Big surprise (not).
\nFreeBSD already has TPM 2.0 support in the kernel, the\nuserspace tool stack was recently added to Ports as well.\nBut of course there was no support for connecting to the TPM over I²C, and especially not to the\nCr50 (GSC) TPM specifically. (it has quirks!)
\nI wrote a driver (WIP)\nhooking up the I²C transport (relies on the aforementioned ACPI-discovery-of-I²C patch).\nIt does not use the interrupt (I found it buggy: at first attachment, it fires continuously,\nand after a reattach it stops completely) and after attach (or after system resume) the first command\nerrors out, but that can be fixed and other than that, it works.\nResume is fixed, entropy can be harvested, it could be used for SSH keys too.
\nAnother thing with resume: I’ve had to build the kernel with nodevice sdhci
to prevent the\nIntel SD/MMC controller (which is not attached to anything here - I’ve heard that the 128GB model might be using eMMC\ninstead of NVMe but that’s unclear) from hanging for a couple minutes on resume.
At least on the stock firmware, the old-school Intel SpeedStep did not work because the driver could not\nfind some required ACPI nodes (perf or something).
\nForget that, the new Intel Speed Shift (which lets the CPU adjust frequency on its own) works nicely with the linked patch.
\nWhen the lid is flipped around, the keyboard is disabled (unless you turn the display brightness to zero,\nI’ve heard - which is fun because that means you can connect a montior and have a sort-of\ncomputer-in-a-keyboard look,\nlike retro computers) and the system gets a notification (Chrome OS reacts to that by enabling tablet mode).
\nLooking at the DSDT table in ACPI, it was quite obvious how to support that notification:
\nDevice (TBMC) {\n Name (_HID, "GOOG0006") // _HID: Hardware ID\n Name (_UID, One) // _UID: Unique ID\n Name (_DDN, "Tablet Motion Control") // _DDN: DOS Device Name\n Method (TBMC, 0, NotSerialized) {\n If ((RCTM () == One)) { Return (One) }\n Else { Return (Zero) }\n }\n}\n
\nOn Linux, this is exposed as an evdev device with switch events.\nI was able to replicate that quite easily.\nMy display server does not support doing anything with that yet, but I’d like to do something like\nenabling an on-screen keyboard to pop up automatically\nwhen tablet mode is active.
\nI generally leave it off because I don’t look at the keyboard, but this was\na fun and easy driver to write.
\nAlso obvious how it works when looking at ACPI:
\nDevice (KBLT) {\n Name (_HID, "GOOG0002") // _HID: Hardware ID\n Name (_UID, One) // _UID: Unique ID\n Method (KBQC, 0, NotSerialized) {\n Return (^^PCI0.LPCB.EC0.KBLV) /* \\_SB_.PCI0.LPCB.EC0_.KBLV */\n }\n Method (KBCM, 1, NotSerialized) {\n ^^PCI0.LPCB.EC0.KBLV = Arg0\n }\n}\n
\nThe debug cable presents serial consoles as bulk endpoints without any configuration capabilities.\nOn Linux, they are supported by the “simple” USB serial driver.
\nAdding the device to the “simple” FreeBSD driver ugensa
took some debugging.\nThe driver was clearing USB stalls when the port is opened.\nThat’s allowed by the USB spec and quite necessary on some devices.\nUnfortunately, the debug interface throws up when it sees that request.\nThe responsible code in the device has a /* Something we need to add support for? */
comment :D
UPD: A Chrome OS engineer has notified me that the workaround is no longer necessary since Cr50 firmware version 0.3.24!
\nThe only thing that’s unsupported is onboard audio.\nThe usual HDA controller only exposes the DisplayPort audio-through-the-monitor thing.\nThe speakers, mic and headphone jack are all connected to various codecs exposed via… yet again, I²C.\nI am not about to write the drivers for these codecs, since I’m not really interested in audio on laptops.
\nAfter the debug cable arrived, I’ve spent some time debugging the console-on-FreeBSD thing mentioned above,\nand then started messing with coreboot and TianoCore EDK2.
\nMy discoveries so far:
\nme_cleaner
needs to be run with -S -w MFS
.\nAs mentioned in the --help
, the MFS
partition contains PCIe related stuff.\nRemoving it causes the NVMe drive to detach soon after boot;UefiPayloadPkg
doesn’t support PS/2 keyboard\nand NVMe out of the box, but they’re very easy to add\n(hopefully someone would add them upstream after seeing my bug reports);UefiPayloadPkg
supports getting the framebuffer from coreboot very well;libgfxinit
- the nice FOSS, written-in-Ada, verified-with-SPARK implementation of Intel GPU\ninitialization and framebuffer configuration - supports Kaby Lake now!\n0x00
, libgfxinit
interprets that as the slowest speed and\nwe end up not having enough bandwidth for the high-res screen;HDMI-A-n
connectors on the GPU, so a passive HDMI-mDP dongle plugged into a mDP-TypeC dongle won’t work;If you’re not the kind of person who’s made happy by just the fact that some more code during the boot process\nof their laptop is now open and verified, and you just want things to work, you might not be as excited\nabout open source firmware development as I am.
\nBut you can do cool things with firmware that give you practical benefit.\nThe best example I’m seeing is better Hackintosh support.\nInstead of patching macOS to work on your machine, you could patch your machine to pretend to almost be a Mac:
\nIs this cool or what?
\nPixelbook, FreeBSD, coreboot, EDK2 good.
\nSeriously, I have no big words to say, other than just recommending this laptop to FOSS enthusiasts :)
", "date_published": "2019-10-06T00:00:00Z" }, { "id": "https://val.packett.cool/blog/vbox-to-hyper-v/", "url": "https://val.packett.cool/blog/vbox-to-hyper-v/", "title": "Moving VMs from VirtualBox to Client Hyper-V", "summary": "The year of Windows on the desktop? Taking Microsoft's hypervisor for a spin by setting up a modern FreeBSD VM.", "content_html": "I’ve decided to move the VMs on my desktop from VirtualBox to Microsoft Hyper-V.\nBecause reasons.
\nActually because I’ve upgraded my desktop to an AMD Ryzen CPU: first, AMD-V/SVM is not supported by the Intel HAXM thing from the Android SDK, so I wanted to try out Microsoft’s Hyper-V based Android “emulator” (VM configurator/runner thingy) instead.\nSecond, giving 16 virtual CPUs on an SMT 8-core to a FreeBSD guest in VirtualBox results in a weird performance issue.\n(Though giving 4 vCPUs to multiple VMs on a 4-core CPU worked fine.)\nThird, it’s Oracle VM VirtualBox and no one likes Oracle.
\n\nSo, here’s how you can do it as well.
\nYou need Windows 10 Pro, Enterprise or Education.\n(Or Windows Server, obviously.)\nJust enable it as a feature and restart.
\nAlternatively, installing the MS Android “emulator” automatically enables it.
\n(NOTE: older versions of FreeBSD apparently had some loader issue that prevented EFI boot in Hyper-V. Everything works for me on a recent build of 11-STABLE.)
\nIn VirtualBox, go to the Virtual Media Manager (Ctrl+D) and copy your disk as VHD
.\nIn the Hyper-V Manager, use the Edit Disk dialog to convert the VHD
to VHDX
.
If you haven’t done that yet, go to the Virtual Switch Manager and make a virtual switch (“External” is like bridge mode in VBox).
\nNow make a virtual machine.\nGeneration 2, no dynamic memory (FreeBSD doesn’t support that), select the virtual switch and the VHDX disk.
\nClick Connect and it should just work.
\nBy the way, it’s nice that you can always close the console window without powering off the VM, unlike in VirtualBox where you need a special “Detachable start”.
\nInterestingly, if you create the VM without a disk and attach the disk later, you won’t see “boot from hard drive” in the firmware / boot order settings.\nAnd there’s no add button! (WTF?)\nThe fix is to use PowerShell:
\n$vm = Get-VM "YOUR VM NAME"\nSet-VMFirmware $vm -FirstBootDevice (Get-VMHardDiskDrive $vm)\n
\nSpeaking of which, it’s nice to have a directly integrated PowerShell interface to all the things.\nMy little xvmmgr script was initially written for VirtualBox, and that required COM.
\nWell, a similar process, but use Generation 1.
\nClient Hyper-V has pleasantly surprised me.\nIt’s a very smooth experience: it looks like a Type 2 hypervisor even though it’s actually Type 1, it runs VMs without any performance issues… what else could you ask for?
\nWell, the downside is its lack of flexibility in terms of paravirtualized (MS calls them “synthetic” or something) vs emulated devices.
\nAll you get is the choice between two generations.\nGeneration 1 means legacy BIOS boot from an emulated IDE drive with emulated all the things plus optionally some paravirtualized devices like the NIC.\nGeneration 2 means EFI boot from a SCSI drive with paravirtualized everything.\nOh and the SCSI controller is also on the vmbus
.\nSo there’s no way to use EFI and SCSI with e.g. OpenBSD, you need full Hyper-V support for at least the disk and network to do that.\nThankfully Microsoft contributed that support to FreeBSD! :)
So, I bought a laptop to run FreeBSD.
\nI was going to get a C720 Chromebook, but I got a good deal for an X240. Yeah, yeah, a laptop from the preinstalled-insecure-adware company, whatever. Anyway, it’s a ThinkPad, so it feels very solid, has an excellent keyboard and good free software support.
\nSo, let’s get FreeBSD running!
\nI’ve replaced the stock HDD with an SSD, compiled the drm-i915-update-38 branch of FreeBSD on a different machine, wrote the memstick image to an old USB flash drive, booted it and installed FreeBSD on the ThinkPad.
\nUPDATE: that landed in head a long time ago, I think you can just pick up the latest release now.
\nThe first installation, with ZFS root + UFS /boot
, did not work because the EFI loader couldn’t load zfs.ko
. After reinstalling on UFS, the loader does load zfs.ko
… Oh well.
UPDATE: now ZFS root works out of the box.
\nGRUB 2 is also an option (and the option for using sysutils/beadm), but the recent “backspace 28 times to bypass boot passphrase” vulnerability really discouraged me from installing it. Of course, what are you even trying to protect with that passphrase, but ugh, GNU code “quality”.
\nThe usual laptop settings for /etc/rc.conf
:
powerd_enable="YES"\npowerd_flags="-a hiadaptive -b adaptive -i 75 -r 85 -p 500"\nperformance_cx_lowest="Cmax"\neconomy_cx_lowest="Cmax"\n
\nUPDATE: powerd++ is a better powerd!
\nAnd for /boot/loader.conf
:
hw.pci.do_power_nodriver=3\ndrm.i915.enable_rc6=7\nhw.snd.latency=7\nhint.pcm.0.buffersize=65536\nhint.pcm.1.buffersize=65536\nhint.pcm.2.buffersize=65536\nhw.snd.feeder_buffersize=65536\n
\nBattery life with the internal + big external battery: ~8 - 8.5 hours of mostly surfing the web with Firefox on Wi-Fi with 50% screen brightness. (Obviously, more hours without Firefox :D) I don’t know how some reviewers got 20 hours of Wi-Fi browsing on Windows. Linux users say it’s 6-7 hours or above 8 hours, so FreeBSD is not worse than Linux there. That’s good :-)
\nI couldn’t get suspend/resume to work. It does suspend but doesn’t resume (pressing the power button makes the fans spin, but the power button is still blinking).
\nBut putting the X240 into sleep mode for short breaks is not really necessary. With the huge battery and the ultra-low-power processor, just leaving it running for 15-30 minutes won’t drain the battery much.
\nOh, and the power consumption can be measured with Intel’s performance counters. Install sysutils/intel-pcm and run:
\n$ sudo kldload cpuctl\n$ sudo pcm.x\n
\nPower consumption of the CPU (and GPU, and everything else on the chip) when idle and running Xorg is around 3 Watt.
\nWorks. This laptop has Intel’s networking hardware, which is great news for free operating systems. Not that I like Intel (super evil Management Engine!!) but they do write open source drivers for Linux, and BSD developers port them to the BSDs.
\nThe Intel PRO/1000 Ethernet card is supported by the em
driver.
The Intel 7260 wireless card is supported by the iwm
driver.
Only 802.11a/b/g
is supported in iwm
for now (IIRC because the driver is imported from OpenBSD, and they’re still working on 802.11n
support).
Doesn’t work.
\nApparently, it’s this one.
\nIt’s not even connecting as a USB device:
\nusbd_req_re_enumerate: addr=1, set address failed! (USB_ERR_TIMEOUT, ignored)\nusbd_setup_device_desc: getting device descriptor at addr 1 failed, USB_ERR_TIMEOUT\nugen0.2: <Unknown> at usbus0 (disconnected)\nuhub_reattach_port: could not allocate new device\n
\nI never use Bluetooth on laptops, anyway.
\nWorks. Well, there’s a reason I’m using the drm-i915-update-38
branch ;-) This is not in a release yet — it’s not even in -CURRENT
! — so I’m not expecting perfect quality.
UPDATE: this was merged a long time ago. There’s a new drm-next
in the graphics team’s fork though, and it brings Skylake support, Wayland…
But it works fine with correct settings.
\nDo not load i915kms
in the boot loader!! The system won’t boot. Instead, use the kld_list
setting in /etc/rc.conf
to load the module later in the boot process.
When you load i915kms
, it will repeat this error for less than a second:
error: [drm:pid51453:intel_sbi_read] *ERROR* timeout waiting for SBI to complete read transaction\nerror: [drm:pid51453:intel_sbi_write] *ERROR* timeout waiting for SBI to complete write transaction\n
\nThat’s okay, it works anyway. Looks like this is not even Haswell specific.
\nSo, here’s the xorg.conf
part:
Section "Device"\n Option "AccelMethod" "sna"\n Option "TripleBuffer" "true"\n Option "HotPlug" "true"\n Option "TearFree" "false"\n Identifier "Card0"\n Driver "intel"\n BusID "PCI:0:2:0"\nEndSection\n
\nUPDATE: with drm-next
, the modesetting
driver with glamor
acceleration works!
Brightness adjustment works via both graphics/intel-backlight and . acpi_video
(sysctl hw.acpi.video.lcd0.brightness
)The brightness keys on the keyboard don’t work properly though. The fn key on F5 (lower brightness) just sets the brightness to maximum, F6 (raise brightness) does nothing. Here’s the error that’s shown when pressing the lower brightness key with drm.debug=3
in /boot/loader.conf
:
[drm:KMS:pid12:intel_panel_get_max_backlight] max backlight PWM = 852\n[drm:KMS:pid12:intel_panel_actually_set_backlight] set backlight PWM = 841\n[drm:pid12:intel_opregion_gse_intr] PWM freq is not supported\n\n
\nSo I’ve configured F5 and F6 (the real function keys, FnLock mode) to call intel_backlight
.
UPDATE: acpi_video
is the one incorrectly changing the brightness to max! Don’t load it. acpi_ibm
changes the brightness correctly!
HDMI output works with a Mini DisplayPort adapter. 1080p video playback on an HDMI TV using mpv
is smooth.
VAAPI video output and hardware accelerated decoding works. With mpv --vo=vaapi --hwdec=vaapi
, CPU usage is around 20% for a 1080p H.264 video (vs. 60% with software decoding), the fans stay silent. You’ll need to install multimedia/libva-intel-driver and multimedia/mpv from pkg, and rebuild multimedia/ffmpeg with the VAAPI option.
OpenCL on the Haswell GPU (powered by Beignet) doesn’t work yet. clinfo
shows:
Beignet: self-test failed: (3, 7, 5) + (5, 7, 3) returned (3, 7, 5)\n
\nUPDATE: OpenCL was fixed a long time ago.
\nWorks. The built-in Realtek ALC292 sound card just works. FreeBSD’s audio support is good.
\nThe internal microphone is recognized as a separate device:
\n$ cat /dev/sndstat\nInstalled devices:\npcm0: <Intel Haswell (HDMI/DP 8ch)> (play)\npcm1: <Realtek ALC292 (Analog 2.0+HP/2.0)> (play/rec) default\npcm2: <Realtek ALC292 (Internal Analog Mic)> (rec)\n
\nHDMI audio works too (sysctl hw.snd.default_unit
to switch the sound card; applications that play sound have to be restarted.)
Works. With webcamd
, of course. But I don’t need it, so I’ve disabled it in the BIOS Setup.
Doesn’t work.
\npciconf
detects it as:
none2@pci0:2:0:0: class=0xff0000 card=0x221417aa chip=0x522710ec rev=0x01 hdr=0x00\n vendor = 'Realtek Semiconductor Co., Ltd.'\n device = 'RTS5227 PCI Express Card Reader'\n
\nIt’s supported in OpenBSD with rtsx(4). FreeBSD bugs for this: 161719, 204521.
\nIt should be possible to use it with OpenBSD/NetBSD/Linux in a bhyve VM with PCI passthrough, like the Wi-Fi card before iwm was added. That would also be more secure (that’s what Qubes does for all the hardware.) But I don’t need to use SD cards on this laptop.
\nOh, this is the most interesting part. Well, it works, sure. But there are at least three ways of using them, none of which is perfect.
UPDATE: evdev synaptics landed in current!
\nFreeBSD includes moused
, a little daemon that watches the mouse device you tell it to watch and forwards all events to a virtualized mouse, which is accessible to Xorg at /dev/sysmouse
and also works on the text console. It has advanced support (like sensitivity settings) for Synaptics touchpads and ThinkPad TrackPoints (set hw.psm.synaptics_support=1
and hw.psm.trackpoint_support=1
in /boot/loader.conf
to enable).
Sadly, it forwards all events to a virtualized mouse. Not a trackpad, just a mouse, so the experience is not as good.
\nxorg.conf
:
Section "InputDevice"\n Identifier "SysMouse"\n Driver "mouse"\n Option "Device" "/dev/sysmouse"\nEndSection\n
\nThis is the Synaptics driver that provides a great trackpad experience. Inertial scrolling, horizontal scrolling, natural scrolling, perfectly smooth cursor movement… Everything is as good as in OS X on a MacBook.
\nBut the TrackPoint doesn’t work. On the X240, it’s attached to the trackpad as a guest mouse. The trackpad forwards the TrackPoint’s PS/2 events with a special mark (IIRC, it’s W = 3).
And the Synaptics driver stopped supporting guest devices in 2010.
\nUPDATE: added the guest mouse support back to xf86-input-synaptics! Also adds ClickPad support to the raw PS/2 protocol used by the driver on the BSDs (on Linux, it uses evdev, and they only added clickpad support there).
\nAlso, clicking doesn’t work, but I don’t care. I’m used to tapping.
xorg.conf
:
Section "InputDevice"\n Identifier "Touchpad"\n Driver "synaptics"\n Option "Protocol" "psm"\n Option "Device" "/dev/psm0"\n Option "VertEdgeScroll" "off"\n Option "VertTwoFingerScroll" "on"\n Option "HorizEdgeScroll" "off"\n Option "HorizTwoFingerScroll" "on"\n Option "VertScrollDelta" "-111"\n Option "HorizScrollDelta" "-111"\n Option "ClickPad" "on"\n Option "SoftButtonAreas" "4201 0 0 1950 2710 4200 0 1950"\n Option "AreaTopEdge" "5%"\nEndSection\n
\nI accidentally found evdevfbsd, a little program that exposes PS/2 devices as evdev
devices via CUSE (Character Device in Userspace).
evdev
is a protocol that comes from Linux. It allows kernel (or CUSE) drivers to provide a standardized interface for devices so that Xorg wouldn’t care about any particular vendor.
evdevfbsd
correctly separates the trackpad and the TrackPoint. Cursor movement works. But only cursor movement. No touch scrolling, no tapping, no clicking. And it looks like it might be xf86-input-evdev
’s fault, because evtest.py shows tap events when tapping!
I’ve tried to write a CUSE program that works as a proxy between /dev/psm0
and the Synaptics driver, extracting guest (TrackPoint) events in the process.
It almost works… the only problem is that the Synaptics driver locks the whole X server while reading from my proxy, so only mouse movement works and nothing else, not even Ctrl+Alt+F1 to switch to a console. Well, the power button works. And SSHing into the laptop.
Seems like the problem with CUSE is that there’s no way to find out, in the poll
method, that the process that polls your device wants to stop. So, when moused
reads from the proxy, it works, but when you stop moused
with Ctrl-C, it doesn’t stop until you touch the TrackPoint or the trackpad a little to send an event.
UPDATE: added the guest mouse support back to xf86-input-synaptics!
\nWorks. It’s recognized as a USB HID device at /dev/uhid0
. There are two ways to use it in Xorg.
The simple way: you can use it with the mouse
driver, as a regular mouse. Obviously, this does not provide multi-touch.
xorg.conf
:
Section "InputDevice"\n Identifier "Touchscreen"\n Driver "mouse"\n Option "Protocol" "usb"\n Option "Device" "/dev/uhid0"\nEndSection\n
\nThe other way: you can use it with webcamd
and the evdev
driver. This will actually support multi-touch.
Recompile x11-drivers/xf86-input-evdev from ports with the MULTITOUCH
option, start webcamd
like this (note that CUSE is part of the base system on 11-CURRENT
, so it’s not called cuse4bsd
anymore):
$ sudo make -C /usr/ports/x11-drivers/xf86-input-evdev config deinstall install clean\n$ sudo kldload cuse\n$ sudo webcamd -d ugen1.3 -N Touchscreen-ELAN -M 0\n
\nxorg.conf
:
Section "InputDevice"\n Identifier "Touchscreen"\n Driver "evdev"\n Option "Device" "/dev/input/event0"\nEndSection\n
\nStart chrome --touch-events
and visit the touch event test! Also, you can scroll in GTK+ 3 applications like Corebird.
Unfortunately, it’s really bad at detecting when a touch ends. This means that scrolling and tapping will get stuck. So I’m using the mouse
driver for now.
UPDATE: the new wmt(4)
kernel driver supports the touchscreen perfectly, without that issue!! Also, libinput
is better than evdev
in Xorg.
Works. (With the dedicated TPM 1.2 module. Haven’t tried Intel’s built-in TPM 2.0 support. The choice between them is in the BIOS/UEFI settings.)
\nOpenSSH works with a TPM key through simple-tpm-pk11.
\nUPDATE: turns out the TPM was preventing the laptop from waking up from suspend! (And I did unload the tpm module before suspend.) Disabled it in firmware settings.
\nIt’s possible to use a Haswell ThinkPad with FreeBSD right now :-) Everything except Bluetooth, SD cards and waking up from sleep works.
OpenBSD would be better though. They have excellent ThinkPad support, because OpenBSD developers use OpenBSD on ThinkPads. But I’m working on software that uses FreeBSD jails, and I just prefer FreeBSD.
", "date_published": "2016-01-03T00:00:00Z" } ] }