あどけない話

Internet technologies

我田引水的な「関数プログラミングの入門」資料紹介

これは、Haskell Advent Calendar 2021の2日目を埋めるために書いた記事です。実は単に僕が作った「関数プログラミングの入門」の資料の宣伝です。

ちなみに、僕の関数プログラミングの定義は「不変データプログラミング」であり、おそらく最も厳しい定義です。なので内容が分かれば、関数プログラミングに入門できた言ってもよいのではないかと思います。

関数プログラミングことはじめ

僕は毎年、岡山大学の三年生に向けて、2コマで関数プログラミングを教えています。その資料が、「Cプログラマーのための関数プログラミングことはじめ」です。岡山大学工学部情報系学科の学生は、C言語を習っているので、C言語に似た文法を独自に定義して、関数プログラミングを説明しています。

[入門]関数プログラミング

[入門]関数プログラミングは、WEB+DB PRESS Vol.67に掲載された記事です。編集部のご厚意により、オンラインで公開していただきました。関数型言語は記述力が高いので、この少ない分量で、赤黒木やパーサの実装まで辿り着けています。利用している言語はHaskellなので、Haskellの入門としても使えます。

再帰ドリル

再帰ドリルは、宮崎大学での集中講義の資料です。関数プログラミングとは切っても切り離せない再帰をドリル形式で解説しています。分かりやすく、包括的な内容になっていると思います。使用言語はHaskellです。コードを読めば、Haskellでのテストの書き方も習得できるかもしれません。

GHCのIOマネージャの歴史と僕の苦悩

これは、Haskell Advent Calendar 2021 の8日目の記事です。

Haskellコンパイラとして事実上一択となったGHCには、「軽量スレッド」が実装されています。軽量スレッドは、ネイティブスレッドよりも軽量なスレッドで、他の言語では「グリーンスレッド」とも呼ばれています。Haskellerが並行プログラミングをするときは、軽量スレッドを息を吸うかのように使います。

複数の軽量スレッドの入出力を束ねるのが、IOマネージャです。IOマネージャも単なる軽量スレッドであり、OSから入出力のイベントを受け取り、それぞれの軽量スレッドにイベントを通知します。

軽量スレッド(っぽい)機能を提供する他の言語では、GHCのIOマネージャを参考にしているようです。僕はIOマネージャの開発に深く関わっています。この記事ではIOマネージャの歴史をまとめるとともに、主にmacOSでの実装に関する苦悩を備忘録として残します。

第1世代

  • 論文 "Extending the Haskell Foreign Function Interface with Concurrency" で解説されています。
  • 2005年3月にリリースされたGHC 6.4で導入されました。

pollシステムコールを使って実装されています。pollはイベントの登録と監視が一体となったシステムコールです。このため、ある軽量スレッドが新たに入出力の登録を依頼する場合は、ブロックされているIOマネージャを一旦起こさないといけません(pollをやめさせなければなりません)。

これを実現するための画期的なアイディアが、wakeupパイプです。IOマネージャは、監視対象としてwakeupパイプも指定して、pollを呼びます。軽量スレッドがwakeupパイプにバイト列を書き込めば、IOマネージャはpollから抜けることができ、新たなイベントを加えて再度pollを呼び出します。

pollシステムコールの弱点は以下の通りです。

  • 登録できるイベントの個数に制限がある。
  • 受け取ったイベントを線形探索する必要があるので、イベントの個数が多くなるとスケールしない。

第2世代

  • 論文 "Scalable Event Handling for GHC" で解説されています。
  • 2010年11月にリリースされたGHC 7.0で導入されました。

pollの問題点を克服するために、Linuxではepoll、BSDではkqueueを使うようになりました。epollやkqueueは、pollのスーパーセットという限定的な利用方法が用いられています。

第2世代のIOマネージャの欠点は、マルチコア環境で性能が出ないことです。この理由としては、グローバルロックが使われていたことなどが挙げられます。

第3世代

  • 論文 "Mio: A High-Performance Multicore IO Manager for GHC" で解説されいます。
  • 2014年4月にリリースされたGHC 7.8で導入されました。

