Friday, August 29, 2025

MacOS VM hints

Useful obscure projects for MacOS virtualization: 

  • UTM (https://mac.getutm.app/)
  • QuickEmu (https://github.com/quickemu-project)
  • Tart (https://tart.run/) 

Nothing works perfectly, except (I've heard) Parallels, which I haven't yet tried.

VMWare fusion sucks. Virtualbox sucks more.

UTM is quite nice. QuickEmu and Tart lack a GUI.

Tart supplies images with xcode preinstalled, so you won't need to use the app store. 

Tips from QuickEmu (haven't tried them in others):

 sudo trimforce enable

Enables SSD TRIM commands on the guest, helping qemu to shrink the QCOW image. Comes complete with a scary screenful of warnings that can be safely ignored, we're in a VM dude. Might not always work, google it.

MacOS VMs will usually be blocked from logging in with Apple IDs by default, due to the Serial, MAC Address, and other information being shared between multiple VMs.

To fix this, you can use this tool to replace or randomize these values in the bootloader.

 https://github.com/quickemu-project/qe_mac_apid

If you see "Your device or computer could not be verified" when you try to login to the App Store, make sure that your wired ethernet device is en0. Use ifconfig in a terminal to verify this.

If the wired ethernet device is not en0, then then go to System Preferences -> Network, delete all the network devices and apply the changes. Next, open a terminal and run the following:

sudo rm /Library/Preferences/SystemConfiguration/NetworkInterfaces.plist

Now reboot, and the App Store should work.

Tips from UTM: 

How to resize MacOS VM image (from https://github.com/utmapp/UTM/issues/4186#issuecomment-2163220345):

If you want to be able to increase the size of your disk image while retaining the ability to upgrade your OS, the Apple_APFS_Recovery partition must be relocated to the end of the disk before expanding the main Apple_APFS container.

So far the only ready-made solution I have found is the Tart packer plugin from the Tart project by cirruslabs which allows for automated VM creation and has the option to automatically resize disk images (see the recovery_partition = "relocate" option).

More information on the projects can be found below:
https://github.com/cirruslabs/tart
https://github.com/cirruslabs/packer-plugin-tart/tree/main

Since I wanted to continue using UTM and simply needed a standalone tool to relocate the partition, I created a small tool based on their code. Understand that it come with no guarantee, but the code is available here:
https://gist.github.com/cdavidc/3509ceefba518f20d1c123b6435407d6

(fully copied below because these things tend to get lost)

// Code taken from https://github.com/cirruslabs/packer-plugin-tart/blob/main/builder/tart/recoverypartition/relocate.go
// to create a standalone tool to relocate Apple_APFS_Recovery partitions to the end of a macOS disk image
// Build with:
// 	go mod init RelocatePartition.go
// 	go mod tidy
// 	go build RelocatePartition.go
// Run with:
// 	./RelocatePartition path_to_disk_image

package main

import (
	"fmt"
	"github.com/diskfs/go-diskfs"
	"github.com/diskfs/go-diskfs/partition/gpt"
	"github.com/samber/lo"
	"io"
	"os"
)

const Name = "RecoveryOSContainer"

func Relocate(diskImagePath string) error {
	// Open the disk image and read its partition table
	disk, err := diskfs.Open(diskImagePath)
	if err != nil {
		return fmt.Errorf("failed to open the disk image: %w", err)
	}

	partitionTable, err := disk.GetPartitionTable()
	if err != nil {
		return fmt.Errorf("failed to get the partition table: %w", err)
	}

	// We only support relocating a recovery partition on a GPT table
	gptTable, ok := partitionTable.(*gpt.Table)
	if !ok {
		return fmt.Errorf("expected a \"gpt\" partition table, got %q", partitionTable.Type())
	}

	// Find the recovery partition
	recoveryPartition, recoveryPartitionIndex, ok := lo.FindIndexOf(gptTable.Partitions, func(partition *gpt.Partition) bool {
		return partition.Name == Name
	})
	if !ok {
		fmt.Println("Nothing to relocate: no recovery partition found.")

		return nil
	}

	// We only support relocating the recovery partition if it's the last partition on disk
	if (recoveryPartitionIndex + 1) != len(gptTable.Partitions) {
		return fmt.Errorf("cannot relocate the recovery partition since it's not the last partition " +
			"on disk")
	}

	// Determine the last sector available for partitions, which is normally the (total LBA - 34) sector[1]
	//
	// [1]: https://commons.wikimedia.org/wiki/File:GUID_Partition_Table_Scheme.svg
	lastSectorAvailableForPartitions := uint64((disk.Size / disk.LogicalBlocksize) - 34)

	// Perhaps the recovery partition already resides at the last sector available for partitions?
	if recoveryPartition.End >= lastSectorAvailableForPartitions {
		fmt.Println("Nothing to relocate: recovery partition already ends at the last sector available to partitions.")

		return nil
	}

	fmt.Println("Dumping recovery partition contents...")

	tmpFile, err := os.CreateTemp("", "")
	if err != nil {
		return fmt.Errorf("failed to create a temporary file for storing "+
			"the recovery partition contents: %w", err)
	}
	defer os.Remove(tmpFile.Name())

	if err := dumpPartition(diskImagePath, tmpFile.Name(), recoveryPartition.GetStart(), recoveryPartition.GetSize()); err != nil {
		return fmt.Errorf("failed to dump the recovery partition contents: %w", err)
	}

	fmt.Println("Re-partitioning the disk to adjust the recovery partition bounds...")

	recoveryPartitionSizeInSectors := recoveryPartition.End - recoveryPartition.Start

	recoveryPartition.Start = lastSectorAvailableForPartitions - recoveryPartitionSizeInSectors
	recoveryPartition.End = lastSectorAvailableForPartitions

	// Re-partition the disk with the new recovery partition bounds
	if err := disk.Partition(gptTable); err != nil {
		return fmt.Errorf("failed to write the new partition table: %w", err)
	}

	fmt.Println("Restoring recovery partition contents...")

	if err := restorePartition(diskImagePath, tmpFile.Name(), recoveryPartition.GetStart(), recoveryPartition.GetSize()); err != nil {
		return fmt.Errorf("failed to restore the recovery partition contents: %w", err)
	}

	return nil
}

func dumpPartition(diskFilePath string, partitionFilePath string, off int64, n int64) error {
	diskFile, err := os.Open(diskFilePath)
	if err != nil {
		return err
	}
	defer diskFile.Close()

	partitionFile, err := os.Create(partitionFilePath)
	if err != nil {
		return err
	}

	partitionContentsReader := io.NewSectionReader(diskFile, off, n)

	if _, err := io.Copy(partitionFile, partitionContentsReader); err != nil {
		return err
	}

	return nil
}

func restorePartition(diskFilePath string, partitionFilePath string, off int64, n int64) error {
	diskFile, err := os.OpenFile(diskFilePath, os.O_RDWR, 0600)
	if err != nil {
		return err
	}

	contentsFile, err := os.Open(partitionFilePath)
	if err != nil {
		return err
	}
	defer contentsFile.Close()

	partitionContentsWriter := io.NewOffsetWriter(diskFile, off)

	if _, err := io.CopyN(partitionContentsWriter, contentsFile, n); err != nil {
		return err
	}

	return diskFile.Close()
}

func main() {
	if len(os.Args) < 2 {
		fmt.Println("Usage:", os.Args[0], "path_to_disk_image")
		os.Exit(1)
	}
	diskImageFilePath := os.Args[1]
	Relocate(diskImageFilePath)
}

 

To build it, install go (with brew for instance) and execute the few commands listed in the comments at the beginning of the file.

Considering that, the full instructions are:

  1. Locate the VM image in UTM by right-clicking on the VM and select "Show in Finder"
  2. Resize the image using hdiutil resize -size <new_size_in_GB>g -imageonly -verbose <full_path_and_name_of_the_image_file>
  3. cd to where you built the RelocatePartition tool and run ./RelocatePartition <full_path_and_name_of_the_image_file>
  4. Close and re-open UTM, so it re-reads the VM config, then start the VM in the recovery mode
  5. Use diskutil list to get the list of the containers (the first block in the output) and located the Apple_APFS container
  6. Resize the APFS container with diskutil apfs resizeContainer <id_of_the_Apple_APFS_container> 0
  7. Restart the VM

I also disable SIP (boot into recovery, there's an option in UTM to do that by right-clicking on the VM, open a terminal, csrutil disable) then remove all apple bundled bullshit (books, tv, etc), then re-enable SIP.

Conversion between raw image and qcow2: https://github.com/utmapp/UTM/discussions/3784 

 

 

  

 

 

 

 

Friday, August 15, 2025

Kids as Beta Testers: How an Orphanage Computer Lab Became My Personal QA Hell

Several years ago, I worked at an orphanage for a while (don't ask). My territory was the computer lab: a few battered desktops, some mismatched chairs, and a rotating cast of kids aged eight to thirteen.

The lab had official opening hours, and the rules were crystal clear: no computer use before or after the official opening hours.

And, of course, anyone who’s ever have to deal with children, knows this was a total fantasy.

Closing time was chaos. I’d shut down one workstation, turn around, and the one I’d just powered off would be booting back up. They wouldn’t leave the room, so I had to chase them out one at a time like a very underpaid nightclub bouncer. Management’s stance was simple: “It’s your responsibility.” Translation: Your circus, your monkeys.

But I was a software engineer. Surely I could automate this.

I wrote a small control program. From my own workstation - the “control server” - I would send a lockout signal to every machine. The lockout was a full-screen modal GTK window that required a password to dismiss.

I wanted to be nice, so I even added a 15-minute warning popup. In theory, it gave anyone doing real work time to save their files. In reality, the moment it appeared, the room erupted into a chorus of:

“Noooo! Not yet! Just one more level!”

But the system worked flawlessly… 

For exactly two days.

Then the kids discovered that if they disconnected the control server from the network by unplugging its CAT5 cable from the switch, the lockout command never reached the clients.

Problem solved - for them. For me, I was baffled about why I didn't have network connectivity at random times always around closing time. Then I saw the disconnected cable. How smart.

I fixed this by making the workstations ping the control server every few seconds. If they didn’t get a response, they locked immediately. That ended the cable-pulling and things went back to normal…

...not for long.  

Next, someone found a magic key combination that bypassed the modal screen entirely. I still don’t remember how - probably some obscure GTK / X11 quirk buried in some man page that no adult has ever the patience to fully read. I also don't remember how I patched this, but I did. 

The modal was now ironclad, but the whole system wasn't. 

The next attack vector was a clever application of social engineering. I naively used the same password for every machine. One kid would lure me over with a convincing excuse -“Sir, my screen is frozen”, while another watched me type the password over my shoulder. Once they had a partial idea of the password, they brute-forced the rest. Eventually, every machine was unlocked again.

Then I introduced a primitive one-time pad. Each workstation got a unique, random password at lockout, used only once. Shoulder surfing was now useless.

At last, peace. Closing time came, machines locked in unison, and the kids reluctantly shuffled out without trying to reroute Ethernet cables under the desks.

Not long after, my stint at the orphanage ended, and I moved on. But I left with one lesson I’ve never been able to use again:

If you need QA testers, use kids.

They will find—and exploit—every single edge case, loophole, and design flaw you didn’t even know existed.

Not because they’re trying to break your software, but because they’re just trying to win at something they perceive as an unfair (from their own perspective) game.

And that’s far more dangerous.