Excorporate and OAuth 2.0

I recently released Excorporate 1.1.0 to GNU ELPA.  Excorporate allows Emacs users to retrieve calendar entries directly from an Exchange server such as Office 365, without the need for external programs.

The latest release adds experimental OAuth 2.0 support, via a new library I wrote and published to GNU ELPA, called url-http-oauth. With Excorporate 1.1.0, I can access Office 365 again.  A while ago, the server to which I connect had disabled password-based authentication — including application-specific passwords.

I haven’t heard any success reports from users yet, so I wanted to mention the update on my blog. Soon I’ll write a followup post about my thoughts on OAuth 2.0 from a client implementer perspective.

Pixel phones are sold with bootloader unlocking disabled

Request to Google: ungrey the “OEM unlocking” toggle in the factory, before shipping store.google.com devices to customers. Do not make your customers connect the device to the Internet before they are allowed to install the operating system they want.

My wife had a requirement to use Android1, and she wanted to run GrapheneOS; I experimented with other devices and ROMs to ensure the specific application she needed would run on GrapheneOS.

As part of my research, I read the GrapheneOS installation guide2, which stated:

Enabling OEM unlocking

OEM unlocking needs to be enabled from within the operating system.

Enable the developer options menu by going to Settings > About phone and repeatedly pressing the build number menu entry until developer mode is enabled.

Next, go to Settings > System > Developer options and toggle on the ‘OEM unlocking’ setting. On device model variants (SKUs) which support being sold as locked devices by carriers, enabling ‘OEM unlocking’ requires internet access so that the stock OS can check if the device was sold as locked by a carrier.”

None of the many many YouTube videos I watched about bootloader unlocking covered whether or not you need Internet connectivity. Nor did any of Google’s official documentation3. GrapheneOS documentation is the only place on the Internet that documents this requirement, so, well done GrapheneOS documentation team!

GrapheneOS only supports recent Google Pixel phones. Those phones are nice hardware4, and I can easily (so I thought) install a different operating system, so I decided to buy one. To be as future-proof as possible, I bought a Pixel 7 Pro from store.google.com (Canada).

I thought (based on the aforementioned GrapheneOS docs) that the device model variant I bought, being sold “unlocked”7 by Google, would not need the Internet connection. NOPE; Google sold it to me with “OEM unlocking” greyed out:

The Pixel 7 Pro Developer options settings screen, showing the OEM unlocking slider greyed out, with the label Connect to the internet or contact your carrier

I consider this a customer-hostile practice. I should not have to connect a piece of hardware to the Internet, even once, to use all of its features. If I hadn’t connected the Pixel 7 Pro to the Internet, then “OEM unlocking” would have stayed greyed out, thus I would not have been able to unlock the bootloader, thus I would not have been able to install GrapheneOS5.

Keep in mind that I bought this phone full price6 from store.google.com, where it was advertised right in the FAQ as an “unlocked smartphone”7. There is zero carrier involvement here, so carriers cannot be blamed for this policy. Also, I paid full price for the phone, so this is not a case of “if you don’t pay for the product, you ARE the product”.

I probably should have returned the device for a refund. Instead, I set up a network debugging environment to see what activity happens when I connect the Pixel 7 Pro to the Internet.

By tailing some log files and watching them closely, I was able to determine that the final site accessed just before “OEM unlocking” goes from greyed to ungreyed is “afwprovisioning-pa.googleapis.com“. Here is the video of “OEM unlocking” ungreying:

Here is the rest of the network activity, all of which is TLS-encrypted by keys buried in the stock Google operating system, and thus not controlled by the device purchaser:

Hostname Downloaded to phone Uploaded from phone
storage.googleapis.com 383 MiB 8 MiB
fonts.gstatic.com 137 MiB 3 MiB
afwprovisioning-pa.googleapis.com 18 MiB 1 MiB
www.gstatic.com 8 MiB 287 kiB
googlehosted.l.googleusercontent.com 8 MiB 345 kiB
ota-cache1.googlezip.net 3 MiB 175 kiB
dl.google.com 3 MiB 86 kiB
instantmessaging-pa.googleapis.com 1 MiB 300 kiB
www.google.com 46 kiB 24 kiB
ssl.gstatic.com 25 kiB 3 kiB
ota.googlezip.net 17 kiB 6 kiB
digitalassetlinks.googleapis.com 17 kiB 4 kiB
clients.l.google.com 14 kiB 7 kiB
gstatic.com 13 kiB 3 kiB
mobile-gtalk.l.google.com 8 kiB 1 kiB
mobile.l.google.com 5 kiB 1 kiB
lpa.ds.gsma.com 5 kiB 4 kiB
connectivitycheck.gstatic.com 3 kiB 3 kiB
app-measurement.com 1 kiB 0 bytes
time.android.com 180 bytes 180 bytes