マルチコア環境ででスケールさせるために、グローバルロックが分割されると共に、コアごとにIOマネージャが起動されることになりました。

また、デフォルトではwakeupパイプは利用されなくなりました。これが実現できるのは、epollやkqueueが、イベントの登録と監視を独立させているからです。たとえば、epollでは登録がepoll_ctlで、監視はepoll_waitを使います。IOマネージャがepoll_waitでブロックされていても、別のスレッドはepoll_ctlでイベントを登録できるのです。登録されたイベントは削除しないといけませんが、削除の手間を省くために、使われたら削除される「one shot」というモードが利用されています。

Windows

WindowsのIOマネージャに関しては詳しくないので、New Windows I/O manager in GHC 8.12を参照してください。GHC 8.12は、GHC 9.0に置き換えて読んでください。

嗚呼、macOS

第3世代のIOマネージャは、Andreas Voellmyさんがepoll版を開発し、僕がkqueueに移植しました。この移植後、macOS上でGHCの並列ビルドが失敗するようになるという問題が発生しましたBSDでは問題ないのに、macOSでは問題が生じます。この問題は結局解決できずに、macOS上ではone shotモードを諦めて、wakeupパイプを使い続けるという方法で回避されました。(FreeBSDなどでは、one shotモードが使われています。)

その後、kqueueのIOマネージャに書き込みイベントの登録が失敗するというバグが発見されます。epoll_ctlの EPOLLINEPOLLOUT はフラグ(ビットマスク)ですが、kqueue の EVFILT_READEVFILT_WRITE はフラグではありません。kqueueで読み書き両方を登録するには、読み込みイベントと書き込みイベントの2つを登録する必要があります。移植の際に、この事実に気づかずバグを入れ込んでいました。one shot用のコードでも、wakeupパイプ用のコードでも、このバグは直されました。GHC 8.4から恩恵にあずかれます。

これでmacOSの並列ビルドの問題も解決したと勘違いして、macOSでone shotモードを使おうという提案をしました。しかし、macOSでone shotを使うようになったGHC 9.0.1をmacOSで使ってみると、ネットワーク関係のライブラリでテストが通らなくなっていました

結局、macOSのkqueueには、one shotにバグがあるというのが僕の結論です。GHC 9.0.2では、再びwakeupパイプが利用されるようになります。

幅80cmで作る在宅勤務環境

ウチは6人家族なので、コロナ禍での在宅勤務は本当に手狭です。最終的には、あまり人の来ない寝室の敷布団の上に座って壁に寄りかかり、MacBook Proを膝に置いてプログラミングをしていました。

会社では標準的なオフィスチェアに座り、iMacの広い画面を見ながら、Happy Hacking Keyboard(HHKB)を2枚使って腕を肩幅に開いて快適に作業していました。布団に座っているのだと腰は痛くなるし、キーボード1枚では肩はこるし、仕様書を見ながらプログラミングするにはMacBook Proの画面は小さくて苦しいしで、「なんとかならないかなぁ」という状況でした。

最近、嫁が家を片付けて不要な物を捨ててくれたんですが、ふと見ると幅80cm可変棚の下にスペースができてるではありませんか。このスペースを仕事場としてもらって、遅ればせながら仕事の環境を整えることにしました。既存の持ち物をできるだけ利用して作った仕事環境の最終形はこんな感じです。

f:id:kazu-yamamoto:20210928060829j:plain
在宅勤務環境最終形

誰かの役に立つかもしれないので、備忘録も兼ねて、何を考えて環境を整えたのか記しておきます。

捨てようと思っていたIKEAの机を再利用することにしました。幅が110cmぐらいあるので、80cmに切断します。右側の引き出しは全部諦めて、天板をノコギリで切ってみるとスカスカで、拍子抜けがする程あっという間に切れました。

f:id:kazu-yamamoto:20210831094028j:plain:w250
右側の引き出しを取り除いたIKEAの机
f:id:kazu-yamamoto:20210831094924j:plain:w250
天板はスカスカ

ディスプレイ

