ESM アジャイル事業部 開発者ブログ

永和システムマネジメント アジャイル事業部の開発者ブログです。

はじめてのRISC-Vベアメタルプログラミング(ヨロシク編)

こんにちは、永和システムマネジメントの内角低め担当、はたけやま( @htkymtks )です。

みなさん、RISC-Vをご存知ですか?RISC-VはCPUの命令セットアーキテクチャ(ISA)のひとつで、使用料のかからないオープンソースライセンスで提供されていることや、命令セットの美しさから注目を集めています。私も以前にRubyでRISC-Vシミュレータを作ったりしてました。

今回はRISC-Vを用いて、OSもライブラリも使用しないベアメタル環境で動作するプログラムを作成してみようと思います。

インストール

まずはRISC-Vのクロスコンパイラとエミュレータをインストールします。クロスコンパイラのビルドには約1時間ほどかかるので、時間の余裕がある時に行ってください。

$ brew tap riscv-software-src/riscv
$ brew install riscv-tools qemu

以下のコマンドを実行し、それぞれのバージョンが正しく表示されれば、インストールは成功です。

 $ riscv64-unknown-elf-gcc --version
 $ qemu-system-riscv64 --version

クロスコンパイラとは、あるアーキテクチャ(ターゲットシステム)向けの実行可能コードを、異なるアーキテクチャ(ホストシステム)上でコンパイルするためのツールです。今回はターゲットシステムがRISC-V、ホストシステムがARM64(Apple M1)に設定されたGCCを利用します。

エミュレータにはQEMUを使用します。QEMUは様々なCPUアーキテクチャをサポートするエミュレータで、32ビット及び64ビットのRISC-Vが対応しています。

サンプルプログラム

レジスタs0に値(0x4649 = ヨロシク)をセットして無限ループするサンプルプログラムを用意します。

  .text
  .globl _start
  .type _start, @function

_start:
  # レジスタ s0 へ即値 0x4649 をロード
  li s0, 0x4649
loop:
  # 無限ループ
  j loop

リンカスクリプト

QEMU(正確にはVirtio)では、アドレス0x80000000番地からプログラムの実行が開始されます。そのため、以下のようなリンカスクリプトを用意し、プログラムの開始位置のアドレスが0x80000000番地になるようにします。

MEMORY {
   RAM (RWX) : ORIGIN = 0x80000000, LENGTH = 0x40000000
}
SECTIONS {
    .text : {
        *(.text)
        _end = .; /* 後日 malloc で使う予定 */
    }
}

ビルド

それではサンプルプログラムをビルドしましょう。アセンブラを使用してアセンブリコード sample1.S からオブジェクトファイル sample1.o を生成し、オブジェクトファイルから実行ファイル sample1.elf を生成します。

$ riscv64-unknown-elf-as sample1.S -o sample1.o
$ riscv64-unknown-elf-ld -Tmy_baremetal.ld --no-relax sample1.o -o sample1.elf

生成された sample1.elf を逆アセンブルしてみましょう。プログラムが0x80000000番地からスタートすることが確認できます。

$ riscv64-unknown-elf-objdump -S sample1.elf
 
sample1.elf:     file format elf64-littleriscv
 
 
Disassembly of section .text:
 
0000000080000000 <_start>:
    80000000:   00004437           lui s0,0x4
    80000004:   6494041b            addiw   s0,s0,1609 # 4649 <_start-0x7fffb9b7>
 
0000000080000008 <loop>:
    80000008:   0000006f            j   80000008 <loop>

実行

先ほど作成したELFファイルを指定してQEMUを実行します。

qemu-system-riscv64 -M virt -monitor stdio -bios sample1.elf

引数の意味は以下のとおり。

  • -bios sample1.elf
    • 実行ファイルを指定します
  • -M virt
    • ターゲットとなるボードを指定します。今回は「RISC-V VirtIO board = virt」を使用します
  • -monitor stdio
    • QEMUモニタをターミナルに表示するために指定します

実行結果を確認

QEMUを起動してプログラムを実行すると、QEMUモニタも一緒に立ち上がります。「info registers」コマンドでレジスタの中身を確認すると、レジスタ「x8/s0」に「4649」がセットされていることがが確認できます。また、プログラムカウンタ(PC)の値が「80000008」であることから、無限ループしていることが分かります。