Only Google knows precisely what all that data is and what it is used for.

As the video shows, the ungreying did happen; I had the Settings application open, then connected the phone to the Internet. I had to close then re-open the Settings application; the access to “afwprovisioning-pa.googleapis.com” seemed to be co-timed with the Settings application restart. After the Settings appliation restart, the “OEM unlocking” option was operable.

I don’t know what subset of the hosts in the above table need to be accessible to the phone for ungreying to take place; I considered firewalling each individually using a script, but I ran out of time. I also don’t know if a factory reset of the phone results in “OEM unlocking” being greyed again. I ended my experimentation when the ungreying took place and I proceeded to install GrapheneOS successfully (the rest of the process was very straightforward, thanks to GrapheneOS’s great documentation and installation scripts).

All in all, cheers to Google for releasing Android as Free and Open Source software, and for selling devices which are (with steps) bootloader-unlockable; both of which make GrapheneOS feasible8. Jeers to Google for selling devices from store.google.com that cannot have their bootloaders unlocked without first connecting them to the Internet.


  1. One day I hope we can both use PinePhones. ^
  2. https://grapheneos.org/install/cli#enabling-oem-unlocking ^
  3. https://source.android.com/docs/core/architecture/bootloader/locking_unlocking

    “Devices should deny the fastboot flashing unlock command unless the get_unlock_ability is set to 1. If set to 0, the user needs to boot to the home screen, open the Settings > System > Developer options menu and enable the OEM unlocking option (which sets the get_unlock_ability to 1). After setting, this mode persists across reboots and factory data resets.” ^

  4. Google Pixel devices lack several features of my PinePhone; luxuries such as a 3.5mm audio jack, a swappable battery, a microSD card slot, and HDMI output (with a hardware mod). ^
  5. The “lock”/”unlock” terminology is hopelessly overloaded; as a result, confusion abounds online, even among phone enthusiasts. The “OEM” term here is also at best confusing and at worst misleading. I hope the screenshot and video make clear the specific context of this post, but here are definitions, and the states of the device I’m discussing:
    • “greyed” => the user interface element is inoperable
    • “ungreyed” => the user interface element is operable
    • “OEM unlocking” toggle is greyed (this is the state of the device after unboxing and before letting it have an Internet connection)
    • “OEM unlocking” toggle is ungreyed (the device must be connected to the Internet for this ungreying to take place (see video))
    • “OEM unlocking” toggle is ungreyed and toggled to “disabled”
    • “OEM unlocking” toggle is ungreyed and toggled to “enabled”
    • “OEM unlocking” toggle is ungreyed and toggled to “enabled” and bootloader is locked
    • “OEM unlocking” toggle is ungreyed and toggled to “enabled” and bootloader is unlocked (this is the state required to install GrapheneOS)

    At this point I don’t care about SIM unlocking or carrier unlocking or any other type of unlocking. There are plenty of horror stories on forums of people having purchased new Pixel phones from carriers at full price and then, via this same mechanism, the carrier never allowing bootloader unlocking (while apparently allowing various forms of SIM and carrier unlocking which are useless for running alternate operating systems like GrapheneOS). ^

  6. With a Black Friday discount. ^
  7. https://store.google.com/product/pixel_7_pro

    “Frequently asked questions

    What is an unlocked smartphone?

    An unlocked smartphone is a phone that isn’t tied to a specific carrier. When you purchase an unlocked Google Pixel phone, you get to choose which carrier or plan works best for you. Most phones in the Google Store come unlocked. Important: Google Pixel phones work with all major carriers. But not all Google Pixel 4a (5G) and later phones have 5G functionality on all 5G networks. See a list of certified carriers to make sure your smartphone works on its 5G network.

    To use a SIM-unlocked phone:

    1. Buy an unlocked Google Pixel phone from the Google Store.
    2. Contact a mobile carrier.
    3. Follow their instructions to set up your phone with their service plan.
    4. For 5G, some carriers may require a 5G plan (sold separately). Contact carrier for details. See g.co/pixel/networkinfofor info.” ^ ^
  8. Other major phone vendors and operating systems are not in this blog’s Overton window. ^

Printing an A4 document on US letter paper using Debian