次はなんといってもディスプレイです。大きさは、幅が61cmの27インチが丁度よさそうで、解像度はWQHD(2560x1440)で十分かなぁと思いました。いろいろ記事を読んでいると、最近だとUSBのType-Cケーブル一本で結ぶだけで、ノートブックに給電までできることを知りました。そんなとき、『EIZO「FlexScan EV2795」で理想を超える仕事環境が! ケーブル1本のデュアルディスプレイからパソコン3台の集約も』という記事を見付けて感動し、FlexScan EV2795の黒を買うことにしました。

僕のEV2795には以下のケーブルが刺さっています。

MacBook Proには、これらの(モニターライトを除いた)全ての情報/電力がType-Cから供給されます。EV2795には概ね満足していますが、Retinaディスプレイに慣れている目からすると、荒いドットが見えたりしてがっかりします。あと、前面下にあるソフトスイッチは、暗いときにも分かるように少し光って欲しかったです。

追記:Type-Cにはアップストリームとダウンストリームの2つがあります。パソコンはアップストリームに繋ぎます。マニュアルには、ダウンストリームは別のEV2795をデイジーチェインで繋ぐため用と書かれています。しかし、単に給電にも使えました。マニュアルにもそう明記してあるべきですね。どなたか、PDに対応しているかチェッカーで調べていただきたいです。

ディスプレイ台

EV2795のスタンドはよくできているんですが、僕はディスプレイの中心を机から50cmぐらいにもってきたいので、高さがあと10cmぐらい足りません。そこで、モニターアームを調べてみました。ヨドバシで実際に触ってみると、50cmも持ち上げられないことに気づきました。モニターアームは、モニターを頻繁に動かす人には便利ですが、そもそも僕は動かさないので本当に必要なのか分からなくなっていました。そんなとき、「モニターアームを使って気づいたメリット・デメリット。モニターアームの選び方。」という記事を見付け、高級なディスプレイのスタンドがあればモニターを好きな位置や角度に固定できるのでモニターアームは不要という意見を聞いて納得しました。

結局10cmぐらい持ち上げられればいいので、ディスプレイ台で十分という結論に達し、BoYataのモニタースタンドを購入しました。これだと、書類を書くときにキーボードをモニタースタンドの下に押し込むことができて快適です。また、後から出てきますが、スタンドをクリップを挟む場所としても使えます。

キーボード

とりあえず、Happy Hacking Keybordの2枚利用を継続しました。Windowsは標準で2枚のキーボードに対応していますが、MacだとKarabiner-Elementsをインストールする必要があります。

机の上にケーブルが2つあるのは邪魔です。とりあえず、USB miniBのL字コネクタを買い、2つのケーブルを見栄えよく中央でまとめられるようにしました。買った左右のL字コネクタは、「変換名人」のUSBM5-LLFとUSBM5-RLFです。amazonでは1個で販売してないので、ヨドバシで買うのがいいと思います。

f:id:kazu-yamamoto:20210925080700j:plain:w250
右キーボードのUSBケーブルを中央に持ってくる

やっぱり机を広くするにはワイヤレスの分割キーボードが必要です。いろいろ調べたのですが、よさそうなのは自作キーボードしかありませんでした。子育てが忙しいので、ハンダ付けは勘弁して欲しいです。Mistel BAROCCO MD770にワイヤレスタイプがあるらしく、ヨドバシでワイヤレスではないMD770を触ってみましたが、どの色の軸も手に馴染みませんでした。メカニカルキーボードは自分には合わないと分かったとき急激にキーボード熱が冷め、結局HHKB 2枚が一番いいという結論に落ち着きました。

キーボードに関しては、『 疲れないキーボードを知りたい。タイピング早打ち日本一の「女王」miriさんに聞いてみた』という記事が入門としては秀逸です。

2つのケーブルをまとめるのには、ルフラップスリーブ(内径8mm)を使いました。

キートップ引き抜き工具を買ったら、娘が面白がってやってキーボードを掃除してくれました。次の目標は、黄ばんだHHKBを流行りのホワイトニングで白くすることです。

マウス

こだわりがなくワイヤレスならなんでもいいので、その辺にあったAppleMagic Mouseを使っています。

充電用のLightningの穴が下にあり、充電中は利用できないのは有名な話ですね。