動作確認を終えたら、QEMUモニタで「quit」と入力してQEMUを終了させます。

終わりに

以上、QEMU上のRISC-Vでベアメタルプログラミングを行う方法を紹介しました。次回はこの上で何か面白いアプリケーションを動かしてみようと思います。

RubyKaigi 2024 に永和システムマネジメントから @koic @junk0612 の2人が登壇します

2024年5月15日(水) から17日(金) の3日間にわたって開催される RubyKaigi 2024 に永和システムマネジメントから @koic (Day 2) 、 @junk0612 (Day 3) の2人が登壇します。

rubykaigi.org

ここでは、それぞれの登壇者から講演内容について軽く紹介をします。

5月16日(木) 14:50-15:20 @koic 『RuboCop: LSP and Prism』

@koic です。今年は『RuboCop: LSP and Prism』というタイトルで話します。タイトルにあるように RuboCop をとおした LSP と Prism の話です。当初 LSP を主体にして、タイトルには Prism と出していなかったのですが、Rails/OSS パッチ会で RubyKaigi オーガナイザーでもある松田さんから「Lrama の講演が3つあるので、Prism もタイムテーブルに入れたい」といった雰囲気の話を受けてタイトルを更新したものになります。結果として思ったよりも Prism に関するボリュームは膨らむかもしれません。

LSP は Language Server Protocol の略称で、よく聞くところでは Ruby LSP などがあります。git hooks で rubocop コマンドを実行するようなエディタ連携の上位互換になる側面もあるので、LSP 未使用の方はぜひ使ってみると良いと思います。LSP の基本的な事柄について詳しくは RubyKaigi 2017 で @mtsmfm が話されている『Ruby Language Server』を予習しておくと事前知識に便利だと思います。

rubykaigi.org

もうひとつの大きなトピックは Prism です。RuboCop が Ruby のソースコードをパースするにあたって @whitequark プロダクトである Parser gem を使っていますが、RuboCop 1.62 で Prism のサポートを実験的に導入しました。そのあたりの裏側と今後の話を展開する予定です。Prism そのものについては、RubyKaigi 2023 で Kevin Newton が話されていた『Yet Another Ruby Parser』あたりは予習になると思います。

rubykaigi.org

そんな感じで LSP と Prism の話です。LSP は去年の RubyKaigi 2023 で Standard gem の作者である Justin Searls と話して RuboCop に組み込んだ機能です。LSP を入れたら Error Tolerance なパーサーのことを検討するわけで、そこで Parsing Ruby is Suddunly Managable の Prism に着目したというのは必然的な流れでした。加えて、昨年 RubyConf Taiwan 2023 で The Bison Slayer の金子さんと4日4晩パーサーそのものやその周辺ツールについて話す機会があって、その影響もそれなりに出ている活動の話になります。

このように RuboCop について、LSP と Prism といったテクノロジーを使ってどのような課題解決を目指しているのか、これからの Linter や Formatter について RubyKaigi する予定です。お楽しみに。

rubykaigi.org

5月17日(金) 14:10-14:40 @junk0612 『From LALR to IELR: A Lrama's next step』

@junk0612 です。RubyKaigi には初めての登壇です。『From LALR to IELR: A Lrama's next step』というタイトルのとおり、Lrama の話をします。Lrama について詳しくは昨年の @yui-knk さんの発表をどうぞ。

rubykaigi.org

言語処理系の分野では、Lexer と Parser は分離可能であると (基本的には) されています。しかし、例えば CRuby では < は比較演算子の役割に加えて、<< として Array の要素追加や、<<- として Heredoc の開始に使われるなどの用途があり、Lexer は状態を持ったうえで必要に応じて自身の状態を変えねばならず、特にこれまで読んできた内容、すなわち Parser の状態の影響を受けてしまうのが現実です。

CRuby ではその「Lexer の状態」を管理するための lex_state という変数があります。しかし現状では、さまざまな場所で状態が変更され見通しが悪くなっているうえ、人手によって管理されているためにある変更が他にどのような影響を与えるか機械的に知る術がなく、とても扱いづらいものになってしまっています。