My wife bought a dress pattern on Etsy that she received as a PDF. It was a large pattern that spanned many pages, meant to be trimmed and taped together into a large continuous sheet. The pattern was labelled “A4-letter“, meaning that it should be printable on A4 or letter paper. For accuracy, the pattern had to be printed at its native scale without cropping. The document contained a calibration box that was intended by the designer to be 5cm x 5cm exactly; I measured it with a ruler after each test print. The pattern’s internal tiles had four alignment chevrons, one on each side, which I could measure to ensure they had not been cropped. Getting the document printed perfectly turned out to be a puzzle, so I thought I’d publish what worked for me.

evince‘s “Properties” dialog showed the document’s native dimensions as “Paper Size:”, “A4, Portrait (8.27 × 11.69 inch)“, but on hand, I only had US letter 8.5 x 11 inch printer paper. I tried printing from several PDF viewers: xpdf 3.04, evince 3.38.2, and Firefox 111.0.1. I did test prints with many settings combinations in the different viewers. All my naive attempts failed in various ways, either scaling the document or cropping the margins. I won’t document all the specific failure modes, I’ll just skip to the method that worked, which combined an external tool and evince.

To change the document’s built-in margins, I used pdfcrop 2020/06/06 v1.40, part of Debian’s texlive-extra-utils 2020.20210202-3 package. First, I removed the document’s built-in margins:

$ pdfcrop pattern_A4-letter.pdf 
PDFCROP 1.40, 2020/06/06 - Copyright (c) 2002-2020 by Heiko Oberdiek, Oberdiek Package Support Group.
==> 23 pages written on `pattern_A4-letter-crop.pdf'.

The cropped document’s right and bottom edges fit on the page, but now the left and top edges spilled off. The final step was to extend the left and top margins of the cropped document by some number of units (arrived at through test-page experimentation), such that no spillover occurred on any side:

$ pdfcrop pattern_A4-letter-crop.pdf --margins "50 20 0 0"
PDFCROP 1.40, 2020/06/06 - Copyright (c) 2002-2020 by Heiko Oberdiek, Oberdiek Package Support Group.
==> 23 pages written on `pattern_A4-letter-crop-crop.pdf'.

In evince, in the “Print” dialog, on the “Page Setup” tab, I set “Scale:” to “100.0” and “Paper size:” to “A4“. On the “Page Handling” tab, I set “Page Scaling:” to “None” and left “Select page size using document page size” unchecked. All of the above steps proved necessary and sufficient to print the pattern at the document-native scale with each tile’s extents fully on the page.

llama.cpp and POWER9

This is a follow-up to my prior post about whisper.cpp. Georgi Gerganov has adapted his GGML framework to run the recently-circulating LLaMA weights. The PPC64 optimizations I made for whisper.cpp seem to carry over directly; after updating my Talos II’s PyTorch installation, I was able to get llama.cpp generating text from a prompt — completely offline — using the LLaMA 7B model.

$ ./main -m ./models/7B/ggml-model-q4_0.bin -t 32 -n 128 -p "Hello world in Common Lisp"
main: seed = 1678578687
llama_model_load: loading model from './models/7B/ggml-model-q4_0.bin' - please wait ...
llama_model_load: n_vocab = 32000
llama_model_load: n_ctx   = 512
llama_model_load: n_embd  = 4096
llama_model_load: n_mult  = 256
llama_model_load: n_head  = 32
llama_model_load: n_layer = 32
llama_model_load: n_rot   = 128
llama_model_load: f16     = 2
llama_model_load: n_ff    = 11008
llama_model_load: n_parts = 1
llama_model_load: ggml ctx size = 4529.34 MB
llama_model_load: memory_size =   512.00 MB, n_mem = 16384
llama_model_load: loading model part 1/1 from './models/7B/ggml-model-q4_0.bin'
llama_model_load: .................................... done
llama_model_load: model size =  4017.27 MB / num tensors = 291

main: prompt: 'Hello world in Common Lisp'
main: number of tokens in prompt = 7
     1 -> ''
 10994 -> 'Hello'
  3186 -> ' world'
   297 -> ' in'
 13103 -> ' Common'
 15285 -> ' Lis'
 29886 -> 'p'

sampling parameters: temp = 0.800000, top_k = 40, top_p = 0.950000

Hello world in Common Lisp!
We are going to learn the very basics of Common Lisp, an open source lisp implementation, which is a descendant of Lisp1.
Common Lisp is the de facto standard lisp implementation of Mozilla Labs, who are using it to create modern and productive lisps for Firefox.
We are going to start by having a look at its implementation of S-Expressions, which are at the core of how Common Lisp implements its lisp features.
Then, we will explore its other features such as I/O, Common Lisp has a really nice and modern I

main: mem per token = 14828340 bytes
main:     load time =  1009.64 ms
main:   sample time =   334.95 ms
main:  predict time = 86867.07 ms / 648.26 ms per token
main:    total time = 90653.54 ms

