Rosettaはなぜ特定のVMM上の仮想マシンでないと使えないか、そしてlibkrunはどうやってそれを回避していたか
はじめに
Appleは、Linux用のRosettaバイナリを提供しています。
Rosettaを使うと、Apple Siliconを搭載するハードウェアのmacOSで稼働するaarch64 Linux仮想マシン上で、x86_64バイナリを実行できるようになります[1]。
Rosettaは、実行環境がVirtualization.framework(Apple純正のVMM用フレームワーク)を使った仮想環境でないと使えません。
ですが、VMMライブラリであるlibkrunは、Virtualization.frameworkを使っていないにもかかわらずRosettaを使うことができます(した) (実はこの機能はrevertされており、今はできません)。
本文書は、(1) RosettaがどうやってVMMをチェックしているのか、(2) libkrunはどうやって回避したのか、の2点についてまとめたものです。
前提知識
macOSは、仮想化を支援するフレームワークとしてHypervisor.framework、Virtualization.frameworkの2つを持っています。Hypervisor.frameworkは仮想化のコア機能を提供するライブラリで、Linuxにおけるkvm.koに相当します。Virtualization.frameworkはVMMを実現するためのライブラリで、LinuxにおけるQemuに相当します。
libkrunやHVFアクセラレーションを使ったQemuは、Hypervisor.frameworkを使っています。
Virtualization.frameworkを使っているツールとしては、UTMやLimaなどがあります。macOS上でPodmanを実行するときも、v5.0以降ではVirtualization.frameworkを使ったLinux仮想マシンを使います。
RosettaのVMMチェックの仕組み
「Quick look at Rosetta on Linux」というブログ記事によると、「rosettaを実行すると、rosettaバイナリ(/proc/self/exe
)に謎のioctl(2)を発行して、特定の文字列が返ってくること」を確認しているようです。
...で終わるのもナニなので、実際に確認してみます。
環境
- ホスト: Macbook Air M2, macOS, UTM
- ゲスト: Fedora 40
ori@myfedora:~/work$ sudo dmidecode | head -n 15
# dmidecode 3.6
Getting SMBIOS data from sysfs.
SMBIOS 3.3.0 present.
Table at 0x16FCB3000.
Handle 0x0000, DMI type 1, 27 bytes
System Information
Manufacturer: Apple Inc.
Product Name: Apple Virtualization Generic Platform
Version: 1
Serial Number: Virtualization-c4e505cf-ced1-493f-9d6f-f5e46434e9d0
UUID: cf05e5c4-d1ce-3f49-9d6f-f5e46434e9d0
Wake-up Type: Power Switch
SKU Number: Not Specified
Family: Not Specified
ori@myfedora:~/work$ uname -r
6.8.5-301.fc40.aarch64
Rosettaを使えるようにする
ori@myfedora:~/work$ sudo mount -t virtiofs rosetta /mnt/rosetta
ori@myfedora:~/work$ findmnt /mnt/rosetta
TARGET SOURCE FSTYPE OPTIONS
/mnt/rosetta rosetta virtiofs rw,relatime,seclabel
ori@myfedora:~/work$ ls -l /mnt/rosetta
total 436
-rwxr-xr-x. 1 ori ori 1660888 May 22 08:09 rosetta
-rwxr-xr-x. 1 ori ori 298680 May 22 08:09 rosettad
straceしてみる
ori@myfedora:~/work$ strace /mnt/rosetta/rosetta
execve("/mnt/rosetta/rosetta", ["/mnt/rosetta/rosetta"], 0xfffff242ddf0 /* 31 vars */) = 0
openat(AT_FDCWD, "/proc/self/exe", O_RDONLY) = 3
ioctl(3, _IOC(_IOC_READ, 0x61, 0x22, 0x45), 0xffffc32340e8) = 1
close(3) = 0
gettid() = 29760
awrite(2, "Usage: rosetta <x86_64 ELF to ru"..., 165Usage: rosetta <x86_64 ELF to run>
Optional environment variables:
ROSETTA_DEBUGSERVER_PORT wait for a debugger connection on given port
version: Rosetta-318.8
) = 165
exit(1) = ?
+++ exited with 1 +++
rosettaを実行すると、いきなり /proc/self/exe
に対して謎のioctl(2)を発行していることがわかります。
ところでioctl(2) の type = 0x61
('a'
) はどのようなデバイスなのでしょうか。Linuxカーネルのドキュメントによると、ATMやSonetと、Intel QuickAssist Technologyのカーネルモジュールで使われている番号のようです。Fedora 40のカーネルソースで雑にgrepすると、他にHDMI CECでも使われているようでした。
ori@myfedora:~/rpmbuild/BUILD/kernel-6.10.6/linux-6.10.6-200.fc40.aarch64$ grep -lEr "_IO.*\(('a'|0x61)" include drivers
include/linux/efi.h
include/uapi/linux/atm_eni.h
include/uapi/linux/atm_he.h
include/uapi/linux/atm_idt77105.h
include/uapi/linux/atm_nicstar.h
include/uapi/linux/atm_tcp.h
include/uapi/linux/atm_zatm.h
include/uapi/linux/atmarp.h
include/uapi/linux/atmclip.h
include/uapi/linux/atmdev.h
include/uapi/linux/atmlec.h
include/uapi/linux/atmmpc.h
include/uapi/linux/atmsvc.h
include/uapi/linux/cec.h
include/uapi/linux/sonet.h
ioctl(2)の発行
straceの出力と上記ブログ記事に載っているコードを参考に、下記のようなコードを作成します。
/* usage: `./a.out PATH_TO_ROSETTA_BINARY` */
/* see also: https://threedots.ovh/blog/2022/06/quick-look-at-rosetta-on-linux/ */
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <sys/ioctl.h>
#define KEY_SIZE 0x45
int main(int argc, char *argv[])
{
if (argc != 2) {
fprintf(stderr, "usage: %s path\n", argv[0]);
exit(1);
}
int fd = openat(AT_FDCWD, argv[1], 0);
if (fd < 0) {
perror("openat");
exit(1);
}
char key[KEY_SIZE];
int result = ioctl(fd, _IOC(_IOC_READ, 0x61, 0x22, KEY_SIZE), key);
if (result < 0) {
perror("ioctl");
exit(1);
}
printf("%s\n", key);
exit(0);
}
ori@myfedora:~/work$ gcc a.c
実行すると...謎の文字列が返ってきました!
ori@myfedora:~/work$ ./a.out /mnt/rosetta/rosetta
Our hard work
by these words guarded
please don't steal
© Apple Inc
余談
実際にx86_64なバイナリを指定してrosettaを実行すると、実は /proc/self/exe
に対してioctl(2)を2回発行していることがわかります。
ori@myfedora:~/work$ strace /mnt/rosetta/rosetta ./peco-x86_64 a.c 2>&1 | head -n 9
execve("/mnt/rosetta/rosetta", ["/mnt/rosetta/rosetta", "./peco-x86_64", "a.c"], 0xffffe006e930 /* 31 vars */) = 0
openat(AT_FDCWD, "/proc/self/exe", O_RDONLY) = 3
ioctl(3, _IOC(_IOC_READ, 0x61, 0x22, 0x45), 0xfffff3eb1708) = 1
close(3) = 0
gettid() = 32591
getpid() = 32591
openat(AT_FDCWD, "/proc/self/exe", O_RDONLY) = 3
ioctl(3, _IOC(_IOC_READ, 0x61, 0x23, 0x80), 0xfffff3eb1708) = 1
close(3)
2回目のioctl(2)の出力は、/run/rosettad/rosetta.sock
を返していました。
libkrunとRosetta
冒頭でも書いたとおり、libkrunはVirtualization.frameworkを使わないVMMにもかかわらず、Rosettaサポートを導入しました。すごい。
...と思いきや、いろいろあって、いつのまにかそのコードは消えて使えなくなっています。残念。
libkrunでrosettaバイナリに対してioctlを発行したときの動き
さて、libkrunがこのRosettaのチェックをどうかいくぐっているかというと... 結論から言うと、仮想マシンがrosettaバイナリに対して上記のioctl(2)を発行したときに呼ばれるVMM内のハンドラで、${HOME}/.krunvm-rosetta
の内容を返す実装になっています。つまりこのファイルに、上で確認した文字列を書いておけば...というわけです。
では実装を確認していきます。以下では、revert前の、commit id af437664855a6dc27954b31f8fef2c27df37748b を参照します。
virtiofsを表すstruct PassthroughFs
に、rosetta_data
フィールドがあります (流用元のcrosvmのPassthroughFsにはこのフィールドはありません)。
/// A file system that simply "passes through" all requests it receives to the underlying file
/// system. To keep the implementation simple it servers the contents of its root directory. Users
/// that wish to serve only a specific directory should set up the environment so that that
/// directory ends up as the root of the file system process. One way to accomplish this is via a
/// combination of mount namespaces and the pivot_root system call.
pub struct PassthroughFs {
inodes: RwLock<MultikeyBTreeMap<Inode, InodeAltKey, Arc<InodeData>>>,
next_inode: AtomicU64,
init_inode: u64,
path_cache: Mutex<BTreeMap<Inode, Vec<String>>>,
file_cache: Mutex<LruCache<Inode, Arc<File>>>,
pinned_files: Mutex<BTreeMap<Inode, Arc<File>>>,
handles: RwLock<BTreeMap<Handle, Arc<HandleData>>>,
next_handle: AtomicU64,
init_handle: u64,
host_volumes: RwLock<HashMap<String, Inode>>,
// Whether writeback caching is enabled for this directory. This will only be true when
// `cfg.writeback` is true and `init` was called with `FsOptions::WRITEBACK_CACHE`.
writeback: AtomicBool,
rosetta_data: Option<Vec<u8>>,
cfg: Config,
}
rosetta_data
フィールドの初期化は、コンストラクタの中で read_rosetta_data()
を呼び出すことで行います。この中で、${HOME}/.krunvm-rosetta
を読んでその内容を rosetta_data
フィールドに格納しています。
const KRUNVM_ROSETTA_FILE: &str = ".krunvm-rosetta";
fn read_rosetta_data() -> io::Result<Vec<u8>> {
let home = std::env::var("HOME")
.map_err(|_| linux_error(io::Error::from_raw_os_error(libc::EINVAL)))?;
let path = format!("{}/{}", home, KRUNVM_ROSETTA_FILE);
let mut file =
File::open(path).map_err(|_| linux_error(io::Error::from_raw_os_error(libc::EINVAL)))?;
let metadata = file
.metadata()
.map_err(|_| linux_error(io::Error::from_raw_os_error(libc::EINVAL)))?;
let mut data = vec![0u8; metadata.len() as usize];
file.read_exact(&mut data)
.map_err(|_| linux_error(io::Error::from_raw_os_error(libc::EINVAL)))?;
Ok(data)
}
libkrunのioctl(2)のハンドラの中で、ioctlのtypeが 0x61
であれば rosetta_data
フィールドの値を返しています。
fn ioctl(
&self,
_ctx: Context,
_inode: Self::Inode,
_ohandle: Self::Handle,
_flags: u32,
cmd: u32,
_arg: u64,
_in_size: u32,
out_size: u32,
) -> io::Result<Vec<u8>> {
if cmd == IOCTL_ROSETTA {
// This is based on the information found in this article:
// https://threedots.ovh/blog/2022/06/quick-look-at-rosetta-on-linux/
if let Some(data) = &self.rosetta_data {
if data.len() == out_size as usize {
return Ok(data.clone());
}
}
Err(linux_error(io::Error::from_raw_os_error(libc::ENOSYS)))
} else {
Err(linux_error(io::Error::from_raw_os_error(libc::ENOSYS)))
}
}
というわけで、あらかじめ ${HOME}/.krunvm-rosetta
を作って、しかるべき文字列を書いておくことで、libkrun上のLinux仮想マシンで、Rosettaを使ってx86_64バイナリを実行できるようになります。
余談
libkrunはライブラリ形式のVMMです。libkrunを使ってコマンドラインから仮想マシンを実行するツールとして、krunvmがあります。
krunvmのコードには、「rosettaを実行するには${HOME}/.krunvm-rosetta
にしかるべき文字列をあらかじめ書いておく必要がある」旨の残骸が今でも残っています。
let path = format!("{}/{}", home, KRUNVM_ROSETTA_FILE);
if !Path::new(&path).is_file() {
println!(
"
To use Rosetta for Linux you need to create the file...
{}
...with the contents that the \"rosetta\" binary expects to be served from
its specific ioctl.
For more information, please refer to this post:
https://threedots.ovh/blog/2022/06/quick-look-at-rosetta-on-linux/
",
path
);
最後に
libkrun開発者のツイートをいくつか貼っておきます (スクショではkrunvmを使っていて、VM内のLinux自体がx86_64になっているのが興味深いですね)。
Discussion