そこで、パーサージェネレーター方面から「より良い LR アルゴリズムを用いたパーサー」の提供をすることで、lex_state にまつわる問題を解消しようという試みを行っています。具体的には PSLR というアルゴリズムを採用した Scannerless parser の提供です。ところが PSLR は、現在 Lrama に実装されている一般的な LR アルゴリズムである LALR からではなく、それを改良した IELR という別のアルゴリズムに立脚しています。そこで "Lrama's next step" として、IELR パーサーを Lrama が生成できるようにしよう、というのが講演の主軸となる内容です。Scannerless parser とは何なのか、IELR とLALR の違い、実装でつらかった点などについては当日の発表をお楽しみに。

rubykaigi.org

ところで @koic の文章にもあったとおり、今年は Lrama にまつわるトークが3本あります。どれか1つでいいか……と思っている皆さんにぜひお伝えしたいのが、内容的に被る部分はおそらくほぼないので、3本それぞれに聞く価値があるという点です。全部聞けばあなたも Lrama マスターになれること間違いなし。3日間に分散していて1日1本ペースなので、体力的にもやさしめです。ぜひお越しください。

rubykaigi.org rubykaigi.org


それでは本編をお楽しみに。沖縄の会場でお会いしましょう。

RubyKaigi 2024 に スポンサーとしても参加している永和システムマネジメントでは、Ruby とアジャイルソフトウェア開発や構文解析器の研究を通じてコミュニティと成長したいエンジニアを絶賛募集しています。

agile.esm.co.jp

Rails / OSS パッチ会オンライン 2024年4月のお知らせ

2024年4月の Rails / OSS パッチ会を 4月10日(水)に Discord でオンライン開催します。

この会をひとことでいうと、日頃のお仕事で使っている Rails をはじめとする OSS について、upstream にパッチを送る会です。

会には Ruby と Rails のコミッターである顧問の a_matsuda もいますので、例えば Rails に送るパッチのネタがあるけれど、パッチを送るに適しているかの判断やパッチを送る流れが悩ましいときなど a_matsuda に相談して足がかりにするなどできます。

開催時間は 17:00-19:00 となりますがご都合のあう方はぜひご参加下さい。

Discord の Rails/OSS パッチ会サーバーへの招待 URL は以下です。

discord.gg

来月開催される RubyKaigi 2024 に向けた話題などがあると思います。

rubykaigi.org

これからパッチ会に参加してみたいという方、OSS 開発者間の会話に興味があるので聞いてみたいという方もお気軽にどうぞ。


永和システムマネジメントでは、Ruby とアジャイルソフトウェア開発を通じてコミュニティと成長したいエンジニアを絶賛募集しています。

agile.esm.co.jp

RuboCop の設定ファイルで、無効な cop を明示する書き方から有効な cop を明示する書き方に直してみた

はじめに

こんにちは、@wai-doi です。

本記事では RuboCop の設定ファイルの .rubocop.ymlで、特定の cop の無効化を明示している書き方から、逆に、有効になっている cop の有効化を明示する書き方に直す方法を考えました。

やりたかったこと

まず以下のような .rubocop.yml がありました。

AllCops:
  DisabledByDefault: true

Layout:
  Enabled: true

Layout/ArgumentAlignment:
  Enabled: false

Layout/ArrayAlignment:
  Enabled: false

# 省略

Layout/SpaceInsideHashLiteralBraces:
  Enabled: false

上から順に見ていくと、まず AllCops: DisabledByDefault: true で全ての cop を無効化しています。そして、Layout: Enabled: true で Layout の cop を有効化にしています。最後に Layout の中で特定の cop を Layout/ArgumentAlignment: Enabled: false のように無効化にしています。

この状態から、逆に、どの cop が有効になっているかがわかりやすいよう、有効化している cop だけを明示した書き方に直したいということがありました。

期待する書き方は以下です。

AllCops:
  DisabledByDefault: true

Layout/AccessModifierIndentation:
  Enabled: true

# Layout/ArgumentAlignment Layout/ArrayAlignment は書かれていない

Layout/BeginEndAlignment:
  Enabled: true

# 省略

Layout/TrailingWhitespace:
  Enabled: true

つまり以下のように書き換えたかったです。

  • Layout: Enabled: true を削除する
  • Layout cops でデフォルトに有効になっている cop を明示的に Enabled: true で書く
  • ただし今 Enabled: false を書いている cop については何も書かない

解決したスクリプト