僕はタイピングのときに、手のひらの付け根を机に置いています。机と接触する部分は汗ばんでくるので、これをなんとかしたく、80cmのマウスパッドを買いました。

パソコン立て

MacBook Proは閉じた状態、いわゆるクラムシェルモードで使っています。クラムシェルモードでは、熱を逃すために縦置きにした方がいいようです。僕の環境では机が狭いので必然的に立てることになりますが、左右に壁があるのでそれに立て掛ければ十分です。でも、ヨドバシでTwelve SouthのBookArcを見て欲しくなってしまったのです。

「単なるアルミに6,800円も払えるか!」と葛藤して、ついにかっこよくて安い「AVLTのノートパソコン縦置きスタンドホルダー」を見付けて買いました(現在、amazonでは売ってないようです)。娘には「こんなのが2,300円もするの?」と言われました。

MacBook Proを壁に寄せるにはType-CのL字コネクタも必要です。Type-Cだと左右がないのがいいですね。僕はカモンのUC-Lを買いました。ちょっときつめです。

f:id:kazu-yamamoto:20210925081239j:plain:w250
Type-CのL字コネクタを使って壁に寄せる

モニターライト

FlexScan EV2795の背中は丸みを帯びているので、モニターライトは設置できるか慎重に選ばないといけません。結局、元祖であるBenQ ScreenBarにしようかなぁと思っていたときに、Exarm Zetaの存在を知りました。高いけれどライト専門の会社だし、日本の会社でサポートもしっかりしていそうなので、これを買いました。

いろんな記事に、暗い部屋でディスプレイだけ付けていると目が疲れるという意見が書かれていました。僕は気にしてなかったのですが、確かにモニターライトが机を照らし、反射光がディスプレイの後ろの壁を照らして全体的に明るいと、目が疲れにくい気がします。

届いたときは軸がずれていて、どうやっても向かって右側が下に傾いていました。スワン電器のサポートからは、着払いで送ってくれれば調整するとのことだったので、さっそくお世話になりました。やはり、日本の会社にしておいてよかったです。

FlexScan EV2795には、下側に主電源があり、前面下にソフトスイッチがあります。ソフトスイッチを消すと画面が消えると共にUSBの給電が止まるので、モニターライトが消えます。逆にソフトスイッチを入れると給電が始まりますが、Exarm Zetaは横の飾りのライトが付くだけです。メインのライトを付けるには、センサーに手をかざす必要があります。

僕の環境のようにExarm Zetaのすぐ上に白い棚があると、ここにiPhoneの光が反射して、メインのライトが付いたり消えたりと誤動作します。まぁ、これは仕様なので仕方ないですね。僕にはセンサーのスイッチ機能は不要でした。

リモート会議環境

リモート会議に関してまず気になるのが、マイクとスピーカーです。いろんなパターンを試しましたが、iPhoneに付いてくるマイク付きのイヤホンをMacBook Proに挿して使うことにしました。これだとハウリングしません。

Webカメラを買うのはもったいないので、EpocCamを購入してiPhoneWebカメラにしました。結局、iPhoneのマイクは使わないことになったので、無償版の方でも十分だった思います。

Exarm Zetaには顔用のライトが付いていますが、これだと光量が足りません。iPhoneのホルダーも付いているリングライトを買うことにました。ヨドバシに置いてあったエレコムのモニターライトが腕がしっかりしていてよさそうでした。クリップタイプを買って、モニター台にクリップ止めしています。厚みが足らなかったので、黒く塗った板をクリップに挟んでいます。

電源タップ

電源タップは不要といえば不要だったのですが、配線をスッキリさせるために以下を満たすものを探しました。

  • ディスプレイの3ピンプラグが刺さること
  • 口の数は、余裕を持って3つあること
  • Type-A で給電できること
  • PD 対応の Type-C の口があること

これを満たすのは Anker PowerPort Strip PD 3 でした。amazonでは、「給電できなくなる」という悪い評判が立っていましたが、Ankerなので買ってみました。今のところ、iPhoneが急速充電できています。

