Waydroidで英語のときだけテンキーが効かない原因を、AndroidとWaydroidのソースから調べた

LinuxでAndroidを動かすWaydroidの中で、テンキーの数字が打てなくなった。キーボードをGboardの「日本語」にすると打てて、「English (US)」にすると打てない。上段の数字キーは、どちらでも打てる。ホスト(Arch Linux、sway)でテンキーを打つと、数字が出る。 swayの設定に input * xkb_numlock enabled を足したが、直らなかった。なぜ効かないのかを、ソースと実測で調べた。 先に結論 ホストのNumLockの状態は、Waydroidの中のAndroidに渡らない。swayの設定は、Waydroidには効かない。 AndroidのNumLockは、Waydroidの中で、入った直後に消されている可能性が高い。ソースの読みと実測が合っているが、消す判定の値そのものは、見ていない。 Androidのアプリで、テンキーを数字に割り当てるキーレイアウトを足したら、英語のままテンキーが打てた。 ソースで分かったこと 読んだのは、WaydroidのHWC(android_hardware_waydroidのhwcomposer/wayland-hwc.cpp)と、Android 13(android13-release)の入力まわり、Waydroidが当てるパッチ(android_vendor_waydroid)。 1. ホストのキーは、キーコードのまま渡される。修飾キーの状態は渡されない。 HWCは、Waydroidのウィンドウに入ったキー入力を、キーコードの形で、Android側のパイプ(/dev/input/wl_keyboard_events)に書く。Waylandの修飾キーの状態を受け取る関数 keyboard_handle_modifiers は、中身が空になっている。ホストのNumLockがオンでも、Androidはそれを知らない。 2. テンキーの数字は、AndroidのNumLockがオンのときだけ出る。 Android 13の Generic.kcm では、NUMPAD_1 は、base: fallback MOVE_END、numlock: '1' と定義されている。NumLockがオフなら、Endキーになる。 3. NumLockの状態は、キーを離したときに切り替わる。 Keyboard.cpp の toggleLockedMetaState は、キーを押したときは何もせず、離したときに、状態を反転する。 4. Waydroidが作る入力機器は、出せるキーを2つしか登録していない。 WaydroidのEventHubのパッチ(Android 13用の 0004-EventHub-Add-wayland-inputs-support.patch)は、wayland_keyboard の keyBitmask に、BTN_MISC と KEY_OK だけを登録する。 AndroidのEventHubの markSupportedKeyCodes は、キーレイアウトにそのキーがあっても、この登録に無いキーは、「機器に無いキー」と判定する。KeyboardInputMapper::updateLedState は、NumLockのキーが無い機器について、NumLockの状態を消す。 NumLock(コード69)は、この登録に入っていない。 実測 dumpsys input で、入力機器の状態を取った。キー入力の履歴を含まない部分だけを、次のように取り出せる。 sudo waydroid shell dumpsys input | sed -n '/Input Reader State/,/Input Dispatcher State/p' > out-1.txt Copy wayland_keyboard の項目の、MetaState(修飾キーとロックの状態)と、DownTime(最後にキーが押された時刻)を、見た。 ...

pavucontrolが起動しない。追ったら、WirePlumberのLuaバグの奥にUSBの通信エラーがあった