明示的に Enabled: true を書くために、RuboCop のドキュメントにあるたくさん Layout cops を書き写すのは面倒なので、以下の Ruby スクリプトを書いてみました。

rubocop v1.60.2 を用いました。

require 'rubocop'

# Layout cops を取得します
layout_cops = RuboCop::Cop::Registry.new(RuboCop::Cop::Registry.all).with_department(:Layout)

# デフォルトで有効になる Layout cops だけを取得する
default_enabled_layout_cops = layout_cops.enabled(RuboCop::ConfigLoader.default_configuration)

# .rubocop.yml から Enabled: false を書いている cop名 を取り出す
disabled_layout_cop_names = RuboCop::ConfigLoader
  .load_file('.rubocop.yml')
  .to_h
  .select {|cop_name, cop_config| cop_name.start_with?('Layout/') && cop_config['Enabled'] == false }
  .keys

# デフォルトで有効になる cop から Enabled: false を書いている cop を除く
enabled_layout_cop_names = default_enabled_layout_cops.map(&:cop_name) - disabled_layout_cop_names

# .rubocop.yml に Enabled: true をコピペできる形式にして出力する
puts enabled_layout_cop_names.map {|cop_name|
  <<~YAML
    #{cop_name}:
      Enabled: true
  YAML
}.join("\n")

1行目と2行目の RuboCop::Cop::RegistryRuboCop::ConfigLoader の使ったコードは @koic さんに教えていただきました。 RuboCop::Cop::Registry は RubCop が内部で持っている cop の一覧情報が取得できるクラスで、RuboCop::ConfigLoader は RuboCop の設定ファイルを処理するクラスです。

工夫した点

工夫したところは3行目の disabled_layout_cop_names を取得するところです。 以下のように RuboCop::Cop::Registry#disabled を使って .rubocop.yml で無効になっている cop を取得しようとしましたが、これではうまくできませんでした。

disabled_layout_cop_names = layout_cops.disabled(RuboCop::ConfigLoader.load_file('.rubocop.yml'))

#disabled実装を見ると、.rubocop.yml に Enabled:true と書かれている cop を除くという処理であることがわかりました。.rubocop.ymlには Enabled: true は書いていないためなにも除かれなかったため、これはうまくできませんでした。

別の方法で取れないかいろいろ探すと、RuboCop::ConfigLoader.load_file('.rubocop.yml') の結果を to_h でハッシュに変換できるようだったため、正規表現などを使って Enabled: false の cop を取得する方法で実現できました。

後からわかったことですが、ここで .rubocop.yml をハッシュへの変換しているのは単純にYAMLをパースしているだけなので、 YAML.load_file('.rubocop.yml') でも同じでした。

RuboCop にあるメソッドで Enabled: false と書いている cop をもっとスマートに取得できる方法もあるかもしれないですが、わかりませんでした。

おわりに

今回やってみて、RuboCop::Cop::RegistryRuboCop::ConfigLoader など RuboCop を普段使うだけでは知らなかったクラスなどを知ることができてよかったです。


永和システムマネジメントでは、Ruby とアジャイルソフトウェア開発を通じてコミュニティと成長したいエンジニアを絶賛募集しています。

agile.esm.co.jp

Rails / OSS パッチ会オンライン 2024年3月のお知らせ

2024年3月の Rails / OSS パッチ会を 3月13日(水)に Discord でオンライン開催します。

この会をひとことでいうと、日頃のお仕事で使っている Rails をはじめとする OSS について、upstream にパッチを送る会です。

会には Ruby と Rails のコミッターである顧問の a_matsuda もいますので、例えば Rails に送るパッチのネタがあるけれど、パッチを送るに適しているかの判断やパッチを送る流れが悩ましいときなど a_matsuda に相談して足がかりにするなどできます。

開催時間は 17:00-19:00 となりますがご都合のあう方はぜひご参加下さい。

Discord の Rails/OSS パッチ会サーバーへの招待 URL は以下です👇

discord.gg

5月に開催される RubyKaigi 2024 に向けた話題などがあるかもしれません。

弊社の構文解析器研究部に関心がある方や、これからパッチ会に参加してみたいという方、OSS 開発者間の会話に興味があるので聞いてみたいという方もお気軽にどうぞ。