【レビュー】Anker『PowerPort Strip PD 3』:机固定に最適なUSB-C搭載マルチ電源タップ」を読むと、Type-C の PD は若干仕様に準じていないようです。自分で作った規格ぐらい準拠しましょうよ、Anker さん

スイングプラグではないので、アダプターも必要です。

椅子

ゲーミングチェアが欲しかったので、ニトリにもあると聞いて行ってみました。ゲーミングチェアに座ると、なるほど柔らかくて長く座っていられそうですが、腰はパッドで単に押されているだけで楽には感じませんでした。隣にあった、デュオレハイは不恰好でなんじゃこれって感じだったのですが、試しに座ってみると衝撃を受けました。腰を持ち上げてくれ、重力が弱くなった感じで、とっても楽なんです。ニトリの商品には、デュオレハイ 2デュオレハイ DXがあります。

デュオレハイ2:

  • 肘の部分が固定
  • 首が高くならない
  • 平日だと送料がかからない

デュオレハイ DX:

  • ポリエステル
  • 肘の高さが調整できる
  • 首が高くなる
  • 平日でも送料がかかる

僕は身長が182cmあるので、デュオレハイ DX一択でした。本当は皮がよかったんですが。。。しばらく使っていますが、本当に腰が楽です。

昇降デスク (野望)

もし、もう少し広いスペースがあれば、昇降デスクが欲しいです。重いものを乗せないのでシングルモーターでよく、コストパフォーマンス的にはMAIDESITEの昇降デスクがよさそうです。天板は厚み2.5cmの一枚板です。注目していたら、ここ1ヶ月で高評価のレビューがたくさん付くようになりましたね。タイムセールで安く買えるときもあります。110cmの黒色が出れば最高なんですが。サポートが気になるので、レビューを見守りたいと思います。

所感

  • 幅80cmのスペースがあれば、なんとかなる
  • キーボードと椅子だけは、実際に触ったり座ったりしてから買いましょう

Releasing QUIC and HTTP/3 libraries

As I described in The Current Plan for Haskell QUIC, I have released the followings:

tls

tls v1.5.5 provides the Network.TLS.QUIC module. If you are interested in how this module has been improved, please read Improving QUIC APIs of the TLS library in Haskell.

http2

As I explained in Implementing HTTP/3 in Haskell, http2 v3.0.0 or later provides both client and server libraries with the abstraction for HTTP requests and responses. These version resist to some HTTP/2 DoS attacks.

quic

quic provides the QUIC APIs based on Haskell's lightweight threads. The architecture described in Implementation status of QUIC in Haskell is still valid. Runner modules for client and server are divided into Network.QUIC.Client and Network.QUIC.Server, respectively.

According to private discussion after Migration API for QUIC clients, Network.QUIC.Client provides both automatic migration and manual migration.

As explained in Developing QUIC Loss Detection and Congestion Control in Haskell, this library implements the congestion control defined in RFC9002.

http3

This library provides HTTP/3 (on QUIC). The encoder does not use the dynamic table at this moment.

warp-quic

warp-quic library is to provide Web Application Interface(WAI) to HTTP/3. In other words, this is a QUIC wrapper for Warp.

mighttpd2

mighttpd2 version 4.0.0 now provides the HTTP/3 (on QUIC) functionality based on warp-quic. Also, the configuration is now based on Dhall.

To create UDP connected sockets on Linux, mighttpd2 drops capabilities except CAP_NET_BIND_SERVICE as described in Haskell vs Linux capabilities.

IIR

I wrote an article about implementation of QUIC in Haskell in Internet Infrastructure Review(IIR)Vol.52. This article is written in Japanese but will be translated into English within a month.

Migration API for QUIC clients

If I understand correctly, most QUIC implementations of clients and servers uses unconnected UDP sockets with sendto()/sendmsg() and recvfrom()/recvmsg(). For the server side, this is probably because they adopt event-driven programming. The event loop calls recvfrom()/recvmsg() and dispatches a received packet according to the peer address.

As I explained in "Implementation status of QUIC in Haskell", the quic library in Haskell made use of connected sockets in both the client and server sides. Perhaps, this is a good thing for the server side first because lightweight thread programming is common in Haskell and second because dispatching is done in the kernel.