The above example was just the first thing I tried; no tuning or prompt engineering — as Georgi mentioned in his README, don’t judge the model by the above output; this was just a quick test. The text is printed as soon as each token prediction is made, at a rate of about one word per second, which makes the generation interesting to watch.

whisper.cpp and POWER9

I saw whisper.cpp mentioned on Hacker News and I was intrigued. whisper.cpp takes an audio file as input, transcribes speech, and prints the output to the terminal. For some time I wanted to see how machine learning projects performed on my POWER9 workstation, and how hard they would be to get running. whisper.cpp had several properties that were interesting to me.

First, it is freely licensed, released under the MIT license and it uses the OpenAI Whisper model whose weights are also released under the MIT license. Second, whisper.cpp is a very compact C/C++ project with no framework dependencies. Finally, after the code and the model are downloaded, whisper.cpp runs completely offline, so it is inherently privacy-respecting.

There was one tiny build issue, but otherwise, it just built and ran on PPC64. I was expecting to need dependent libraries and so forth, but the code was extremely portable. However, I knew it was running much slower than it could. A clue: the minor build failure was due to a missing architecture-specific header for vector intrinsics (immintrin.h) that wasn’t available for ppc64le Debian.

I took the opportunity to learn PPC64 vector intrinsics. Thanks to the OpenPOWER initiative, freely-licensed, high-quality documentation was readily downloadable from https://openpowerfoundation.org (no registration, paywalls, click-throughs, JS requirements, etc.).

I did an initial implementation for POWER9 using the IBM Vector-Scalar Extension (VSX) and the transcription speed improved considerably; for the base model, the example transcription ran in about one tenth the time. Meanwhile, the upstream project had re-organized its intrinsics support, so I reorganized my implementation to fit in. This was trickier than I expected, because of how FP32/short packing and unpacking worked in VSX.

Here is a graph of the results:

A Bar Graph;
Title: whisper.cpp;
Subtitle: PPC64 Performance Improvements;
Subsubtitle: ./extra/bench-all.sh 32; 77226aa vs 3b010f9;
Y Axis Label: Encoding Duration (seconds);
X Axis Label: Whisper Model;
Data Format: Model: Pre-VSX, Post-VSX;
Bar Data Follow:;
tiny:    14.606,  1.283;
base:    33.438,  2.786;
small:  110.570,  8.534;
medium: 311.653, 22.282;
large:  692.425, 41.106;