永和システムマネジメントでは、Ruby とアジャイルソフトウェア開発を通じてコミュニティと成長したいエンジニアを絶賛募集しています。

agile.esm.co.jp

パターンマッチで NODE_ONCE に対応するようにした

こんにちは、構文解析研究部員の S.H. です。

社内でやっているエンジニアお茶会で (@koic) さんから以下のIssueの件で「Rubyのパーサー周りの問題ではないか」と相談を受けました。

github.com

そこで調査したところ比較的簡単に修正できそうなことが分かり、以下のPull Requestを作成して無事マージされました。

github.com

これは、その時の調査した内容などをまとめた記事になります。

もともとの挙動

RuboCop と RuboCop Performance を使っている際に以下のようなコードを自動修正すると、コンパイルエラーになるコードへと変換されてしまうようです。

自動修正前のコード:

v = "117"

case "117"
in /#{v}/
  p "Passed"
end
# => Passed

自動修正後のコード:

v = "117"

case "117"
in /#{v}/o # オプションとして `o` が末尾についている
  p "Passed"
end
# => NODE_IN: unknown node (NODE_ONCE)

自動修正前のコードとの差分はオプションとして o が末尾についているかどうかだけのようです。またエラーメッセージにある NODE_INNODE_ONCE はRuby内部で利用しているASTノードのことなのでRuby本体側の問題らしいことが分かりますね。

調査

まずはエラーメッセージを元に CRuby のソースコードを grep してみました。

NODE関連のエラーメッセージなので該当しそうな個所としては parse.y、node.c、node_dump.c、compile.c、 ruby_parser.c あたりが怪しそうでした。ただ parse.y、node.c、node_dump.c、ruby_parser.c に関してはおそらく無関係かもしれないな思っていました。

これは以下のように考えたためです。

  • parse.y、node.c、node_dump.c については、Universal Parser の C API依存削減対応や新しい NODE 追加などを直近で調べていたなので、該当しそうな箇所を見た覚えがないので無関係かもしれない
  • またIssueのコードからパターンマッチ関係で発生しているエラーらしく、以前からあるソースコードが怪しいため比較的最近追加された ruby_parser.c ではなさそう

そこで compile.c のソースコードを grep すると該当しそうなところがありました。

github.com

iseq_compile_pattern_each という関数の中でエラーメッセージを出力して、パターンマッチで受け取ったパターンのASTノードに合わせて ISeq に変換しているようです。またエラーメッセージと合わせて考えると、 NODE_ONCE というASTノードにマッチした時の処理がないためエラーになっていそうですね。

次に再現コードを作成し、最新のRuby(ruby 3.4.0dev (2024-02-01T12:17:37Z master 8531ac3115) [x86_64-linux])で実行してみました。

v = "117"

case "117"
in /#{v}/o
  p "Passed"
end

無事、コンパイルエラーになり再現できました。

test.rb:4: NODE_IN: unknown node (NODE_ONCE)
test.rb: compile error (SyntaxError)

その後、再現コードがどういったASTノードを生成しているのか dump してみました。

$ ruby --dump parsetree test.rb

以下が dump した結果になります。

###########################################################
## Do NOT use this node dump for any purpose other than  ##
## debug and research.  Compatibility is not guaranteed. ##
###########################################################