But what about the client side? Since RFCs relating to QUIC are published, I'm trying to fix API of the quic library for the first official release. What kind of migration API should be provided for clients?

Let's consider this typical scenario:

  1. A QUIC client is using a 5G network.
  2. The client moves to the place where WiFi is available.
  3. The client migrates the connection from the 5G network to the WiFi network.

If the clients uses a connected socket, this migration can be implemented as follows:

  1. The client needs to detect the event that the WiFi network interface is available.
  2. The client creates a new connected socket. When connect() is called, the kernel sets the remote address/port according to the argument. Then it looks up the routing table with the server's IP address. Since the WiFi network interface is resolved, the kernel sets the local address of the socket to the IP address of the network interface. A local port number is chosen randomly.
  3. The client starts sending packets through the new socket with send().

It's easy for the quic library to provide the API for item 2. But how to implement item 1? Do major OSes provide such API for network interfaces? Should we prepare a watch-dog thread for network interface events?

I have been wondering why other implementors do not talk about this issue. I finally realized that I went the wrong way. That is, unconnected socket should be used in the client side. If sendto()/sendmsg() is used with a unconnected socket, connection migration is done automatically:

  • When sendto()/sendmsg() is called, the kernel sets the destination address/port of the packet according to the argument. Then it looks up the routing table with the server's IP address. Since a proper network interface is resolved, the kernel sets the source address of the packet to the IP address of the network interface. A local port number is chosen randomly at the first sendto()/sendmsg().

Conclusion: connection migration can be done without any specific API.

Seeking the reasons for segfaults of a Haskell program

My open server of Haskell QUIC on Linux sometimes got segfaults. I saw two types of segfaults. One is a simple segfault by accessing a wrong address:

mighty: segmentation fault

The other is relating to free():