音量を調整しようとpavucontrolを開いたら、こう言われた。 pa_context_get_card_info_by_index() 失敗: Invalid argument Copy それだけ。開かない。 環境はArch Linux、sway、PipeWire 1.6.8、WirePlumber 0.5.16。よくある「PulseAudio互換レイヤーが機嫌を損ねた」系の話に見えたが、実際に追ってみたら、思ったより下の層まで潜る羽目になった。 まず journalctl systemctl --user status pipewire pipewire-pulse wireplumber でログを見ると、pipewireとwireplumberが3〜5秒おきに同じエラーを吐き続けていた。しかも調査してる最中もリアルタイムで進行中。 pipewire: spa.alsa: 'front:0': capture open failed: デバイスもしくはリソースがビジー状態です pipewire: pw.node: (alsa_input.usb-Kingston_HyperX_Quadcast_4110-00.analog-stereo-94) suspended -> error (Start error: デバイスもしくはリソースがビジー状態です) wireplumber: s-monitors: Failed to create ALSA node alsa_input.usb-Kingston_HyperX_Quadcast_4110-00.analog-stereo: Object activation aborted: PipeWire proxy destroyed wireplumber: wplua: [string "alsa.lua"]:446: attempt to call a nil value (method 'store_managed_pending') Copy 犯人らしきデバイス名が出ている。USBマイク(Kingston HyperX Quadcast)だ。 fuser -v /dev/snd/* で確認したが、掴んでいるのはwireplumber/pipewire自身だけ。Discordや他のアプリがマイクを横取りしているわけではなかった。PipeWireの中だけで完結して壊れている。 エラーメッセージに「Lua」と書いてある ログをよく見ると wplua: [string "alsa.lua"]:446 とある。C言語のデーモンのはずなのに、なぜLuaのエラーが出てくるのか。 ...

sudoersのNOPASSWDを絞り込んだら、PCが2回勝手に落ちた話

sudo systemctl restart bluetoothと打ったら、パスワードを聞かれずに通った。何かがおかしい。ここから、GTFOBinsの調査、SSH鍵の作り直し、rrsyncとの格闘、そしてOpenSSHのソースコードを読むところまで進んだ調査の記録。最後には、原因が分からないままPCが2回勝手に落ちるという、後味の悪いオチが付く。 きっかけ: パスワードを聞かれない Bluetoothの調子が悪くてsudo systemctl restart bluetoothを打った。いつもならパスワードを聞かれるはずが、そのまま通った。直近でsudoを使った覚えもない。おかしいと思ってsudo -n -l(パスワード無しで、自分に許可されているsudoコマンドを一覧表示する)を叩いてみた。 ユーザー takashi は takashi-pc 上で コマンドを実行できます (ALL : ALL) ALL (root) NOPASSWD: /usr/bin/rsync, /usr/bin/systemctl, /usr/bin/poweroff Copy /etc/sudoers.d/配下に、rsync・systemctl・poweroffの3つが、パスワード無しで、引数を問わず実行できる設定が入っていた。 GTFOBinsで確認した危険性 GTFOBinsは、Unix系のバイナリを悪用した権限昇格の手口をまとめたデータベースだ。systemctlとrsyncのページを見ると、この設定がどれだけまずいかがすぐ分かる。 systemctl: 自分でsystemdのサービスファイルを作り、systemctl link→systemctl enable --nowで登録・起動すれば、そのサービスはroot権限で動く。あるいはSYSTEMD_EDITOR環境変数に自分のスクリプトを指定した状態でsystemctl editを叩くと、そのスクリプトがrootで呼ばれる。 rsync: --rsh(または-e)オプションで、任意のコマンドを「リモートシェル」として起動できる。sudo rsync -e /bin/sh ...のような形で、rootのシェルを直接取れる。 つまりこの設定は、一般ユーザーのまま実質rootになれる、という状態だった。poweroffだけは被害が限定的(シャットダウンのみ)だが、残り2つは深刻。 誰がこれを使っていたのか このPCで動いている自動化を一通り確認したが、systemd timerにもcronにも、この3つのNOPASSWDに依存しているものは見当たらなかった。 答えは、別のマシン(自宅のミニPC、以下NAS)側にあった。backup-archという、このPCの/home /boot /etcを毎晩NASへrsyncでバックアップするスクリプトが、SSH経由でこのPCに対してsudo rsyncを実行していた。バックアップ完了後、sudo systemctl stop/startでコンテナを一時停止する処理と、sudo poweroffで自動シャットダウンする処理も持っていた。 backup-archの最初のコミットは2025年10月。当時はコーディングエージェントに書かせたもので、SSH経由のsudo実行を前提にした設計だった。systemctlを使っていた「バックアップ前後にコンテナを止める」処理は、後になって「そもそも止める対象がバックアップ範囲に含まれておらず、意味が無かった」という理由で既に削除されていたが、sudoers側の許可だけが広いまま残っていた。 絞り込みの設計: rrsyncとSSH強制コマンド rsyncとsystemctlのNOPASSWDを外し、その代わりに、必要な操作だけをできる形に絞り込むことにした。 rsyncにはrrsync(restricted rsync)という、rsync本体に同梱されている補助スクリプトがある。SSHのauthorized_keysで、特定の鍵に対してcommand="rrsync -ro /path/to/dir"のように強制コマンドを指定すると、その鍵で接続してきたクライアントが何を要求しても無視され、指定した1つのディレクトリへの読み取り専用アクセスだけが許可される。 ただしrrsyncは、1回の起動につき1つのディレクトリしか制限できない。今回は/home /boot /etcの3か所が必要だったので、ディレクトリごとに専用の鍵を1本ずつ、計3本作った。加えて、シャットダウン専用の鍵をもう1本。合計4本の鍵、それぞれに対応する強制コマンドをauthorized_keysに設定した。 command="sudo /usr/bin/rrsync -ro /home",restrict ssh-ed25519 AAAA... nas-backup-home command="sudo /usr/bin/rrsync -ro /boot",restrict ssh-ed25519 AAAA... nas-backup-boot command="sudo /usr/bin/rrsync -ro /etc",restrict ssh-ed25519 AAAA... nas-backup-etc command="sudo /usr/bin/poweroff",restrict ssh-ed25519 AAAA... nas-backup-poweroff Copy sudoers側も、引数まで完全一致させた形にする。 ...

暗号化されたWaydroidのキャッシュを諦めて、PSNの非公開APIから友人とのチャット履歴を救出した話

古い友人とのPlayStation Network(PSN)上でのチャットを、消えてしまう前に手元に残しておきたくなった。使っているのはPCの中の[Waydroid]で動くPS App。まずはローカルのキャッシュファイルを直接読みに行こうとしたのだが、そこから思いのほか長い調査になった。 ローカルのキャッシュは暗号化されていた Waydroidのアプリごとのデータは~/.local/share/waydroid/data/data/<パッケージ名>/以下にAndroidの/dataパーティションと同じ構造で保存されている。PS Appのフォルダを覗くと、databases/の中にそれらしいファイルが並んでいた。 fileコマンドで確認すると「OpenPGP Public Key」という判定が出て面食らったが、実際には単に先頭バイトが平文のSQLiteの磁気文字列(SQLite format 3)ではなかっただけだった。xxdで生のバイト列を見ると、最初から意味のないランダムな値が並んでいる。つまり中身は暗号化されていて、復号する鍵はアプリ側が握っている。ここは早々に諦めた。 アプリの画面を手で遡るのは、そもそも選択肢に入らなかった PSNのメッセージはサーバー側に保存されていて、PS4・PS5・スマホのPS Appのどれからでも同じ会話を見られる。だからPS Appを開いて手で遡れば、6年分だろうと全部読めることは最初から分かっていた。 問題はそこじゃない。何年分もの会話を、スクロールしながらスクリーンショットで残していくのがあまりに面倒だった、というだけの話だ。だから、ローカルの暗号化ファイルを諦めた後の次の一手は「手で遡る」ではなく、サーバーから直接、プログラムでメッセージを取得する方法を探すことだった。 PSNは1対1のDMも「Group」として扱っている PSNには非公式ながら実用に足るPythonラッパー(psnawp)がある。ソースを読むと、興味深いことが分かった。 The Group class manages PSN group endpoints related to messages (Group and Direct Messages). PSN内部では、複数人のグループチャットも1対1のダイレクトメッセージも、どちらも同じ「Group」という概念で扱われている。つまり友人との会話を取得するには、「その人とのGroup」を見つければよく、専用のDM用APIを探す必要はなかった。 ページネーションのパラメータ名を、独立した別実装から確定させる get_conversation()というメソッドでメッセージを取得できるが、レスポンスにはprevious・nextというカーソルらしきフィールドとreachedEndOfPageという終端フラグが含まれていた。ページネーションができる構造だとは分かったが、次のページを取るときに渡すクエリパラメータの正式な名前がドキュメントに載っていない。 最初はnextという名前を推測で使ったが、動かなかった。ここで、同じPSNの非公式APIを別言語(PHP)で実装している、完全に独立した別プロジェクト(Tustin/psn-php)のソースコードを確認した。該当箇所にはこう書かれていた。 if ($cursor != null) { $params['before'] = $cursor; } ... $this->update($this->totalCount, $results->messages, $results->previous); Copy 正しいパラメータ名はnextではなく**beforeで、渡す値はレスポンスのprevious**フィールドだった。ドキュメントに載っていない挙動でも、独立した複数の実装を突き合わせれば、推測に頼らず答えを確定できる。 実行環境まわりの小さなつまずき Arch Linuxではpip install --userがexternally-managed-environmentエラーで弾かれる(PEP 668)。素直にpython -m venvで仮想環境を作ってそこにインストールする。 認証にはNPSSOという64文字のトークンを使うが、ライブラリのドキュメントには「これはパスワードと同等に扱うべき」と明記されている。人に見せない・チャットに貼らない前提で、ローカルの環境変数として渡す運用にした。 もう一つ、正直に書いておく。 PSNには公式に公開されたAPIは無く、psnawpのようなライブラリはPS Appの内部通信をリバースエンジニアリングしたものだ。これはPSNの利用規約が禁じる「許可されていない自動化ツールの使用」に該当し、規約違反になる。 読み取り専用だから軽い、書き込みより安全、という話ではない。規約違反であること自体は読み書き問わず同じで、それを「相対的にリスクが低い」という言い方でぼかすべきではなかった。 結果と、届かなかったケース こうして書いたスクリプトを実行すると、6年近く前、会話グループが作られた時点まで遡って、1万件を超えるメッセージを数分で取得できた。手で遡っていたら、この量は現実的な時間では終わらなかったはずだ。 一方で、もう何年も連絡を取っていない別の知人に同じ方法を試したところ、Request is rejected due to all targets' settingsというエラーで弾かれた。これは相手の側のプライバシー設定(あるいはブロック)によるもので、こちらの実装や理屈の間違いではない。API がどれだけ正しく動いても、相手側の設定という現実の壁は越えられない。技術的な調査がどこまでも通用するわけではない、という当たり前の事実を確認する形になった。 ...

Termux の sshd はどんなユーザー名でも同じユーザーで入れる ── ソースで確かめる

母艦から Pixel 6(Termux の sshd)へ画像を scp するために ~/.ssh/config に エントリを足したとき、User を書いた。あとで気づいたが、相手が Termux だと ssh banana@<phone> でも ssh root@<phone> でも、同じユーザーでログインできる。 ~/.ssh/config の User 行は効いていない。設定の問題ではなく、Termux の OpenSSH ビルドの話だった。 まず観察 $ ssh -p 8022 banana@100.65.202.69 'whoami' u0_a440 $ ssh -p 8022 root@100.65.202.69 'whoami' u0_a440 Copy banana も root も通って、着地するのは u0_a440(Termux アプリのユーザー)。 鍵は /data/data/com.termux/files/home/.ssh/authorized_keys だけを見ている。 通常の OpenSSH がユーザーをどう解決するか sshd は、接続してきたユーザー名を struct passwd に解決する。これをやるのが auth.c の getpwnamallow()。素の OpenSSH ではこう: pw = getpwnam(user); Copy user はクライアントが送ってきた名前。それを /etc/passwd(実際には NSS 経由で LDAP / SSSD なども)で引く。 ...

nvim-treesitter の master→main 移行で踏んだ treesitter エラー3連発(Neovim 0.12): attempt to call method 'range' (a nil value)

Neovim を 0.12.4 に上げたあたりから、nvim-treesitter 関連のエラーが立て続けに出るようになった。原因を追っていくと、nvim-treesitter の master ブランチがすでに EOL で Neovim 0.12 に付いてこられていないという一点に行き着く。main ブランチへ移行して解決したが、その過程と、移行後にもう一段踏んだ落とし穴をまとめておく。 環境: 項目 値 Neovim v0.12.4(Arch Linux) nvim-treesitter master(tag v0.10.0、2025年5月で凍結)→ main へ移行 プラグインマネージャ Lazy.nvim 症状1: Markdown を開くと gotmpl parser で落ちる Markdown ファイルを開いた瞬間、エラーで停止する(続けるにはENTERを押すか…)。 Error in BufReadPost Autocommands for "*": ... ftplugin/markdown.lua: Vim(runtime):E5113: Lua chunk: .../treesitter.lua:460: Parser could not be created for buffer 1 and language "gotmpl" Copy これを ENTER で流すと、今度はコードフェンス(```go など)のシンタックスハイライトが効かなくなり、:messages に別のエラーが出ている。 Decoration provider "start" (ns=nvim.treesitter.highlighter): .../treesitter.lua:197: attempt to call method 'range' (a nil value) Copy Packer.nvim → Lazy.nvim へ移行した時期と重なっていたので最初はプラグインマネージャを疑ったが、それは無関係。同時期に上げた Neovim 0.12 が引き金だった。原因は2つ重なっていた。 ...

swayfxのscratchpadクラッシュを追う: git blameで見つけたupstreamとの噛み合わせ問題

事象 普段使っているswayfx上で、scratchpadに入れたウィンドウを表示している状態からもう一度scratchpad showを叩くと、コンポジタごと丸ごと落ちる現象に遭遇した。SDDMのログイン画面まで戻される、なかなか派手なクラッシュ。 環境: 項目 値 swayfx 0.5.3-4(sway 1.11.0ベース、Arch Linuxパッケージ) wlroots 0.19.3 GPU AMD Radeon RX 6600 XT(amdgpu) bindsymはこう組んでいた: bindsym $mod+minus scratchpad show, resize set width 90 ppt height 90 ppt, move position center Copy scratchpad showと同時にresize・moveを連続実行する形。これを2回続けて押す(1回目で表示、2回目でフォーカスしたまま押してhideパスに入る)とクラッシュする。 スタックトレースを取る coredumpctl + gdb(デバッグシンボル入り)でコアダンプを解析した。 #0 root_scratchpad_hide (con=0x55f058a52400) at ../swayfx-0.5.3/sway/tree/root.c:257 257 set_container_transform(con->pending.workspace, con); seat = 0x55f0580db940 focus = 0x55f058a41760 ws = 0x0 Copy ws(= con->pending.workspace)がNULLのままset_container_transform()に渡され、内部で参照外しして落ちていた。該当箇所のコードはこうなっていた: void root_scratchpad_hide(struct sway_container *con) { struct sway_seat *seat = input_manager_current_seat(); struct sway_node *focus = seat_get_focus_inactive(seat, &root->node); struct sway_workspace *ws = con->pending.workspace; if (con->pending.fullscreen_mode == FULLSCREEN_GLOBAL && !con->pending.workspace) { // If the container was made fullscreen global while in the scratchpad, // it should be shown until fullscreen has been disabled return; } ... set_container_transform(con->pending.workspace, con); // ガードなし Copy NULLチェックが「FULLSCREEN_GLOBALのときだけ」に絞られていて、それ以外の理由でpending.workspaceがNULLになるケースを素通りしてしまう。set_container_transform()はswayfx独自の追加関数(本家swayには存在しない)なので、一見swayfx側のバグに見えた。 ...

Street Fighter 6 (Proton) で日本語入力ができない問題は /etc/environment のIME環境変数で解決した

訂正 (2026-08-08) この記事の当初の結論「ユーザー側の設定では解消できない」は 誤りでした。 /etc/environment にIME環境変数を設定することで、Ctrl+Space での直接入力が動作するようになりました。 結論だけ必要な方は 追記: 解決した へ。 以下の調査記録は、当時どう考えて誤った結論に至ったかの記録として、そのまま残します。 事象 Street Fighter 6のチャット欄でCtrl+Space(fcitx5のトリガーキー)を押しても、日本語入力(Mozc)に 切り替わらない。ローマ字のまま反応しない。 項目 値 OS Arch Linux WM Sway (swayfx) IME fcitx5 + fcitx5-mozc ゲーム Street Fighter 6 (Steam, AppID 1364780) Proton Proton Experimental / GE-Proton11-3 当時の結論(誤り) Proton公式ビルドは意図的にXIMを無効化しており、加えてWineの新しいIME実装がキー入力を XIMサーバーまで橋渡ししていない。この二つが重なっているため、レジストリでXIMを 再有効化しても効果が出ない。Street Fighter 6固有の不具合ではなく、Wine/Proton全体に 共通する構造的な制約であり、ユーザー側の設定では解消できない。 ——これが誤りだった。 実際には環境変数がゲームの起動経路まで届いていなかっただけの可能性が高い。 詳細は末尾の追記を参照。 当時の根拠 以下は2026-08-06時点で実際に観測した内容。観測自体は正しいが、そこから導いた結論が誤っていた。 Sway/fcitx5の設定は正常 — Ctrl+Spaceに該当するbindsymはなし、GTK_IM_MODULE/ QT_IM_MODULE/XMODIFIERSはenvironment.dで正しく設定済み、fcitx5のトリガーキーも Control+spaceのままでXIMアドオンも無効化されていない。 後から振り返ると、ここが見落としの核心だった。 「environment.dで設定済み」であることは確認したが、 その変数がSteam経由で起動されるゲームプロセスまで届いているか は確認していなかった。 Protonは意図的にXIMを無効化している — Proton公式リポジトリのIssueに記載がある。 XIM is disabled for working around a X11 issue ...

Arch Linux と Windows 11 を安全にデュアルブートするためのメモ

このメモの目的 このメモは、ASRock 環境で実際にトラブルを踏んだ経験を元に、 Arch Linux と Windows 11 を同じディスクでデュアルブートしたい systemd-boot を使いたい 変な EFI トラブルで何日も溶かしたくない Arch / Windows / systemd-boot / UEFI の関係図 という人向けの「基礎まとめ」です。 詳細なチュートリアルというより、やってはいけないポイント や 設計の考え方 を中心にしています。 前提環境 例として想定している環境はこんな感じです。 マザーボード: ASRock A520M Pro4(UEFI) CPU: Ryzen 7 5700X GPU: Radeon RX 6600 XT メモリ: DDR4 32GB ストレージ: NVMe SSD 1 本 OS: Windows 11 + Arch Linux ブートローダー: systemd-boot パーティション形式: GPT ブート方式: UEFI(Legacy BIOS / CSM は使わない) 他のマザーボードでも基本の考え方は同じですが、UEFI のクセはメーカーごとに違うので、あくまで「考え方の参考」として読んでください。 ...

Waydroid Binder/BinderFS

Waydroidが起動しない原因とBinder/BinderFSの仕組み

概要 Arch Linux で Waydroid を動かそうとした際、 「Can't open /dev/anbox-binder: No such device or address」というエラーが発生した。 原因は カーネルが BinderFS を正しくマウントしていなかったことにあった。 ここでは、Binder/BinderFS の役割と、実際に動作させるまでの手順をまとめる。 1. Binder と BinderFS の違い Android のアプリ間通信(IPC)は Binder ドライバで実現されている。 Linux でこれを再現するためには、カーネルモジュールと特別な仮想ファイルシステムが必要になる。 名称 説明 /dev/binder Android のプロセス間通信 (IPC) のコアデバイス /dev/hwbinder ハードウェア抽象層 (HAL) 用 Binder /dev/vndbinder ベンダーサービス用 Binder BinderFS これらのデバイスノードを動的に管理するためのファイルシステム 昔の環境では /dev/binder を直接作っていたが、 最近の Waydroid や Mainline カーネルでは BinderFS を使うのが標準。 2. エラーの原因 Waydroid 起動時のログ: [gbinder] ERROR: Can't open /dev/anbox-binder: No such device or address [08:49:18] Failed to add presence handler: None Copy これは、/dev/anbox-binder が存在しても中身が無効、 つまり Binder デバイスがカーネル側で認識されていないことを意味する。 ...