# @ NODE_SCOPE (id: 15, line: 1, location: (1,0)-(6,3))
# +- nd_tbl: :v
# +- nd_args:
# |   (null node)
# +- nd_body:
#     @ NODE_BLOCK (id: 13, line: 1, location: (1,0)-(6,3))
#     +- nd_head (1):
#     |   @ NODE_LASGN (id: 0, line: 1, location: (1,0)-(1,9))*
#     |   +- nd_vid: :v
#     |   +- nd_value:
#     |       @ NODE_STR (id: 1, line: 1, location: (1,4)-(1,9))
#     |       +- nd_lit: "117"
#     +- nd_head (2):
#         @ NODE_CASE3 (id: 12, line: 3, location: (3,0)-(6,3))*
#         +- nd_head:
#         |   @ NODE_STR (id: 2, line: 3, location: (3,5)-(3,10))
#         |   +- nd_lit: "117"
#         +- nd_body:
#             @ NODE_IN (id: 11, line: 4, location: (4,0)-(5,12))
#             +- nd_head:
#             |   @ NODE_ONCE (id: 7, line: 4, location: (4,3)-(4,10))
#             |   +- nd_body:
#             |       @ NODE_DREGX (id: 6, line: 4, location: (4,3)-(4,10))
#             |       +- nd_lit: ""
#             |       +- nd_next->nd_head:
#             |       |   @ NODE_EVSTR (id: 4, line: 4, location: (4,4)-(4,8))
#             |       |   +- nd_body:
#             |       |       @ NODE_LVAR (id: 3, line: 4, location: (4,6)-(4,7))
#             |       |       +- nd_vid: :v
#             |       +- nd_next->nd_next:
#             |           (null node)
#             +- nd_body:
#             |   @ NODE_FCALL (id: 8, line: 5, location: (5,2)-(5,12))*
#             |   +- nd_mid: :p
#             |   +- nd_args:
#             |       @ NODE_LIST (id: 10, line: 5, location: (5,4)-(5,12))
#             |       +- as.nd_alen: 1
#             |       +- nd_head:
#             |       |   @ NODE_STR (id: 9, line: 5, location: (5,4)-(5,12))
#             |       |   +- nd_lit: "Passed"
#             |       +- nd_next:
#             |           (null node)
#             +- nd_next:
#                 (null node)

これを見ると NODE_ONCE は動的に生成された正規表現用の NODE_DREGEX をラップしていることがわかります。

NODE_ONCE は6年ほど前に導入されたもので、 /regexp/o の時にのみ扱うASTノード ということも以下の記事から分かりました。

ruby-trunk-changes.hatenablog.com

また NODE_DREGX は以下の部分でパターンマッチ用のISeqを生成しているようでした。NODE_LIT(リテラル用のNODE)やNODE_STR なども同じ部分で判定しているようです。

github.com

ここまでのことから以下のことが分かりました。

  • パターンマッチ用のISeq生成処理でNODE_ONCEのケースがないため、すり抜けてエラーになっている
  • NODE_ONCENODE_DREGX をラップしているだけの NODE なので他への影響範囲は小さそう
  • NODE_DREGXと同じ部分にはNODE_LITやNODE_STRなどもあることから、そこへNODE_ONCEを渡してやればよさそう

これらの調査結果をもとに対応に進めます。

対応

iseq_compile_pattern_eachNODE_ONCE のケースでもISeqを生成するように修正してみました。すると以下の再現コードを実行できるようになりました。

v = "117"

case "117"
in /#{v}/o
  p "Passed"
end
# => Passed

あとはテストケースも追加してPull Requestを作成しました。

github.com

先日無事マージされ、現在のRubyのmasterでは修正されています。

反省点

無事修正できましたが反省点もありました。

https://bugs.ruby-lang.org にチケットを作成していなかった点です。

チケットを作成しておくことで後からチケットを見た時にどういった経緯だったかが確認しやすくなりますし、bugs.ruby-lang.org にはバックポートが必要かどうかをチケットに記載できたりもします。 そのため後からバックポートが必要な修正だった場合に、コミッターの方々が対応しやすくなる可能性があります。

こういった点からチケットを起票していなかった点は、良くないですね。

今後はチケットを起票するべきかなども考慮しつつ、対応を進められればと思いますね。


永和システムマネジメントでは、Rubyへのコントリビューションを通じてコミュニティと成長したいエンジニアを絶賛募集しています。

agile.esm.co.jp

サーバー依存のGPG errorを解決する

docker buildできない

宮崎からお送りします。yoshinoです。

私が担当しているプロジェクトでは、デプロイするために、お客様の管理するサーバー上でdockerイメージをbuildする必要があります。

先日、ローカルでは問題なくdocker buildできるのに、お客様サーバー上ではbuildできないという事象が起きて、試行錯誤しながら解決できましたので共有します(同僚のtaijuさんに助けてもらい解決できました)。

ローカルとサーバーではDockerfileの定義も少し異なり、本番用のDockerfileではhttps_proxyオプションを利用しており、 はじめはこの差異が原因かと考えたのですが、原因は別のところにありました。

docker buildで生じるエラー内容は以下のようになっていました。

Step 11/22 : RUN apt-get update
 ---> Running in 66ec271bf6ce