*** Error in `mighty': corrupted double-linked list: 0x00007fcdf0008f90 ***

I guessed that a buffer overrun occurred against a buffer allocated by malloc() and this segfault happened when the buffer is freed.

Many Haskellers would be surprised at this kind of segfaults because it is hard to cause segfaults in normal Haskell programming. However, if you manipulate pointers or use unsafe functions, segfaults are usual like other programming language.

For the first type of segfault, you can use Foreign.Storable.peek:

% ghci
> import Foreign.Ptr
> import Foreign.Storable
> :type peek
peek :: Storable a => Ptr a -> IO a

Let's try to access so-called NULL:

> peek nullPtr :: IO Int
sh: segmentation fault  ghci

Buffer overruns can be caused by Foreign.Storable.poke. Its type is as follows:

> :type poke
poke :: Storable a => Ptr a -> a -> IO ()

I checked all peeks and pokes in my code but I could not figure out the reasons of segfaults. So, I needed to take another approach.

The -g option of GHC

Like other compilers, GHC provides the -g option to add debug information to a complied program. We can run the program in gdb and get a back trace if a segfault happens. To compile all dependent libraries with the -g option, I modified my Cabal wrapper, called cab, to provide a command line option (whose name is also -g) to implement this feature. I also used the sandbox feature of Cabal-v1:

% cd mighty
% cab init             # creating a sandbox
% cab add ~/work/quic  # adding non-Hackage deps
...
% cab install -d -f tls -f quic -g
% cab conf -f tls -f quic -g
% cab build

Then run the complied program in gdb:

% sudo gdb --args mighty conf route
(gdb) handle SIGPIPE nostop noprint pass
Signal        Stop  Print   Pass to program Description
SIGPIPE       No    No  Yes     Broken pipe
(gdb) handle SIGUSR1 nostop noprint pass
Signal        Stop  Print   Pass to program Description
SIGUSR1       No    No  Yes     User defined signal 1
(gdb) run

As you can see, I needed to modify behavior of two signal handlers to ignore them:

Segfault 1

When I added some test cases of QPACK to h3spec and test the open server, gdb finally caught a segfault and showed a back trace. The reason is Data.Array.Base.unsafeAt. I did not check the boundary of an array! (My QPACK code is derived from my HPACK code where this boundary check is not necessary.)

Segfault 2

The segfault relating to free() was really mysterious because the buffer boundary is always checked when poke is used. The error message of free() on Linux is not so kind. But when I got the same segfault on macOS, the following message was displayed:

mighty(75755,0x700009519000) malloc: Incorrect checksum for freed object 0x7fb8de80ea00: probably modified after being freed.

Eureka! Even if the boundary is checked everytime, this segfault happens because a freed buffer is used.

But why is a freed buffer used? This is one of difficulties of multi-thread programming. Suppose thread A and thread B share a buffer. The following is an example clean-up procedure:

  • Thread A sends a kill signal to thread B
  • Thread A frees the buffer
  • Thread A exits

This looks perfect. However the timing of termination of thread B depends on the scheduler. Even after thread A freed the buffer, thread B is alive and can manipulate the buffer.

To prevent this contention, I gave up the approach of Foreign.Marshal.Alloc.mallocBytes and Foreign.Marshal.Alloc.free. Instead, I started using GHC.ForeignPtr.mallocPlainForeignPtrBytes. Buffers allocated by this function are GCed like ByteString.

Now I believe that my QUIC server gets much stabler than before.

Haskell vs Linux capabilities

I found an elegant solution for the problem of Haskell vs Linux capabilities explained in "QUIC and Linux capabilities". To know why the CAP_NET_BIND_SERVICE capability is necessary, please read this article in advance.

On Linux, the following is the procedure to boot a secure multi-threaded server with CAP_NET_BIND_SERVICE:

  • Executed by root.
  • Reading a TLS private key.
  • Setting SECBIT_KEEP_CAPS by prctl(2) -- Without this, all capabilities are lost after setuid(2).
  • Switching the root user to nobody (or something) by setuid(2).
  • Dropping capabilities except CAP_NET_BIND_SERVICE by capset(2).
  • Spawning native threads. CAP_NET_BIND_SERVICE is inherited by all native threads.

GHC RTS executes Haskell code after spawning native threads. So, there are two problems to implement a secure multi-threaded server with CAP_NET_BIND_SERVICE in Haskell.

  1. How to set SECBIT_KEEP_CAPS to all native threads?
  2. How to drop capabilities except CAP_NET_BIND_SERVICE of all native threads?

For 1), by reading the source code of GHC RTS, I finally found a C level hook called FlagDefaultsHook(). The user manual has the section of Hooks to change RTS behaviour, but this hook is not written, sign. GHC RTS executes this hook before spawning native threads. So, if the following code is linked your Haskell program, all native threads keeps all capabilities after setuid(2), yay!

void FlagDefaultsHook () {
  if (geteuid() == 0) {
    prctl(PR_SET_SECUREBITS, SECBIT_KEEP_CAPS, 0L, 0L, 0L);
  }
}

For 2), I considered that signals can be used. On Linux, we can get the thread IDs of all native threads in a process by scanning /proc/<process id>/task/. And Linux provides tgkill(2) to send a signal to the native thread specified a thread ID.

I first tried to use installHandler of Haskell to install a signal handler. But it appeared that an improper native thread catches the signal from tgkill(2), sigh. So, I used sigaction(2) again in FlagDefaultsHook().

The following is the procedure to implement a secure multi-threaded server with CAP_NET_BIND_SERVICE in Haskell:

  • Executed by root.
  • GHC RTS executes FlagDefaultsHook():
    • Setting SECBIT_KEEP_CAPS by prctl(2).
    • Setting a signal handler to drop capabilities except CAP_NET_BIND_SERVICE by sigaction(2).
  • GHC RTS spawns native threads.
  • GHC RTS executes Haskell code:
    • Reading a TLS private key.
    • Switching the root user to nobody (or something) by setuid(2).
    • Sending signals to all native threads to drop capabilities except CAP_NET_BIND_SERVICE by tgkill(2).

You can see a concrete implementation in this commit.

One awkward thing is that the capabilities of the process itself remains in a wrong value. It seems to me that capset(2) for a process is not permitted if it is multi-threaded. However, if I understand correctly, there is no way to access or inherit the capabilities of the process in GHC RTS. So, I don't care it so much.