For the sake of completeness (and for my friends on #talos-workstation) I also added big endian support and confirmed that the example ran on my PPC64BE virtual machine.

I’m sure more optimizations are possible. I may try OpenBLAS (CPU) and/or ROCm (GPU) acceleration later. So far everything is running on the CPU. But I’m glad that, at least for the inference side, the Whisper model can attain reasonable performance on owner-controlled hardware like the Talos II.

One potential downside of Whisper’s trained-model approach (vs other transcription approaches, like Julius) is that for downstream projects, the model is pretty much unfixable if it has an issue. I have run whisper.cpp on real world materials with excellent results, especially with the large model. But if there are bugs, I don’t think fixing them is possible without retraining the model, which at least for Whisper, seems beyond the means of individuals.

I would like to thank Matt Tegelberg for evaluating whisper.cpp’s results against real world audio and for proof-reading this post.

Thunderbird and OpenPGP

I recently helped some friends set up Thunderbird and OpenPGP; the combination is much more user-friendly now.

OpenPGP is end-to-end encryption for email. Each user generates a private and public key. Each user imports a copy of the other user’s public key in their Thunderbird setup (they can copy the keys onto a USB drive or even email them to each other). Then when they select the “Encrypt” button during message composition, Thunderbird does the rest: no one on the Internet can read the message body. (The message metadata, like the subject line and the fact that the users are emailing each other, is still visible to Internet mail server administrators.)

The OpenPGP + Thunderbird user experience in 2022 is quite straightforward! I was worried I would need to use add-ons and external programs, but nope, it’s all built-in, including keypair generation. Public key import/export via the key manager is simple. OpenPGP is also nicely integrated into the reading and composition interfaces, which clearly indicate message signing and encryption status. Nice work by the Thunderbird team!

Mastodon and HTML

Request to Mastodon instance operators: Provide a read-only anonymous HTML-only mode.

Update 2022-11-17: Mastodon supports RSS; try just tacking “.rss” onto the end of a Mastodon URL.  It doesn’t seem to work for comment threads, but it does work for main threads. For example

M-x gnus ENTER G R https://mastodon.social/@markmccaughrean.rss ENTER

will create a Gnus group containing the author’s Mastodon posts.  This is a nice workaround, though I do still hope logged-out HTML-only browsing will be possible again, post 4.x.  Thanks to the helpful people on the #mastodon IRC channel for the above suggestion.

I’ve been following some Mastodon instances for several months. In Emacs, I type:

ESCAPE x eww ENTER https://mastodon.ar.al/@aral ENTER

and, without any authentication requirement, I’m greeted with a read-only HTML view of the instance, for example:

Example toot in Mastodon v3.5.3 HTML-only mode.
Example toot in Mastodon v3.5.3 HTML-only mode.

This week I tried another instance

ESCAPE x eww ENTER https://mastodon.social/@markmccaughrean ENTER

and I am blocked by:

Mastodon v4.0.0rc1 "please enable JavaScript" message.
Mastodon v4.0.0rc1 “please enable JavaScript” message.

Is this a new default? I was surprised that read-only anonymous HTML-only mode (ala Twitter classic and Nitter) is not supported by all Mastodon instances.

uLisp on the SMART Response XE

The Lisp Badge mini computer has turned out to be quite useful and fun for little hardware hacking projects. Its designer, David Johnson-Davies, suggested that the SMART Response XE would make a good off-the-shelf uLisp computer, eliminating the need to build one from scratch.

I ordered a few SMART Response XEs from an auction site to see what was possible. I found Larry Bank‘s excellent Arduino library and fdufnews‘s schematics, which provided a great starting point. With guidance from David, I completed an initial uLisp port:

uLisp 4.1 running on the SMART Response XE

To load the code, I use an ISP programmer and a special PCB with POGO pins:

ISP POGO programming of the SMART Response XE

On Debian, I run:

make ispload

to load LispBadge.ino without a bootloader.

The SMART Response XE uses the ATmega128RFA1 microcontroller, which has a ZigBee IEEE 802.15.4 transceiver. David and I are discussing adding uLisp functions to make use of this capability.

Mezzano on Librebooted ThinkPads

I decided to try running Mezzano on real hardware. I figured my Librebooted ThinkPads would be good targets, since, thanks to Coreboot and the Linux kernel, I have reference source code for all the hardware.

On boot, these machines load Libreboot from SPI flash; included in this Libreboot image is GRUB, as a Coreboot payload.

Mezzano, on the other hand, uses the KBoot bootloader. I considered chainloading KBoot from GRUB, but I wondered if I could have GRUB load the Mezzano image directly, primarily to save a video mode switch.

I didn’t want to have to reflash the Libreboot payload on each modification (writing to SPI flash is slow and annoying to recover from if something goes wrong), so I tried building a GRUB module “out-of-tree” and loading it in the existing GRUB. Eventually I got this working, at which point I could load the module from a USB drive, allowing fast development iteration. (I realize out-of-tree modules are non-ideal so if there’s interest I may try to contribute this work to GRUB.)

The resulting GRUB module, mezzano.mod, is largely the KBoot Mezzano loader code, ported to use GRUB facilities for memory allocation, disk access, etc. It’s feature-complete, so I released it to Sourcehut. (I’ve only tested it on Libreboot GRUB, not GRUB loaded by other firmware implementations.)

Here’s a demo of loading Mezzano on two similar ThinkPads:

For ease of use, mezzano.mod supports directly loading the mezzano.image file generated by MBuild — instead of requiring that mezzano.image be dd‘d to a disk. It does so by skipping the KBoot partitions to find the Mezzano disk image. The T500 in the video is booted this way. Alternatively, mezzano.mod can load the Mezzano disk image from a device, as is done for the W500 in the video. Both methods look for the Mezzano image magic — first at byte 0 and, failing that, just after the KBoot partitions.

I added the set-i8042-bits argument because Coreboot does not set these legacy bits, yet Mezzano’s PS/2 keyboard and mouse drivers expect them; at this point Mezzano does not have a full ACPI device tree implementation.

Excorporate 1.0.0

I released Excorporate 1.0.0 recently and declared the API stable. I was careful not to break API compatibility throughout Excorporate’s development so the API version stays the same at “0”.

The project is now in a state where it does everything I want it to do, API-wise. The UI is still missing features like meeting creation, but I just call the required Elisp functions when I need to, referring to the “API Usage” section of the Info manual.

I think there’s a lot of potential to create nice user interface features with Excorporate’s API — like a scheduler that shows people’s availability with ASCII-art bars, usable on a TTY. The included Org, diary and calfw front-ends show real-world usage of the API. I hope people send patches for new user interface features and keybindings, and contribute new authentication methods. I’ll continue watching for bug reports.