Get:1 http://deb.debian.org/debian bookworm InRelease [151 kB]
Get:2 http://deb.debian.org/debian bookworm-updates InRelease [52.1 kB]
Get:3 http://deb.debian.org/debian-security bookworm-security InRelease [48.0 kB]
.
.
W: GPG error: http://deb.debian.org/debian bookworm InRelease: The following signatures couldn't be verified because the public key is not available: NO_PUBKEY .......
E: The repository 'http://deb.debian.org/debian bookworm InRelease' is not signed.

GPG error?

the public key is not availableと怒られているので、GPGのpublic keyまわりで何か問題が起きていることがわかります。
ローカルではエラーが起きていないので、単純なpublic key不足ではないだろうなと思いつつ調査します。

Dockerのコンテナに入りGPGのpublic keyが存在するかを確認すると、問題の原因につながりそうなヒントが得られました。

> apt-key list

ローカルでは登録されているpublic key が表示されているのですが、サーバーでは何も返ってこなかったのです(エラーはこの時点では起きませんでした。エラー起きて欲しかった。)

改めてpublic keyが入っているはずのディレクトリをサーバー上で確認してみます。

> ls /etc/
ls: cannot access '/etc/': Operation not permitted

!!!!?

ディレクトリにパーミッションの関連でアクセスできない事象が発生していることがわかりました。

調べてみると、Dockerへの権限付与を行う箇所の問題であるようで、すべてのシステムコールが利用可能になるようなオプション(--security-opt="seccomp=unconfined")を付与して実行すると、buildが成功することを確認できました!!!

> docker run --security-opt="seccomp=unconfined" -it test bash
> apt-get update

原因と修正方法

原因と修正方法の話の前に、seccompというカーネル機能について知っておく必要があります(突然の「Operation not permitted」—Dockerが採用するセキュリティ機構「Seccomp」とは何か?を参考にさせていただきました)。

  • Seccompとは、Linuxカーネルが持つセキュリティ機構の一つで、Secure Computing Modelの略です。簡単に言うと、Seccompはシステムコールの許可・不許可を設定できるようにし、危険なシステムコールを実行できなくするためのものです。
  • Seccompはカーネルの機能であり、Dockerはカーネルとの仲介にlibseccompライブラリを使っています。

つまり、原因は以下のように推察されます。

Dockerで使用しているruby:3.2.2のコンテナではlsコマンドは「lstat」ではなく「statx」というシステムコールを使用しており、Dockerが起動しているサーバーではこのシステムコールが禁止されているため、operation not permitted が出力された。

Dockerが起動しているサーバーのlibseccompのバージョンが古いことにより、システムコールのルール制御も古くなり、Docker上で利用するシステムコールとのミスマッチが起きていることが原因だと考えられます。したがって、修正方法は2つあることがわかります。

1つ目は「linuxのバージョンを下げる」ことです。
例えば、Dockerのimageをruby:3.2.2-busterに指定するだけで対応可能です。
この対応でも動くことは確認できましたが、LTS のサポート期間は2024-06-30 までなので、一時的な対応となってしまいます。

2つ目は「対象サーバーのlibseccompのバージョンを上げる」ことです。
今回はこちらの方法を採用して、古かったバージョン(libseccomp-2.3.1-3.amzn2.0.1.x86_64)を最新バージョン(libseccomp-2.5.2-1.amzn2.0.1.x86_64)にアップデートすることで対応しました。

おわりに

実はこの問題が生じたのは、imageのバージョンを変更した時点ではありませんでした(そのため原因推測がより難しかった)。
はっきりと認識できているわけではないのですが、問題が生じたタイミングはdocker のcacheを削除した時点だと思います。 以下は推測になります。

ruby:3.1 -> ruby:3.2のようにイメージを変更した場合、低レイヤの部分(linux部分?)はcacheされ、そのまま使われ上位レイヤだけ変更される。そのため、例えばruby:3.1 で古いlinuxバージョンがcacheされruby:3.2では新しいバージョンのlinuxが使われているにもかかわらず、build時はcacheが使われて気がつかない。そして、cacheを削除した段階で、新しいlinuxバージョンが利用されるようになり、問題が顕在化する。

運用という最上流工程にたずさわる*1、同士の皆様の助けに少しでもなることがあれば、 著者としてこれ以上の喜びはありません。 現場からは以上です。