tech.guitarrapc.cóm

Technical updates

straceでC#のシステムコールを覗いてみる

ClassMethodsさんの記事で、C,Go,Rust,Python,Node.jsのHello Worldプログラムをstraceで覗いていたのですが、C#も気になりませんか?私は気になるので見てみましょう。

C#のビルド成果物にはJITとAOTの2種類があり、これらの言語がVMを用いないことを考えると比較するならAOTでのstraceが適切です。ただ、せっかくなので両方を見てみましょう。

結論

記事が長いので、結論を書いておきます。

システムコール数の比較は次の通りです。C# (JIT + ICU無効)C# (JIT + ICU指定)のシステムコール数は本記事に載せていませんがリポジトリを参照してください。

実装 システムコール数
C# (JIT) 1159
C# (JIT + ICU無効) 940
C# (JIT + ICU指定) 985
C# (AOT) 470
C# (AOT + ICU無効) 228
C# (AOT + ICU指定) 290
Go 185
Rust 63
C 34

C#の起動時のシステムコールを減らすには、ICUの使用有無を指定するのが効果的です。

  • InvariantCultureで問題ない: DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=1
  • 多言語対応が必要: DOTNET_ICU_VERSION_OVERRIDE=<バージョン>で探索を削減

環境

今回の検証はguitarrapc/linux-straceリポジトリで公開しています。元の記事のリポジトリを参考にしつつ、C#のJIT/AOT両方や結果を記載し、再現が取れるようにコンテナで動かしています。気になる方は参照してください。

項目 バージョン
OS Ubuntu 24.04 (コンテナ)
C (gcc) 13.3.0
Rust 1.93.0
Go 1.24.1
.NET SDK 10.0.102
strace 6.8

検証

各言語でHello, Worldを書いていきます。C、Rust、Goは元記事と同じコードです。

C#

Console.WriteLine("Hello, World");

C

#include <stdio.h>

int main() {
    printf("Hello World\n");
    return 0;
}

Rust

fn main() {
    println!("Hello World");
}

Go

package main

import (
    "fmt"
)

func main() {
    fmt.Println("Hello World")
}

成果物の生成

成果物を生成する流れは本題ではないので、再現したい人向けに簡単に説明します。

C#は、JITとAOTはPublishAotの有無だけで切り替わります。publish時のパラメーターで指定してもいいのですが、わかりやすくするため、フォルダを分けて2つ用意しましょう。それぞれhello_csharp (JIT)とhello_csharp_aot (AOT)を使います。

C# (JIT)

JIT設定のhello_csharp.csprojです。ランタイムを自己完結型にして、単一ファイルにまとめる設定です。

mkdir hello_csharp
<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net10.0</TargetFramework>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
    <PublishSingleFile>true</PublishSingleFile>
    <SelfContained>true</SelfContained>
  </PropertyGroup>

</Project>

成果物をリリースビルドで作成します。

dotnet publish -c Release -r linux-x64 -o ./bin hello_csharp/hello_csharp.csproj

C# (AOT)

AOT設定のhello_csharp_aot.csprojです。JITとの違いは、<PublishAot>true</PublishAot>の追加だけです。

mkdir hello_csharp_aot
<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net10.0</TargetFramework>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
    <PublishSingleFile>true</PublishSingleFile>
    <SelfContained>true</SelfContained>
    <PublishAot>true</PublishAot>
  </PropertyGroup>

</Project>
dotnet publish -c Release -r linux-x64 -o ./bin hello_csharp_aot/hello_csharp_aot.csproj

C

gcc hello_c/main.c -o ./bin/hello_c

Rust

rustc hello_rust/main.rs -o ./bin/hello_rust

Go

静的リンク版(通常)と共有ライブラリ版1の2種類を作成します。

cd hello_go && go build -o ../bin/hello_go && cd ..
cd hello_go && go build -linkshared -o ../bin/hello_go_linkshared && cd ..

システムコールの統計を比較

元記事同様に、strace -cオプションでシステムコール数の統計を出します。

strace -c `./バイナリ`

使うバイナリはこちらです。各言語を見ていきましょう。

$ ls -l hello*
-rwxr-xr-x 1 guitarrapc guitarrapc    15960 Feb  9 18:32 hello_c
-rwxr-xr-x 1 guitarrapc guitarrapc 73522500 Feb  9 18:09 hello_csharp
-rwxr-xr-x 1 guitarrapc guitarrapc  1270792 Feb  9 18:18 hello_csharp_aot
-rwxr-xr-x 1 guitarrapc guitarrapc  2204715 Feb  9 18:32 hello_go
-rwxr-xr-x 1 guitarrapc guitarrapc  3893728 Feb  9 18:33 hello_rust

C

$ strace -c ./hello_c
Hello World
% time     seconds  usecs/call     calls    errors syscall
------ ----------- ----------- --------- --------- ----------------
 32.15    0.000109          13         8           mmap
 10.62    0.000036          12         3           mprotect
  8.55    0.000029          29         1           write
  6.49    0.000022           7         3           brk
  5.31    0.000018           9         2           close
  5.31    0.000018           6         3           fstat
  5.31    0.000018           9         2           pread64
  4.42    0.000015          15         1           munmap
  4.13    0.000014           7         2           openat
  2.95    0.000010          10         1           read
  2.65    0.000009           9         1           prlimit64
  2.65    0.000009           9         1           getrandom
  2.36    0.000008           8         1           arch_prctl
  2.36    0.000008           8         1           set_tid_address
  2.36    0.000008           8         1           set_robust_list
  2.36    0.000008           8         1           rseq
  0.00    0.000000           0         1         1 access
  0.00    0.000000           0         1           execve
------ ----------- ----------- --------- --------- ----------------
100.00    0.000339           9        34         1 total

Rust

$ strace -c ./hello_rust
Hello World
% time     seconds  usecs/call     calls    errors syscall
------ ----------- ----------- --------- --------- ------------------
 18.45    0.000119           9        13           mmap
  9.46    0.000061          12         5           mprotect
  8.84    0.000057          11         5           read
  7.91    0.000051          12         4           close
  7.13    0.000046           9         5           rt_sigaction
  7.13    0.000046          11         4           openat
  6.67    0.000043          21         2           munmap
  4.03    0.000026           8         3           brk
  4.03    0.000026           8         3           sigaltstack
  3.72    0.000024           6         4           fstat
  3.26    0.000021          21         1           write
  3.26    0.000021          10         2           pread64
  3.26    0.000021          21         1           getrandom
  2.79    0.000018           9         2           prlimit64
  1.86    0.000012          12         1           poll
  1.55    0.000010          10         1           sched_getaffinity
  1.40    0.000009           9         1           arch_prctl
  1.40    0.000009           9         1           gettid
  1.40    0.000009           9         1           set_tid_address
  1.24    0.000008           8         1           set_robust_list
  1.24    0.000008           8         1           rseq
  0.00    0.000000           0         1         1 access
  0.00    0.000000           0         1           execve
------ ----------- ----------- --------- --------- ------------------
100.00    0.000645          10        63         1 total

Go

$ strace -c ./hello_go
Hello World
% time     seconds  usecs/call     calls    errors syscall
------ ----------- ----------- --------- --------- ------------------
 54.59    0.001041           9       114           rt_sigaction
 14.53    0.000277          15        18           rt_sigreturn
 10.44    0.000199          39         5         1 futex
  7.03    0.000134          67         2           clone
  4.09    0.000078          13         6           fcntl
  3.15    0.000060          10         6           rt_sigprocmask
  1.73    0.000033           1        20           mmap
  1.68    0.000032          16         2           prlimit64
  1.31    0.000025          25         1           write
  1.05    0.000020          10         2           sigaltstack
  0.42    0.000008           8         1           gettid
  0.00    0.000000           0         1           read
  0.00    0.000000           0         1           close
  0.00    0.000000           0         2           madvise
  0.00    0.000000           0         1           execve
  0.00    0.000000           0         1           arch_prctl
  0.00    0.000000           0         1           sched_getaffinity
  0.00    0.000000           0         1           openat
------ ----------- ----------- --------- --------- ------------------
100.00    0.001907          10       185         1 total

C# (AOT)

$ strace -c ./hello_csharp_aot
Hello, World
% time     seconds  usecs/call     calls    errors syscall
------ ----------- ----------- --------- --------- ------------------
 33.24    0.001192           9       129        83 openat
 14.89    0.000534           8        61           mmap
 12.97    0.000465          17        27           mprotect
  7.45    0.000267          12        22           munmap
  6.53    0.000234           5        44           fstat
  5.94    0.000213           4        45           close
  4.35    0.000156           9        16        12 newfstatat
  3.12    0.000112           2        38           read
  2.51    0.000090           5        18           rt_sigaction
  2.29    0.000082          13         6           rt_sigprocmask
  1.06    0.000038           9         4           ioctl
  0.98    0.000035          17         2           write
  0.98    0.000035          17         2           clone3
  0.70    0.000025           8         3           futex
  0.56    0.000020           1        17           madvise
  0.47    0.000017          17         1           pipe2
  0.31    0.000011          11         1           lseek
  0.28    0.000010           3         3           pread64
  0.25    0.000009           2         4           brk
  0.25    0.000009           3         3           sched_getaffinity
  0.22    0.000008           2         3           getpid
  0.22    0.000008           8         1           fcntl
  0.22    0.000008           8         1           gettid
  0.22    0.000008           2         4           prlimit64
  0.00    0.000000           0         1         1 access
  0.00    0.000000           0         1           execve
  0.00    0.000000           0         1           sysinfo
  0.00    0.000000           0         2           statfs
  0.00    0.000000           0         1           arch_prctl
  0.00    0.000000           0         2           getdents64
  0.00    0.000000           0         1           set_tid_address
  0.00    0.000000           0         1           get_mempolicy
  0.00    0.000000           0         1           set_robust_list
  0.00    0.000000           0         1           getrandom
  0.00    0.000000           0         2           membarrier
  0.00    0.000000           0         1           rseq
------ ----------- ----------- --------- --------- ------------------
100.00    0.003586           7       470        96 total

C# (JIT)

$ strace -c ./hello_csharp
Hello, World
% time     seconds  usecs/call     calls    errors syscall
------ ----------- ----------- --------- --------- ------------------
 46.49    0.002176           6       343           mprotect
 18.44    0.000863           5       161           mmap
  9.25    0.000433           2       163        92 openat
  4.10    0.000192           5        37           munmap
  3.18    0.000149           3        38        34 readlink
  2.88    0.000135           6        21           pread64
  2.67    0.000125           1        67           fstat
  2.63    0.000123           5        22           fcntl
  2.22    0.000104           6        15           rt_sigprocmask
  1.60    0.000075           8         9           stat
  1.30    0.000061           1        61           close
  1.22    0.000057           8         7           clone3
  1.03    0.000048           2        24           rt_sigaction
  0.58    0.000027           0        58           read
  0.43    0.000020           3         6         6 access
  0.41    0.000019           1        11           brk
  0.34    0.000016           8         2           pipe2
  0.28    0.000013           1         9           futex
  0.26    0.000012           3         4           ioctl
  0.21    0.000010           2         5           getpid
  0.19    0.000009           1         8           write
  0.19    0.000009           0        23           madvise
  0.11    0.000005           5         1           lseek
  0.00    0.000000           0         1           socket
  0.00    0.000000           0         1           bind
  0.00    0.000000           0         1           listen
  0.00    0.000000           0         1           execve
  0.00    0.000000           0         1           ftruncate
  0.00    0.000000           0         5         2 unlink
  0.00    0.000000           0         1           fchmod
  0.00    0.000000           0         2           sysinfo
  0.00    0.000000           0         1           getsid
  0.00    0.000000           0         2           sigaltstack
  0.00    0.000000           0         2           statfs
  0.00    0.000000           0         1           arch_prctl
  0.00    0.000000           0         1           gettid
  0.00    0.000000           0         4           sched_getaffinity
  0.00    0.000000           0         2           getdents64
  0.00    0.000000           0         1           set_tid_address
  0.00    0.000000           0         1           get_mempolicy
  0.00    0.000000           0         2           mknodat
  0.00    0.000000           0        16        12 newfstatat
  0.00    0.000000           0         1           set_robust_list
  0.00    0.000000           0        10           prlimit64
  0.00    0.000000           0         1           getrandom
  0.00    0.000000           0         1           memfd_create
  0.00    0.000000           0         4           membarrier
  0.00    0.000000           0         1           rseq
------ ----------- ----------- --------- --------- ------------------
100.00    0.004681           4      1159       146 total

元記事に比べてGoのシステムコール数が減っています。(911 -> 185前後) straceは決定論的に結果が出るわけではないので、実行ごとに多少のばらつきがありますが、明らかに違いがあります。Goのバージョンは元記事が1.24.0、今回が1.24.1なので、このような差が生まれる要因とは考えにくく、実際記事のリポジトリにあるコンテナを用いても本記事と同様の190前後でした。おや?

本記事で追加したC# (JIT)とC# (AOT)ですが、JITは1159回、AOTは470回と大きな差があります。AOTはネイティブコードに変換されているので、JITのように実行時にコードを生成する必要がなく、その分システムコール数が減っています。これは、あとから見るwriteの数の違いでも分かります。

C#のopenatシステムコールとICU

C#はAOTにしてもまだシステムコールが大きく、特にopenatの数が多いです。openat/readlink/accessはerrorsがあるのも気になります。 straceでopenatを調べてみると、openat(…libicu*.so…)ログから、プログラムロード前にICU(国際化対応ライブラリ)のデータファイルを探しに行っていることが分かります。glibc-hwcapsバージョンごとにICUバージョンを90 > 89 > 88 ... 74と探索するため、openatが大きく増え、ICUファイルが見つからなければENOENTが出てerrorがカウントされます。最後にlibicuuc.so.74が見つかった瞬間から、libicudata.so.74libicui18n.so.74のロードに進んでいます。

結果、(候補バージョン数) × (探索ディレクトリ数) × (hwcaps分岐)でopenatもエラーが増えているわけです。

$ strace -f -e trace=openat,newfstatat,access -s 200 ./bin/hello_csharp_aot 2>&1
access("/etc/ld.so.preload", R_OK)      = -1 ENOENT (No such file or directory)
openat(AT_FDCWD, "/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 3
openat(AT_FDCWD, "/lib/x86_64-linux-gnu/libm.so.6", O_RDONLY|O_CLOEXEC) = 3
openat(AT_FDCWD, "/lib/x86_64-linux-gnu/libc.so.6", O_RDONLY|O_CLOEXEC) = 3
openat(AT_FDCWD, "/sys/devices/system/cpu/online", O_RDONLY|O_CLOEXEC) = 3
openat(AT_FDCWD, "/proc/self/mountinfo", O_RDONLY) = 3
openat(AT_FDCWD, "/proc/self/cgroup", O_RDONLY) = 3
openat(AT_FDCWD, "/proc/self/mountinfo", O_RDONLY) = 3
openat(AT_FDCWD, "/proc/self/cgroup", O_RDONLY) = 3
openat(AT_FDCWD, "/sys/fs/cgroup//cpu.max", O_RDONLY) = 3
strace: Process 160 attached
[pid   159] openat(AT_FDCWD, "/sys/fs/cgroup//memory.max", O_RDONLY) = 3
[pid   159] openat(AT_FDCWD, "/sys/devices/system/cpu/cpu0/cache/index0/size", O_RDONLY) = 3
[pid   159] openat(AT_FDCWD, "/sys/devices/system/cpu/cpu0/cache/index0/level", O_RDONLY) = 3
[pid   159] openat(AT_FDCWD, "/sys/devices/system/cpu/cpu0/cache/index1/size", O_RDONLY) = 3
[pid   159] openat(AT_FDCWD, "/sys/devices/system/cpu/cpu0/cache/index1/level", O_RDONLY) = 3
[pid   159] openat(AT_FDCWD, "/sys/devices/system/cpu/cpu0/cache/index2/size", O_RDONLY) = 3
[pid   159] openat(AT_FDCWD, "/sys/devices/system/cpu/cpu0/cache/index2/level", O_RDONLY) = 3
[pid   159] openat(AT_FDCWD, "/sys/devices/system/cpu/cpu0/cache/index3/size", O_RDONLY) = 3
[pid   159] openat(AT_FDCWD, "/sys/devices/system/cpu/cpu0/cache/index3/level", O_RDONLY) = 3
[pid   159] openat(AT_FDCWD, "/sys/devices/system/cpu/cpu0/cache/index4/size", O_RDONLY) = -1 ENOENT (No such file or directory)
[pid   159] openat(AT_FDCWD, "/proc/meminfo", O_RDONLY) = 3
[pid   159] openat(AT_FDCWD, "/proc/self/maps", O_RDONLY|O_CLOEXEC) = 3
strace: Process 161 attached
[pid   159] openat(AT_FDCWD, "/root/.terminfo/x/xterm", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
[pid   159] openat(AT_FDCWD, "/root/.terminfo/78/xterm", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
[pid   159] openat(AT_FDCWD, "/etc/terminfo/x/xterm", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
[pid   159] openat(AT_FDCWD, "/etc/terminfo/78/xterm", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
[pid   159] openat(AT_FDCWD, "/lib/terminfo/x/xterm", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
[pid   159] openat(AT_FDCWD, "/lib/terminfo/78/xterm", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
[pid   159] openat(AT_FDCWD, "/usr/share/terminfo/x/xterm", O_RDONLY|O_CLOEXEC) = 6
[pid   159] openat(AT_FDCWD, "/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 6
[pid   159] openat(AT_FDCWD, "/lib/x86_64-linux-gnu/glibc-hwcaps/x86-64-v4/libicuuc.so.90", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
[pid   159] newfstatat(AT_FDCWD, "/lib/x86_64-linux-gnu/glibc-hwcaps/x86-64-v4/", 0x7ffc1064fc30, 0) = -1 ENOENT (No such file or directory)
[pid   159] openat(AT_FDCWD, "/lib/x86_64-linux-gnu/glibc-hwcaps/x86-64-v3/libicuuc.so.90", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
[pid   159] newfstatat(AT_FDCWD, "/lib/x86_64-linux-gnu/glibc-hwcaps/x86-64-v3/", 0x7ffc1064fc30, 0) = -1 ENOENT (No such file or directory)
[pid   159] openat(AT_FDCWD, "/lib/x86_64-linux-gnu/glibc-hwcaps/x86-64-v2/libicuuc.so.90", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
[pid   159] newfstatat(AT_FDCWD, "/lib/x86_64-linux-gnu/glibc-hwcaps/x86-64-v2/", 0x7ffc1064fc30, 0) = -1 ENOENT (No such file or directory)
[pid   159] openat(AT_FDCWD, "/lib/x86_64-linux-gnu/libicuuc.so.90", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
[pid   159] newfstatat(AT_FDCWD, "/lib/x86_64-linux-gnu/", {st_mode=S_IFDIR|0755, st_size=16384, ...}, 0) = 0
[pid   159] openat(AT_FDCWD, "/usr/lib/x86_64-linux-gnu/glibc-hwcaps/x86-64-v4/libicuuc.so.90", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
[pid   159] newfstatat(AT_FDCWD, "/usr/lib/x86_64-linux-gnu/glibc-hwcaps/x86-64-v4/", 0x7ffc1064fc30, 0) = -1 ENOENT (No such file or directory)
[pid   159] openat(AT_FDCWD, "/usr/lib/x86_64-linux-gnu/glibc-hwcaps/x86-64-v3/libicuuc.so.90", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
[pid   159] newfstatat(AT_FDCWD, "/usr/lib/x86_64-linux-gnu/glibc-hwcaps/x86-64-v3/", 0x7ffc1064fc30, 0) = -1 ENOENT (No such file or directory)
[pid   159] openat(AT_FDCWD, "/usr/lib/x86_64-linux-gnu/glibc-hwcaps/x86-64-v2/libicuuc.so.90", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
[pid   159] newfstatat(AT_FDCWD, "/usr/lib/x86_64-linux-gnu/glibc-hwcaps/x86-64-v2/", 0x7ffc1064fc30, 0) = -1 ENOENT (No such file or directory)
[pid   159] openat(AT_FDCWD, "/usr/lib/x86_64-linux-gnu/libicuuc.so.90", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
[pid   159] newfstatat(AT_FDCWD, "/usr/lib/x86_64-linux-gnu/", {st_mode=S_IFDIR|0755, st_size=16384, ...}, 0) = 0
[pid   159] openat(AT_FDCWD, "/lib/glibc-hwcaps/x86-64-v4/libicuuc.so.90", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
[pid   159] newfstatat(AT_FDCWD, "/lib/glibc-hwcaps/x86-64-v4/", 0x7ffc1064fc30, 0) = -1 ENOENT (No such file or directory)
[pid   159] openat(AT_FDCWD, "/lib/glibc-hwcaps/x86-64-v3/libicuuc.so.90", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
[pid   159] newfstatat(AT_FDCWD, "/lib/glibc-hwcaps/x86-64-v3/", 0x7ffc1064fc30, 0) = -1 ENOENT (No such file or directory)
[pid   159] openat(AT_FDCWD, "/lib/glibc-hwcaps/x86-64-v2/libicuuc.so.90", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
[pid   159] newfstatat(AT_FDCWD, "/lib/glibc-hwcaps/x86-64-v2/", 0x7ffc1064fc30, 0) = -1 ENOENT (No such file or directory)
[pid   159] openat(AT_FDCWD, "/lib/libicuuc.so.90", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
[pid   159] newfstatat(AT_FDCWD, "/lib/", {st_mode=S_IFDIR|0755, st_size=4096, ...}, 0) = 0
[pid   159] openat(AT_FDCWD, "/usr/lib/glibc-hwcaps/x86-64-v4/libicuuc.so.90", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
[pid   159] newfstatat(AT_FDCWD, "/usr/lib/glibc-hwcaps/x86-64-v4/", 0x7ffc1064fc30, 0) = -1 ENOENT (No such file or directory)
[pid   159] openat(AT_FDCWD, "/usr/lib/glibc-hwcaps/x86-64-v3/libicuuc.so.90", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
[pid   159] newfstatat(AT_FDCWD, "/usr/lib/glibc-hwcaps/x86-64-v3/", 0x7ffc1064fc30, 0) = -1 ENOENT (No such file or directory)
[pid   159] openat(AT_FDCWD, "/usr/lib/glibc-hwcaps/x86-64-v2/libicuuc.so.90", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
[pid   159] newfstatat(AT_FDCWD, "/usr/lib/glibc-hwcaps/x86-64-v2/", 0x7ffc1064fc30, 0) = -1 ENOENT (No such file or directory)
[pid   159] openat(AT_FDCWD, "/usr/lib/libicuuc.so.90", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
[pid   159] newfstatat(AT_FDCWD, "/usr/lib/", {st_mode=S_IFDIR|0755, st_size=4096, ...}, 0) = 0
[pid   159] openat(AT_FDCWD, "/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 6
[pid   159] openat(AT_FDCWD, "/lib/x86_64-linux-gnu/libicuuc.so.89", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
[pid   159] openat(AT_FDCWD, "/usr/lib/x86_64-linux-gnu/libicuuc.so.89", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
[pid   159] openat(AT_FDCWD, "/lib/libicuuc.so.89", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
[pid   159] openat(AT_FDCWD, "/usr/lib/libicuuc.so.89", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
[pid   159] openat(AT_FDCWD, "/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 6
[pid   159] openat(AT_FDCWD, "/lib/x86_64-linux-gnu/libicuuc.so.88", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
[pid   159] openat(AT_FDCWD, "/usr/lib/x86_64-linux-gnu/libicuuc.so.88", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
[pid   159] openat(AT_FDCWD, "/lib/libicuuc.so.88", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
[pid   159] openat(AT_FDCWD, "/usr/lib/libicuuc.so.88", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
[pid   159] openat(AT_FDCWD, "/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 6
[pid   159] openat(AT_FDCWD, "/lib/x86_64-linux-gnu/libicuuc.so.87", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
[pid   159] openat(AT_FDCWD, "/usr/lib/x86_64-linux-gnu/libicuuc.so.87", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
[pid   159] openat(AT_FDCWD, "/lib/libicuuc.so.87", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
[pid   159] openat(AT_FDCWD, "/usr/lib/libicuuc.so.87", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
[pid   159] openat(AT_FDCWD, "/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 6
[pid   159] openat(AT_FDCWD, "/lib/x86_64-linux-gnu/libicuuc.so.86", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
[pid   159] openat(AT_FDCWD, "/usr/lib/x86_64-linux-gnu/libicuuc.so.86", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
[pid   159] openat(AT_FDCWD, "/lib/libicuuc.so.86", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
[pid   159] openat(AT_FDCWD, "/usr/lib/libicuuc.so.86", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
[pid   159] openat(AT_FDCWD, "/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 6
[pid   159] openat(AT_FDCWD, "/lib/x86_64-linux-gnu/libicuuc.so.85", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
[pid   159] openat(AT_FDCWD, "/usr/lib/x86_64-linux-gnu/libicuuc.so.85", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
[pid   159] openat(AT_FDCWD, "/lib/libicuuc.so.85", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
[pid   159] openat(AT_FDCWD, "/usr/lib/libicuuc.so.85", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
[pid   159] openat(AT_FDCWD, "/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 6
[pid   159] openat(AT_FDCWD, "/lib/x86_64-linux-gnu/libicuuc.so.84", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
[pid   159] openat(AT_FDCWD, "/usr/lib/x86_64-linux-gnu/libicuuc.so.84", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
[pid   159] openat(AT_FDCWD, "/lib/libicuuc.so.84", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
[pid   159] openat(AT_FDCWD, "/usr/lib/libicuuc.so.84", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
[pid   159] openat(AT_FDCWD, "/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 6
[pid   159] openat(AT_FDCWD, "/lib/x86_64-linux-gnu/libicuuc.so.83", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
[pid   159] openat(AT_FDCWD, "/usr/lib/x86_64-linux-gnu/libicuuc.so.83", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
[pid   159] openat(AT_FDCWD, "/lib/libicuuc.so.83", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
[pid   159] openat(AT_FDCWD, "/usr/lib/libicuuc.so.83", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
[pid   159] openat(AT_FDCWD, "/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 6
[pid   159] openat(AT_FDCWD, "/lib/x86_64-linux-gnu/libicuuc.so.82", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
[pid   159] openat(AT_FDCWD, "/usr/lib/x86_64-linux-gnu/libicuuc.so.82", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
[pid   159] openat(AT_FDCWD, "/lib/libicuuc.so.82", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
[pid   159] openat(AT_FDCWD, "/usr/lib/libicuuc.so.82", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
[pid   159] openat(AT_FDCWD, "/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 6
[pid   159] openat(AT_FDCWD, "/lib/x86_64-linux-gnu/libicuuc.so.81", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
[pid   159] openat(AT_FDCWD, "/usr/lib/x86_64-linux-gnu/libicuuc.so.81", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
[pid   159] openat(AT_FDCWD, "/lib/libicuuc.so.81", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
[pid   159] openat(AT_FDCWD, "/usr/lib/libicuuc.so.81", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
[pid   159] openat(AT_FDCWD, "/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 6
[pid   159] openat(AT_FDCWD, "/lib/x86_64-linux-gnu/libicuuc.so.80", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
[pid   159] openat(AT_FDCWD, "/usr/lib/x86_64-linux-gnu/libicuuc.so.80", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
[pid   159] openat(AT_FDCWD, "/lib/libicuuc.so.80", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
[pid   159] openat(AT_FDCWD, "/usr/lib/libicuuc.so.80", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
[pid   159] openat(AT_FDCWD, "/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 6
[pid   159] openat(AT_FDCWD, "/lib/x86_64-linux-gnu/libicuuc.so.79", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
[pid   159] openat(AT_FDCWD, "/usr/lib/x86_64-linux-gnu/libicuuc.so.79", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
[pid   159] openat(AT_FDCWD, "/lib/libicuuc.so.79", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
[pid   159] openat(AT_FDCWD, "/usr/lib/libicuuc.so.79", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
[pid   159] openat(AT_FDCWD, "/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 6
[pid   159] openat(AT_FDCWD, "/lib/x86_64-linux-gnu/libicuuc.so.78", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
[pid   159] openat(AT_FDCWD, "/usr/lib/x86_64-linux-gnu/libicuuc.so.78", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
[pid   159] openat(AT_FDCWD, "/lib/libicuuc.so.78", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
[pid   159] openat(AT_FDCWD, "/usr/lib/libicuuc.so.78", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
[pid   159] openat(AT_FDCWD, "/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 6
[pid   159] openat(AT_FDCWD, "/lib/x86_64-linux-gnu/libicuuc.so.77", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
[pid   159] openat(AT_FDCWD, "/usr/lib/x86_64-linux-gnu/libicuuc.so.77", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
[pid   159] openat(AT_FDCWD, "/lib/libicuuc.so.77", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
[pid   159] openat(AT_FDCWD, "/usr/lib/libicuuc.so.77", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
[pid   159] openat(AT_FDCWD, "/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 6
[pid   159] openat(AT_FDCWD, "/lib/x86_64-linux-gnu/libicuuc.so.76", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
[pid   159] openat(AT_FDCWD, "/usr/lib/x86_64-linux-gnu/libicuuc.so.76", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
[pid   159] openat(AT_FDCWD, "/lib/libicuuc.so.76", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
[pid   159] openat(AT_FDCWD, "/usr/lib/libicuuc.so.76", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
[pid   159] openat(AT_FDCWD, "/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 6
[pid   159] openat(AT_FDCWD, "/lib/x86_64-linux-gnu/libicuuc.so.75", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
[pid   159] openat(AT_FDCWD, "/usr/lib/x86_64-linux-gnu/libicuuc.so.75", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
[pid   159] openat(AT_FDCWD, "/lib/libicuuc.so.75", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
[pid   159] openat(AT_FDCWD, "/usr/lib/libicuuc.so.75", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
[pid   159] openat(AT_FDCWD, "/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 6
[pid   159] openat(AT_FDCWD, "/lib/x86_64-linux-gnu/libicuuc.so.74", O_RDONLY|O_CLOEXEC) = 6
[pid   159] openat(AT_FDCWD, "/lib/x86_64-linux-gnu/libicudata.so.74", O_RDONLY|O_CLOEXEC) = 6
[pid   159] openat(AT_FDCWD, "/lib/x86_64-linux-gnu/libstdc++.so.6", O_RDONLY|O_CLOEXEC) = 6
[pid   159] openat(AT_FDCWD, "/lib/x86_64-linux-gnu/libgcc_s.so.1", O_RDONLY|O_CLOEXEC) = 6
[pid   159] openat(AT_FDCWD, "/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 6
[pid   159] openat(AT_FDCWD, "/lib/x86_64-linux-gnu/libicui18n.so.74", O_RDONLY|O_CLOEXEC) = 6
Hello, World
[pid   161] +++ exited with 0 +++
[pid   160] +++ exited with 0 +++
+++ exited with 0 +++

ICUの読み込みを無効化する

DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=1を指定するとICUの読み込みを無効化できます。この状態でstraceを取ると、全体の数が470から228へ、openatの数が129 (83)から28 (7)へ減っています。

$ DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=1 strace -c ./bin/hello_csharp_aot
Hello, World
% time     seconds  usecs/call     calls    errors syscall
------ ----------- ----------- --------- --------- ------------------
 18.23    0.000144           5        28         7 openat
 14.18    0.000112           6        18           rt_sigaction
 14.05    0.000111          55         2           clone
  8.48    0.000067           2        30           read
  6.84    0.000054          13         4           ioctl
  6.46    0.000051           2        22           mprotect
  4.81    0.000038           6         6           rt_sigprocmask
  4.68    0.000037          18         2           write
  3.16    0.000025           1        20           mmap
  2.78    0.000022           1        21           close
  2.78    0.000022           1        17           madvise
  2.53    0.000020           1        20           fstat
  2.28    0.000018          18         1           pipe2
  1.65    0.000013           4         3           pread64
  1.39    0.000011           3         3           sched_getaffinity
  1.27    0.000010          10         1           fcntl
  1.14    0.000009           9         1           lseek
  1.14    0.000009           3         3           getpid
  1.14    0.000009           2         4           prlimit64
  1.01    0.000008           8         1           gettid
  0.00    0.000000           0         4           munmap
  0.00    0.000000           0         3           brk
  0.00    0.000000           0         1         1 access
  0.00    0.000000           0         1           execve
  0.00    0.000000           0         1           sysinfo
  0.00    0.000000           0         2           statfs
  0.00    0.000000           0         1           arch_prctl
  0.00    0.000000           0         1           set_tid_address
  0.00    0.000000           0         1         1 get_mempolicy
  0.00    0.000000           0         1           set_robust_list
  0.00    0.000000           0         1           getrandom
  0.00    0.000000           0         2           membarrier
  0.00    0.000000           0         1           rseq
  0.00    0.000000           0         1         1 clone3
------ ----------- ----------- --------- --------- ------------------
100.00    0.000790           3       228        10 total

openatの呼び出しログからもICU探索ブロックが丸ごと消えています。

$ DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=1 strace -f -e trace=openat,newfstatat,access -s 200 ./bin/hello_csharp_aot 2>&1
access("/etc/ld.so.preload", R_OK)      = -1 ENOENT (No such file or directory)
openat(AT_FDCWD, "/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 3
openat(AT_FDCWD, "/lib/x86_64-linux-gnu/libm.so.6", O_RDONLY|O_CLOEXEC) = 3
openat(AT_FDCWD, "/lib/x86_64-linux-gnu/libc.so.6", O_RDONLY|O_CLOEXEC) = 3
openat(AT_FDCWD, "/sys/devices/system/cpu/online", O_RDONLY|O_CLOEXEC) = 3
openat(AT_FDCWD, "/proc/self/mountinfo", O_RDONLY) = 3
openat(AT_FDCWD, "/proc/self/cgroup", O_RDONLY) = 3
openat(AT_FDCWD, "/proc/self/mountinfo", O_RDONLY) = 3
openat(AT_FDCWD, "/proc/self/cgroup", O_RDONLY) = 3
openat(AT_FDCWD, "/sys/fs/cgroup//cpu.max", O_RDONLY) = 3
strace: Process 187 attached
[pid   186] openat(AT_FDCWD, "/sys/fs/cgroup//memory.max", O_RDONLY) = 3
[pid   186] openat(AT_FDCWD, "/sys/devices/system/cpu/cpu0/cache/index0/size", O_RDONLY) = 3
[pid   186] openat(AT_FDCWD, "/sys/devices/system/cpu/cpu0/cache/index0/level", O_RDONLY) = 3
[pid   186] openat(AT_FDCWD, "/sys/devices/system/cpu/cpu0/cache/index1/size", O_RDONLY) = 3
[pid   186] openat(AT_FDCWD, "/sys/devices/system/cpu/cpu0/cache/index1/level", O_RDONLY) = 3
[pid   186] openat(AT_FDCWD, "/sys/devices/system/cpu/cpu0/cache/index2/size", O_RDONLY) = 3
[pid   186] openat(AT_FDCWD, "/sys/devices/system/cpu/cpu0/cache/index2/level", O_RDONLY) = 3
[pid   186] openat(AT_FDCWD, "/sys/devices/system/cpu/cpu0/cache/index3/size", O_RDONLY) = 3
[pid   186] openat(AT_FDCWD, "/sys/devices/system/cpu/cpu0/cache/index3/level", O_RDONLY) = 3
[pid   186] openat(AT_FDCWD, "/sys/devices/system/cpu/cpu0/cache/index4/size", O_RDONLY) = -1 ENOENT (No such file or directory)
[pid   186] openat(AT_FDCWD, "/proc/meminfo", O_RDONLY) = 3
[pid   186] openat(AT_FDCWD, "/proc/self/maps", O_RDONLY|O_CLOEXEC) = 3
strace: Process 188 attached
[pid   186] openat(AT_FDCWD, "/root/.terminfo/x/xterm", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
[pid   186] openat(AT_FDCWD, "/root/.terminfo/78/xterm", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
[pid   186] openat(AT_FDCWD, "/etc/terminfo/x/xterm", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
[pid   186] openat(AT_FDCWD, "/etc/terminfo/78/xterm", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
[pid   186] openat(AT_FDCWD, "/lib/terminfo/x/xterm", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
[pid   186] openat(AT_FDCWD, "/lib/terminfo/78/xterm", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
[pid   186] openat(AT_FDCWD, "/usr/share/terminfo/x/xterm", O_RDONLY|O_CLOEXEC) = 6
Hello, World
[pid   188] +++ exited with 0 +++
[pid   187] +++ exited with 0 +++
+++ exited with 0 +++

ICUバージョンを指定する

ICU探索が重いからと言って、グローバリゼーションを無効にしてもいいアプリケーションばかりではありません。当然多言語対応が必要な場合もあります。この場合、ICUライブラリバージョンを指定するか、アプリに同梱する手があります。DOTNET_ICU_VERSION_OVERRIDE=74でICUバージョンを指定でき、該当バージョンのICUライブラリだけを探索するようになります。この状態でstraceを取ると、全体の数が470から290 (無効だと228)へ、openatの数が129 (83)から35(7)(無効だと28 (7))へ減っています。

ICU対応は、AOTに限らずJITでも同様に効果があります。

$ DOTNET_ICU_VERSION_OVERRIDE=74 strace -c ./bin/hello_csharp_aot
Hello, World
% time     seconds  usecs/call     calls    errors syscall
------ ----------- ----------- --------- --------- ------------------
 22.68    0.000414           9        45           mmap
 13.86    0.000253           7        35         7 openat
 11.45    0.000209           7        27           mprotect
  7.07    0.000129           3        35           read
  5.37    0.000098           3        28           close
  5.37    0.000098           5        17           madvise
  5.26    0.000096           5        18           rt_sigaction
  5.15    0.000094           3        27           fstat
  4.66    0.000085          14         6           rt_sigprocmask
  3.67    0.000067          33         2           clone
  3.34    0.000061          10         6           munmap
  2.74    0.000050          12         4           ioctl
  2.03    0.000037          18         2           write
  1.48    0.000027           9         3           futex
  1.04    0.000019          19         1           pipe2
  0.77    0.000014           4         3           sched_getaffinity
  0.77    0.000014           3         4           prlimit64
  0.66    0.000012           4         3           pread64
  0.60    0.000011           2         4           brk
  0.55    0.000010          10         1           fcntl
  0.49    0.000009           9         1           lseek
  0.49    0.000009           3         3           getpid
  0.49    0.000009           9         1           gettid
  0.00    0.000000           0         1         1 access
  0.00    0.000000           0         1           execve
  0.00    0.000000           0         1           sysinfo
  0.00    0.000000           0         2           statfs
  0.00    0.000000           0         1           arch_prctl
  0.00    0.000000           0         1           set_tid_address
  0.00    0.000000           0         1         1 get_mempolicy
  0.00    0.000000           0         1           set_robust_list
  0.00    0.000000           0         1           getrandom
  0.00    0.000000           0         2           membarrier
  0.00    0.000000           0         1           rseq
  0.00    0.000000           0         1         1 clone3
------ ----------- ----------- --------- --------- ------------------
100.00    0.001825           6       290        10 total

straceでopenatの呼び出しログを見ると、ICU探索ブロックが期待通り1つだけ存在し、ENOENTも発生していません。

$ DOTNET_ICU_VERSION_OVERRIDE=74 strace -f -e trace=openat,newfstatat,access -s 200 ./bin/hello_csharp_aot
access("/etc/ld.so.preload", R_OK)      = -1 ENOENT (No such file or directory)
openat(AT_FDCWD, "/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 3
openat(AT_FDCWD, "/lib/x86_64-linux-gnu/libm.so.6", O_RDONLY|O_CLOEXEC) = 3
openat(AT_FDCWD, "/lib/x86_64-linux-gnu/libc.so.6", O_RDONLY|O_CLOEXEC) = 3
openat(AT_FDCWD, "/sys/devices/system/cpu/online", O_RDONLY|O_CLOEXEC) = 3
openat(AT_FDCWD, "/proc/self/mountinfo", O_RDONLY) = 3
openat(AT_FDCWD, "/proc/self/cgroup", O_RDONLY) = 3
openat(AT_FDCWD, "/proc/self/mountinfo", O_RDONLY) = 3
openat(AT_FDCWD, "/proc/self/cgroup", O_RDONLY) = 3
openat(AT_FDCWD, "/sys/fs/cgroup//cpu.max", O_RDONLY) = 3
strace: Process 175 attached
[pid   174] openat(AT_FDCWD, "/sys/fs/cgroup//memory.max", O_RDONLY) = 3
[pid   174] openat(AT_FDCWD, "/sys/devices/system/cpu/cpu0/cache/index0/size", O_RDONLY) = 3
[pid   174] openat(AT_FDCWD, "/sys/devices/system/cpu/cpu0/cache/index0/level", O_RDONLY) = 3
[pid   174] openat(AT_FDCWD, "/sys/devices/system/cpu/cpu0/cache/index1/size", O_RDONLY) = 3
[pid   174] openat(AT_FDCWD, "/sys/devices/system/cpu/cpu0/cache/index1/level", O_RDONLY) = 3
[pid   174] openat(AT_FDCWD, "/sys/devices/system/cpu/cpu0/cache/index2/size", O_RDONLY) = 3
[pid   174] openat(AT_FDCWD, "/sys/devices/system/cpu/cpu0/cache/index2/level", O_RDONLY) = 3
[pid   174] openat(AT_FDCWD, "/sys/devices/system/cpu/cpu0/cache/index3/size", O_RDONLY) = 3
[pid   174] openat(AT_FDCWD, "/sys/devices/system/cpu/cpu0/cache/index3/level", O_RDONLY) = 3
[pid   174] openat(AT_FDCWD, "/sys/devices/system/cpu/cpu0/cache/index4/size", O_RDONLY) = -1 ENOENT (No such file or directory)
[pid   174] openat(AT_FDCWD, "/proc/meminfo", O_RDONLY) = 3
[pid   174] openat(AT_FDCWD, "/proc/self/maps", O_RDONLY|O_CLOEXEC) = 3
strace: Process 176 attached
[pid   174] openat(AT_FDCWD, "/root/.terminfo/x/xterm", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
[pid   174] openat(AT_FDCWD, "/root/.terminfo/78/xterm", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
[pid   174] openat(AT_FDCWD, "/etc/terminfo/x/xterm", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
[pid   174] openat(AT_FDCWD, "/etc/terminfo/78/xterm", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
[pid   174] openat(AT_FDCWD, "/lib/terminfo/x/xterm", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
[pid   174] openat(AT_FDCWD, "/lib/terminfo/78/xterm", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
[pid   174] openat(AT_FDCWD, "/usr/share/terminfo/x/xterm", O_RDONLY|O_CLOEXEC) = 6
[pid   174] openat(AT_FDCWD, "/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 6
[pid   174] openat(AT_FDCWD, "/lib/x86_64-linux-gnu/libicuuc.so.74", O_RDONLY|O_CLOEXEC) = 6
[pid   174] openat(AT_FDCWD, "/lib/x86_64-linux-gnu/libicudata.so.74", O_RDONLY|O_CLOEXEC) = 6
[pid   174] openat(AT_FDCWD, "/lib/x86_64-linux-gnu/libstdc++.so.6", O_RDONLY|O_CLOEXEC) = 6
[pid   174] openat(AT_FDCWD, "/lib/x86_64-linux-gnu/libgcc_s.so.1", O_RDONLY|O_CLOEXEC) = 6
[pid   174] openat(AT_FDCWD, "/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 6
[pid   174] openat(AT_FDCWD, "/lib/x86_64-linux-gnu/libicui18n.so.74", O_RDONLY|O_CLOEXEC) = 6
Hello, World
[pid   176] +++ exited with 0 +++
[pid   175] +++ exited with 0 +++
+++ exited with 0 +++

ICUを指定するためには、対象環境に入っているICUライブラリのバージョンを知る必要があります。ldconfig -pで確認できるので、使うと良いでしょう。

$ ldconfig -p | grep -E 'libicu(uc|i18n)\.so' | head -n 50
        libicuuc.so.74 (libc6,x86-64) => /lib/x86_64-linux-gnu/libicuuc.so.74
        libicui18n.so.74 (libc6,x86-64) => /lib/x86_64-linux-gnu/libicui18n.so.74

あるいはアプリケーションでICUバージョンを指定できるので、いずれかを使うと良いでしょう。

InvariantCultureを使えればいいのですが、そうでない場合はICUバージョン指定で起動時間を改善できるのはなるほどです。

C# AOTのopenatシステムコール

straceログから、ICUを除いたopenatの呼び出しは以下だけに限定されています。2

  • /sys/devices/system/cpu/...
  • /proc/self/mountinfo, /proc/self/cgroup
  • /sys/fs/cgroup/.../cpu.max, memory.max
  • /proc/meminfo, /proc/self/maps
  • /dev/urandom
  • terminfo探索

他言語を見てみましょう。CとRustはlibcを通じて同様の情報を取得しているようです。Goはランタイムが独自に実装しているため、プロセスを複数生成して情報を集めています。こうやって見ると、C#は診断用にいくつかのファイルを読んだり、icu、terminfoを読んだりしているのが追加部分と分かります。

C

$ strace -f -e trace=openat,newfstatat,access -s 200 ./bin/hello_c 2>&1
access("/etc/ld.so.preload", R_OK)      = -1 ENOENT (No such file or directory)
openat(AT_FDCWD, "/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 3
openat(AT_FDCWD, "/lib/x86_64-linux-gnu/libc.so.6", O_RDONLY|O_CLOEXEC) = 3
Hello World
+++ exited with 0 +++

Rust

$ strace -f -e trace=openat,newfstatat,access -s 200 ./bin/hello_rust 2>&1
access("/etc/ld.so.preload", R_OK)      = -1 ENOENT (No such file or directory)
openat(AT_FDCWD, "/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 3
openat(AT_FDCWD, "/lib/x86_64-linux-gnu/libgcc_s.so.1", O_RDONLY|O_CLOEXEC) = 3
openat(AT_FDCWD, "/lib/x86_64-linux-gnu/libc.so.6", O_RDONLY|O_CLOEXEC) = 3
openat(AT_FDCWD, "/proc/self/maps", O_RDONLY|O_CLOEXEC) = 3
Hello World
+++ exited with 0 +++

Go

$ strace -f -e trace=openat,newfstatat,access -s 200 ./bin/hello_go 2>&1
openat(AT_FDCWD, "/sys/kernel/mm/transparent_hugepage/hpage_pmd_size", O_RDONLY) = 3
strace: Process 239 attached
strace: Process 240 attached
[pid   238] --- SIGURG {si_signo=SIGURG, si_code=SI_TKILL, si_pid=238, si_uid=0} ---
strace: Process 241 attached
strace: Process 242 attached
strace: Process 243 attached
Hello World
[pid   243] +++ exited with 0 +++
[pid   242] +++ exited with 0 +++
[pid   241] +++ exited with 0 +++
[pid   240] +++ exited with 0 +++
[pid   239] +++ exited with 0 +++
+++ exited with 0 +++

C#ランタイムが読むシステムファイルの意味

C#が/sys,/procを色々読んでいるのは、dotnetランタイムがコンテナ/CPU/メモリのチューニングに必要なためです。index4ENOENTなのは単にそのCPUにindex4が無いので、最後まで試しているだけです。環境検出(CPU/NUMA/キャッシュ)、制限検出(cgroup)、メモリ検出、乱数、Console(terminfo)の5系統しかなく、パスが固定なので、起動時決め打ちの必要な情報を取っているように見えます。

他言語より多いものの、むしろC/Rustは最低限の情報しか取っていないとも言えます。

コンテナリソース制限(cgroup)検出

これらはdotnetランタイムがコンテナ内のCPU quotaやメモリ上限を読むためのものです。実際、dotnetランタイムのGCはコンテナリソース制限を考慮して動作するため必要な情報です。

  • /proc/self/mountinfo
  • /proc/self/cgroup:
  • /sys/fs/cgroup/.../cpu.max
  • /sys/fs/cgroup/.../memory.max

CPU情報

C/Rustより多いポイントで、CPUトポロジ/キャッシュ階層を取得しています。dotnetランタイムが、GCのセグメント/ヒープ構成やスレッドプールや初期パラメーターに必要な情報です。

  • /sys/devices/system/cpu/online
  • cpu0/cache/index*/{size,level}

メモリ情報・自己マップ

物理メモリ等、自プロセスのマップ(保護設定、診断、アンワインド/例外、スタック関連など)でランタイム起動時に必要な情報です。

  • /proc/meminfo
  • /proc/self/maps:

乱数

ハッシュのランダム化やセキュリティ機能のために乱数を取得します。

  • /dev/urandom

terminfo

TERM=dumbにしても1しか減らないことから、Console周りがterminfoをスキップしない、あるいはTERM=dumbでも最低限の確認をしているようです。terminfoで6回ENOENTが出ているのは嫌ですが、まぁ仕方ないでしょう。

  • ~/.terminfo/
  • /etc/terminfo/
  • /lib/terminfo/
  • /usr/share/terminfo

C#のwriteシステムコール

ここまでopenatを見てきましたが、writeシステムコールが他言語は標準出力の1回だけなのに対して、C# AOTは2回、JITは8回呼ばれています。これは割と特徴的なのstrace -e writeでwriteシステムコールを深堀りしてみましょう。

C

$ strace -e write ./bin/hello_c > /dev/null
write(1, "Hello World\n", 12)           = 12
+++ exited with 0 +++

Rust

$ strace -e write ./bin/hello_rust > /dev/null
write(1, "Hello World\n", 12)           = 12
+++ exited with 0 +++

Go

$ strace -e write ./bin/hello_go > /dev/null
--- SIGURG {si_signo=SIGURG, si_code=SI_TKILL, si_pid=5462, si_uid=0} ---
--- SIGURG {si_signo=SIGURG, si_code=SI_TKILL, si_pid=5462, si_uid=0} ---
--- SIGURG {si_signo=SIGURG, si_code=SI_TKILL, si_pid=5462, si_uid=0} ---
write(1, "Hello World\n", 12)           = 12
+++ exited with 0 +++

C# AOT

$ strace -e write ./bin/hello_csharp_aot > /dev/null
write(0, "\33[?1h\33=", 7)              = 7
write(3, "Hello, World\n", 13)          = 13
+++ exited with 0 +++

C# JIT

$ strace -e write ./bin/hello_csharp > /dev/null
write(13, "*", 1)                       = 1
write(14, ".NET Finalizer", 14)         = 14
write(13, "*", 1)                       = 1
write(16, ".NET Tiered Com", 15)        = 15
write(15, "*", 1)                       = 1
write(0, "\33[?1h\33=", 7)              = 7
write(24, "Hello, World\n", 13)         = 13
write(4, "\1", 1)                       = 1
+++ exited with 0 +++

C、Rust、Goは標準出力への1回のwriteだけですが、C# AOTでは端末制御シーケンスを書き込んでから標準出力へ書き込んでいます。JITではさらに診断情報を書き込んでいます。C# AOTとJITを詳しく見てみましょう。

C# AOTのwriteシステムコール

straceのオプションを追加して、タイムスタンプ、File descriptor(FD)のパス、バッファ内容を表示します。

$ strace -f -ttt -e trace=write,writev -s 200 -yy -xx ./bin/hello_csharp_aot
strace: Process 13 attached
strace: Process 14 attached
[pid    12] 1770645277.828800 write(1<\x2f\x64\x65\x76\x2f\x70\x74\x73\x2f\x30<char 136:0>>, "\x1b\x5b\x3f\x31\x68\x1b\x3d", 7) = 7
[pid    12] 1770645277.830239 write(3<\x2f\x64\x65\x76\x2f\x70\x74\x73\x2f\x30<char 136:0>>, "\x48\x65\x6c\x6c\x6f\x2c\x20\x57\x6f\x72\x6c\x64\x0a", 13Hello, World
) = 13
[pid    14] 1770645277.830626 +++ exited with 0 +++
[pid    13] 1770645277.830656 +++ exited with 0 +++
1770645277.831918 +++ exited with 0 +++

$ strace -f -ttt -e trace=write,writev -s 200 -yy ./bin/hello_csharp_aot
strace: Process 31 attached
strace: Process 32 attached
[pid    30] 1770646142.571391 write(1</dev/pts/0<char 136:0>>, "\33[?1h\33=", 7) = 7
[pid    30] 1770646142.572052 write(3</dev/pts/0<char 136:0>>, "Hello, World\n", 13Hello, World
) = 13
[pid    32] 1770646142.573542 +++ exited with 0 +++
[pid    31] 1770646142.573563 +++ exited with 0 +++
1770646142.574448 +++ exited with 0 +++

1回目のwriteで端末のモード切り替え(ANSIエスケープ)を流し、それからFD 3へ"Hello, World\n"を書き込んでいます。FD 3はstraceの-yyオプションでパスを表示させたところ、/dev/pts/0となっており、標準出力が端末に接続されている場合に割り当てられる擬似端末デバイスです。つまり、C# AOTでは標準出力に直接書き込むのではなく、一旦擬似端末デバイスに書き込んでから端末に出力していることになります。

1回目のwrite

端末のモード切り替えは以下の処理で、DECCKM: cursor keys modeDECKPAM(Keypad Application Modeを設定しています。

  • \x1b = ESC
  • \x1b\x5b\x3f\x31\x68 = ESC [ ? 1 h = DEC Private Mode Set 1 (DECCKM: cursor keys mode)
  • \x1b\x3d = ESC = DECKPAM(Keypad Application Mode)
write(1<\x2f\x64\x65\x76\x2f\x70\x74\x73\x2f\x30<char 136:0>>, "\x1b\x5b\x3f\x31\x68\x1b\x3d", 7) = 7

あるいは、エスケープシーケンスを可読化すると以下のようになります。

write(1</dev/pts/0<char 136:0>>, "\33[?1h\33=", 7) = 7

dotnet/runtimeのどこで処理されているかというと、src/libraries/System.Console/src/System/ConsolePal.Unix.csのEnsureConsoleInitialized関数にて、Interop.Sys.SetKeypadXmit(s_terminalHandle, keypadXmit);で端末のモード切替をしています。

private static unsafe void EnsureInitializedCore()
{
    lock (Console.Out) // ensure that writing the ANSI string and setting initialized to true are done atomically
    {
        if (!s_initialized)
        {
            // Do this even when redirected to make CancelKeyPress works.
            if (!Interop.Sys.InitializeTerminalAndSignalHandling())
            {
                throw new Win32Exception();
            }
            // InitializeTerminalAndSignalHandling will reset the terminal on a normal exit.
            // This also resets it for termination due to an unhandled exception.
            AppDomain.CurrentDomain.UnhandledException += (_, _) => { Interop.Sys.UninitializeTerminal(); };

            s_terminalHandle = !Console.IsOutputRedirected ? OpenStandardOutputHandle() :
                                !Console.IsInputRedirected  ? OpenStandardInputHandle() :
                                null;

            // Provide the native lib with the correct code from the terminfo to transition us into
            // "application mode".  This will both transition it immediately, as well as allow
            // the native lib later to handle signals that require re-entering the mode.
            if (s_terminalHandle != null &&
                TerminalFormatStringsInstance.KeypadXmit is string keypadXmit)
            {
                Interop.Sys.SetKeypadXmit(s_terminalHandle, keypadXmit);
            }

2回目のwriteはなぜFD3なのか

Hello, World\nを出力していますが、FD 3へ書き込んでいます。

write(3<\x2f\x64\x65\x76\x2f\x70\x74\x73\x2f\x30<char 136:0>>, "\x48\x65\x6c\x6c\x6f\x2c\x20\x57\x6f\x72\x6c\x64\x0a", 13Hello, World

エスケープシーケンスを可読化すると以下のようになります。

write(3</dev/pts/0<char 136:0>>, "Hello, World\n", 13Hello, World

CやRust、Goは標準出力FD 1へ直接書き込んでいるのに対して、C# AOTは擬似端末デバイスFD 3へ書き込んでいる点は不思議ですね。

# C
$ strace -f -ttt -e trace=write,writev -s 200 -yy ./bin/hello_c
write(1</dev/pts/0<char 136:0>>, "Hello World\n", 12Hello World

# Rust
$ strace -f -ttt -e trace=write,writev -s 200 -yy ./bin/hello_rust
write(1</dev/pts/0<char 136:0>>, "Hello World\n", 12Hello World

# Go
$ strace -f -ttt -e trace=write,writev -s 200 -yy ./bin/hello_go
write(1</dev/pts/0<char 136:0>>, "Hello World\n", 12Hello World

書き込みを追ってみましょう。

$ strace -f -ttt -e trace=dup,dup2,dup3,fcntl,openat,close,write -s 200 -yy ./bin/hello_csharp_aot
1770646837.538617 openat(AT_FDCWD</workspace>, "/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 3</etc/ld.so.cache>
1770646837.538979 close(3</etc/ld.so.cache>) = 0
1770646837.539104 openat(AT_FDCWD</workspace>, "/lib/x86_64-linux-gnu/libm.so.6", O_RDONLY|O_CLOEXEC) = 3</usr/lib/x86_64-linux-gnu/libm.so.6>
1770646837.539803 close(3</usr/lib/x86_64-linux-gnu/libm.so.6>) = 0
1770646837.539917 openat(AT_FDCWD</workspace>, "/lib/x86_64-linux-gnu/libc.so.6", O_RDONLY|O_CLOEXEC) = 3</usr/lib/x86_64-linux-gnu/libc.so.6>
1770646837.540707 close(3</usr/lib/x86_64-linux-gnu/libc.so.6>) = 0
1770646837.543771 openat(AT_FDCWD</workspace>, "/sys/devices/system/cpu/online", O_RDONLY|O_CLOEXEC) = 3</sys/devices/system/cpu/online>
1770646837.544000 close(3</sys/devices/system/cpu/online>) = 0
1770646837.544644 openat(AT_FDCWD</workspace>, "/proc/self/mountinfo", O_RDONLY) = 3</proc/137/mountinfo>
1770646837.545334 close(3</proc/137/mountinfo>) = 0
1770646837.545512 openat(AT_FDCWD</workspace>, "/proc/self/cgroup", O_RDONLY) = 3</proc/137/cgroup>
1770646837.545820 close(3</proc/137/cgroup>) = 0
1770646837.546360 openat(AT_FDCWD</workspace>, "/proc/self/mountinfo", O_RDONLY) = 3</proc/137/mountinfo>
1770646837.546940 close(3</proc/137/mountinfo>) = 0
1770646837.547053 openat(AT_FDCWD</workspace>, "/proc/self/cgroup", O_RDONLY) = 3</proc/137/cgroup>
1770646837.547324 close(3</proc/137/cgroup>) = 0
1770646837.547582 openat(AT_FDCWD</workspace>, "/sys/fs/cgroup//cpu.max", O_RDONLY) = 3</sys/fs/cgroup/cpu.max>
1770646837.547895 close(3</sys/fs/cgroup/cpu.max>) = 0
strace: Process 138 attached
[pid   137] 1770646837.549562 openat(AT_FDCWD</workspace>, "/sys/fs/cgroup//memory.max", O_RDONLY) = 3</sys/fs/cgroup/memory.max>
[pid   137] 1770646837.549880 close(3</sys/fs/cgroup/memory.max>) = 0
[pid   137] 1770646837.550374 openat(AT_FDCWD</workspace>, "/sys/devices/system/cpu/cpu0/cache/index0/size", O_RDONLY) = 3</sys/devices/system/cpu/cpu0/cache/index0/size>
[pid   137] 1770646837.550791 close(3</sys/devices/system/cpu/cpu0/cache/index0/size>) = 0
[pid   137] 1770646837.550911 openat(AT_FDCWD</workspace>, "/sys/devices/system/cpu/cpu0/cache/index0/level", O_RDONLY) = 3</sys/devices/system/cpu/cpu0/cache/index0/level>
[pid   137] 1770646837.551252 close(3</sys/devices/system/cpu/cpu0/cache/index0/level>) = 0
[pid   137] 1770646837.551367 openat(AT_FDCWD</workspace>, "/sys/devices/system/cpu/cpu0/cache/index1/size", O_RDONLY) = 3</sys/devices/system/cpu/cpu0/cache/index1/size>
[pid   137] 1770646837.551765 close(3</sys/devices/system/cpu/cpu0/cache/index1/size>) = 0
[pid   137] 1770646837.551889 openat(AT_FDCWD</workspace>, "/sys/devices/system/cpu/cpu0/cache/index1/level", O_RDONLY) = 3</sys/devices/system/cpu/cpu0/cache/index1/level>
[pid   137] 1770646837.552238 close(3</sys/devices/system/cpu/cpu0/cache/index1/level>) = 0
[pid   137] 1770646837.552360 openat(AT_FDCWD</workspace>, "/sys/devices/system/cpu/cpu0/cache/index2/size", O_RDONLY) = 3</sys/devices/system/cpu/cpu0/cache/index2/size>
[pid   137] 1770646837.552654 close(3</sys/devices/system/cpu/cpu0/cache/index2/size>) = 0
[pid   137] 1770646837.552790 openat(AT_FDCWD</workspace>, "/sys/devices/system/cpu/cpu0/cache/index2/level", O_RDONLY) = 3</sys/devices/system/cpu/cpu0/cache/index2/level>
[pid   137] 1770646837.553103 close(3</sys/devices/system/cpu/cpu0/cache/index2/level>) = 0
[pid   137] 1770646837.553222 openat(AT_FDCWD</workspace>, "/sys/devices/system/cpu/cpu0/cache/index3/size", O_RDONLY) = 3</sys/devices/system/cpu/cpu0/cache/index3/size>
[pid   137] 1770646837.553513 close(3</sys/devices/system/cpu/cpu0/cache/index3/size>) = 0
[pid   137] 1770646837.553629 openat(AT_FDCWD</workspace>, "/sys/devices/system/cpu/cpu0/cache/index3/level", O_RDONLY) = 3</sys/devices/system/cpu/cpu0/cache/index3/level>
[pid   137] 1770646837.553912 close(3</sys/devices/system/cpu/cpu0/cache/index3/level>) = 0
[pid   137] 1770646837.554043 openat(AT_FDCWD</workspace>, "/sys/devices/system/cpu/cpu0/cache/index4/size", O_RDONLY) = -1 ENOENT (No such file or directory)
[pid   137] 1770646837.555098 openat(AT_FDCWD</workspace>, "/proc/meminfo", O_RDONLY) = 3</proc/meminfo>
[pid   137] 1770646837.555372 close(3</proc/meminfo>) = 0
[pid   137] 1770646837.557397 openat(AT_FDCWD</workspace>, "/proc/self/maps", O_RDONLY|O_CLOEXEC) = 3</proc/137/maps>
[pid   137] 1770646837.558138 close(3</proc/137/maps>) = 0
[pid   137] 1770646837.561204 fcntl(1</dev/pts/0<char 136:0>>, F_DUPFD_CLOEXEC, 0) = 3</dev/pts/0<char 136:0>>
strace: Process 139 attached
[pid   137] 1770646837.562720 openat(AT_FDCWD</workspace>, "/root/.terminfo/x/xterm", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
[pid   137] 1770646837.562981 openat(AT_FDCWD</workspace>, "/root/.terminfo/78/xterm", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
[pid   137] 1770646837.563093 openat(AT_FDCWD</workspace>, "/etc/terminfo/x/xterm", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
[pid   137] 1770646837.563212 openat(AT_FDCWD</workspace>, "/etc/terminfo/78/xterm", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
[pid   137] 1770646837.563341 openat(AT_FDCWD</workspace>, "/lib/terminfo/x/xterm", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
[pid   137] 1770646837.563475 openat(AT_FDCWD</workspace>, "/lib/terminfo/78/xterm", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
[pid   137] 1770646837.563648 openat(AT_FDCWD</workspace>, "/usr/share/terminfo/x/xterm", O_RDONLY|O_CLOEXEC) = 6</usr/share/terminfo/x/xterm>
[pid   137] 1770646837.564125 close(6</usr/share/terminfo/x/xterm>) = 0
[pid   137] 1770646837.564249 openat(AT_FDCWD</workspace>, "/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 6</etc/ld.so.cache>
[pid   137] 1770646837.564534 close(6</etc/ld.so.cache>) = 0
[pid   137] 1770646837.564700 openat(AT_FDCWD</workspace>, "/lib/x86_64-linux-gnu/glibc-hwcaps/x86-64-v4/libicuuc.so.90", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
[pid   137] 1770646837.564886 openat(AT_FDCWD</workspace>, "/lib/x86_64-linux-gnu/glibc-hwcaps/x86-64-v3/libicuuc.so.90", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
[pid   137] 1770646837.565197 openat(AT_FDCWD</workspace>, "/lib/x86_64-linux-gnu/glibc-hwcaps/x86-64-v2/libicuuc.so.90", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
[pid   137] 1770646837.565444 openat(AT_FDCWD</workspace>, "/lib/x86_64-linux-gnu/libicuuc.so.90", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
[pid   137] 1770646837.565722 openat(AT_FDCWD</workspace>, "/usr/lib/x86_64-linux-gnu/glibc-hwcaps/x86-64-v4/libicuuc.so.90", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
[pid   137] 1770646837.565980 openat(AT_FDCWD</workspace>, "/usr/lib/x86_64-linux-gnu/glibc-hwcaps/x86-64-v3/libicuuc.so.90", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
[pid   137] 1770646837.566227 openat(AT_FDCWD</workspace>, "/usr/lib/x86_64-linux-gnu/glibc-hwcaps/x86-64-v2/libicuuc.so.90", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
[pid   137] 1770646837.566497 openat(AT_FDCWD</workspace>, "/usr/lib/x86_64-linux-gnu/libicuuc.so.90", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
[pid   137] 1770646837.566767 openat(AT_FDCWD</workspace>, "/lib/glibc-hwcaps/x86-64-v4/libicuuc.so.90", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
[pid   137] 1770646837.567001 openat(AT_FDCWD</workspace>, "/lib/glibc-hwcaps/x86-64-v3/libicuuc.so.90", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
[pid   137] 1770646837.567277 openat(AT_FDCWD</workspace>, "/lib/glibc-hwcaps/x86-64-v2/libicuuc.so.90", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
[pid   137] 1770646837.567554 openat(AT_FDCWD</workspace>, "/lib/libicuuc.so.90", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
[pid   137] 1770646837.567779 openat(AT_FDCWD</workspace>, "/usr/lib/glibc-hwcaps/x86-64-v4/libicuuc.so.90", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
[pid   137] 1770646837.567974 openat(AT_FDCWD</workspace>, "/usr/lib/glibc-hwcaps/x86-64-v3/libicuuc.so.90", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
[pid   137] 1770646837.568192 openat(AT_FDCWD</workspace>, "/usr/lib/glibc-hwcaps/x86-64-v2/libicuuc.so.90", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
[pid   137] 1770646837.568404 openat(AT_FDCWD</workspace>, "/usr/lib/libicuuc.so.90", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
[pid   137] 1770646837.568702 openat(AT_FDCWD</workspace>, "/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 6</etc/ld.so.cache>
[pid   137] 1770646837.569027 close(6</etc/ld.so.cache>) = 0
[pid   137] 1770646837.569146 openat(AT_FDCWD</workspace>, "/lib/x86_64-linux-gnu/libicuuc.so.89", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
[pid   137] 1770646837.569274 openat(AT_FDCWD</workspace>, "/usr/lib/x86_64-linux-gnu/libicuuc.so.89", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
[pid   137] 1770646837.569402 openat(AT_FDCWD</workspace>, "/lib/libicuuc.so.89", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
[pid   137] 1770646837.569532 openat(AT_FDCWD</workspace>, "/usr/lib/libicuuc.so.89", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
[pid   137] 1770646837.569740 openat(AT_FDCWD</workspace>, "/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 6</etc/ld.so.cache>
[pid   137] 1770646837.570007 close(6</etc/ld.so.cache>) = 0
[pid   137] 1770646837.570137 openat(AT_FDCWD</workspace>, "/lib/x86_64-linux-gnu/libicuuc.so.88", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
[pid   137] 1770646837.570305 openat(AT_FDCWD</workspace>, "/usr/lib/x86_64-linux-gnu/libicuuc.so.88", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
[pid   137] 1770646837.570443 openat(AT_FDCWD</workspace>, "/lib/libicuuc.so.88", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
[pid   137] 1770646837.570583 openat(AT_FDCWD</workspace>, "/usr/lib/libicuuc.so.88", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
[pid   137] 1770646837.570792 openat(AT_FDCWD</workspace>, "/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 6</etc/ld.so.cache>
[pid   137] 1770646837.571078 close(6</etc/ld.so.cache>) = 0
[pid   137] 1770646837.571184 openat(AT_FDCWD</workspace>, "/lib/x86_64-linux-gnu/libicuuc.so.87", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
[pid   137] 1770646837.571303 openat(AT_FDCWD</workspace>, "/usr/lib/x86_64-linux-gnu/libicuuc.so.87", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
[pid   137] 1770646837.571408 openat(AT_FDCWD</workspace>, "/lib/libicuuc.so.87", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
[pid   137] 1770646837.571549 openat(AT_FDCWD</workspace>, "/usr/lib/libicuuc.so.87", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
[pid   137] 1770646837.571828 openat(AT_FDCWD</workspace>, "/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 6</etc/ld.so.cache>
[pid   137] 1770646837.572099 close(6</etc/ld.so.cache>) = 0
[pid   137] 1770646837.572216 openat(AT_FDCWD</workspace>, "/lib/x86_64-linux-gnu/libicuuc.so.86", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
[pid   137] 1770646837.572358 openat(AT_FDCWD</workspace>, "/usr/lib/x86_64-linux-gnu/libicuuc.so.86", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
[pid   137] 1770646837.572511 openat(AT_FDCWD</workspace>, "/lib/libicuuc.so.86", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
[pid   137] 1770646837.572662 openat(AT_FDCWD</workspace>, "/usr/lib/libicuuc.so.86", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
[pid   137] 1770646837.572861 openat(AT_FDCWD</workspace>, "/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 6</etc/ld.so.cache>
[pid   137] 1770646837.573188 close(6</etc/ld.so.cache>) = 0
[pid   137] 1770646837.573420 openat(AT_FDCWD</workspace>, "/lib/x86_64-linux-gnu/libicuuc.so.85", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
[pid   137] 1770646837.573612 openat(AT_FDCWD</workspace>, "/usr/lib/x86_64-linux-gnu/libicuuc.so.85", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
[pid   137] 1770646837.573790 openat(AT_FDCWD</workspace>, "/lib/libicuuc.so.85", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
[pid   137] 1770646837.574000 openat(AT_FDCWD</workspace>, "/usr/lib/libicuuc.so.85", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
[pid   137] 1770646837.574299 openat(AT_FDCWD</workspace>, "/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 6</etc/ld.so.cache>
[pid   137] 1770646837.574644 close(6</etc/ld.so.cache>) = 0
[pid   137] 1770646837.574781 openat(AT_FDCWD</workspace>, "/lib/x86_64-linux-gnu/libicuuc.so.84", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
[pid   137] 1770646837.574940 openat(AT_FDCWD</workspace>, "/usr/lib/x86_64-linux-gnu/libicuuc.so.84", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
[pid   137] 1770646837.575085 openat(AT_FDCWD</workspace>, "/lib/libicuuc.so.84", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
[pid   137] 1770646837.575207 openat(AT_FDCWD</workspace>, "/usr/lib/libicuuc.so.84", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
[pid   137] 1770646837.575539 openat(AT_FDCWD</workspace>, "/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 6</etc/ld.so.cache>
[pid   137] 1770646837.575823 close(6</etc/ld.so.cache>) = 0
[pid   137] 1770646837.575963 openat(AT_FDCWD</workspace>, "/lib/x86_64-linux-gnu/libicuuc.so.83", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
[pid   137] 1770646837.576089 openat(AT_FDCWD</workspace>, "/usr/lib/x86_64-linux-gnu/libicuuc.so.83", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
[pid   137] 1770646837.576212 openat(AT_FDCWD</workspace>, "/lib/libicuuc.so.83", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
[pid   137] 1770646837.576351 openat(AT_FDCWD</workspace>, "/usr/lib/libicuuc.so.83", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
[pid   137] 1770646837.576564 openat(AT_FDCWD</workspace>, "/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 6</etc/ld.so.cache>
[pid   137] 1770646837.576850 close(6</etc/ld.so.cache>) = 0
[pid   137] 1770646837.576976 openat(AT_FDCWD</workspace>, "/lib/x86_64-linux-gnu/libicuuc.so.82", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
[pid   137] 1770646837.577100 openat(AT_FDCWD</workspace>, "/usr/lib/x86_64-linux-gnu/libicuuc.so.82", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
[pid   137] 1770646837.577265 openat(AT_FDCWD</workspace>, "/lib/libicuuc.so.82", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
[pid   137] 1770646837.577408 openat(AT_FDCWD</workspace>, "/usr/lib/libicuuc.so.82", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
[pid   137] 1770646837.577682 openat(AT_FDCWD</workspace>, "/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 6</etc/ld.so.cache>
[pid   137] 1770646837.578019 close(6</etc/ld.so.cache>) = 0
[pid   137] 1770646837.578130 openat(AT_FDCWD</workspace>, "/lib/x86_64-linux-gnu/libicuuc.so.81", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
[pid   137] 1770646837.578248 openat(AT_FDCWD</workspace>, "/usr/lib/x86_64-linux-gnu/libicuuc.so.81", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
[pid   137] 1770646837.578374 openat(AT_FDCWD</workspace>, "/lib/libicuuc.so.81", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
[pid   137] 1770646837.578507 openat(AT_FDCWD</workspace>, "/usr/lib/libicuuc.so.81", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
[pid   137] 1770646837.578751 openat(AT_FDCWD</workspace>, "/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 6</etc/ld.so.cache>
[pid   137] 1770646837.579075 close(6</etc/ld.so.cache>) = 0
[pid   137] 1770646837.579186 openat(AT_FDCWD</workspace>, "/lib/x86_64-linux-gnu/libicuuc.so.80", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
[pid   137] 1770646837.579302 openat(AT_FDCWD</workspace>, "/usr/lib/x86_64-linux-gnu/libicuuc.so.80", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
[pid   137] 1770646837.579457 openat(AT_FDCWD</workspace>, "/lib/libicuuc.so.80", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
[pid   137] 1770646837.579541 openat(AT_FDCWD</workspace>, "/usr/lib/libicuuc.so.80", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
[pid   137] 1770646837.579801 openat(AT_FDCWD</workspace>, "/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 6</etc/ld.so.cache>
[pid   137] 1770646837.580128 close(6</etc/ld.so.cache>) = 0
[pid   137] 1770646837.580244 openat(AT_FDCWD</workspace>, "/lib/x86_64-linux-gnu/libicuuc.so.79", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
[pid   137] 1770646837.580403 openat(AT_FDCWD</workspace>, "/usr/lib/x86_64-linux-gnu/libicuuc.so.79", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
[pid   137] 1770646837.580520 openat(AT_FDCWD</workspace>, "/lib/libicuuc.so.79", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
[pid   137] 1770646837.580683 openat(AT_FDCWD</workspace>, "/usr/lib/libicuuc.so.79", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
[pid   137] 1770646837.580877 openat(AT_FDCWD</workspace>, "/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 6</etc/ld.so.cache>
[pid   137] 1770646837.581203 close(6</etc/ld.so.cache>) = 0
[pid   137] 1770646837.581383 openat(AT_FDCWD</workspace>, "/lib/x86_64-linux-gnu/libicuuc.so.78", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
[pid   137] 1770646837.581544 openat(AT_FDCWD</workspace>, "/usr/lib/x86_64-linux-gnu/libicuuc.so.78", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
[pid   137] 1770646837.581703 openat(AT_FDCWD</workspace>, "/lib/libicuuc.so.78", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
[pid   137] 1770646837.581872 openat(AT_FDCWD</workspace>, "/usr/lib/libicuuc.so.78", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
[pid   137] 1770646837.582150 openat(AT_FDCWD</workspace>, "/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 6</etc/ld.so.cache>
[pid   137] 1770646837.582529 close(6</etc/ld.so.cache>) = 0
[pid   137] 1770646837.582696 openat(AT_FDCWD</workspace>, "/lib/x86_64-linux-gnu/libicuuc.so.77", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
[pid   137] 1770646837.582857 openat(AT_FDCWD</workspace>, "/usr/lib/x86_64-linux-gnu/libicuuc.so.77", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
[pid   137] 1770646837.583033 openat(AT_FDCWD</workspace>, "/lib/libicuuc.so.77", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
[pid   137] 1770646837.583186 openat(AT_FDCWD</workspace>, "/usr/lib/libicuuc.so.77", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
[pid   137] 1770646837.583474 openat(AT_FDCWD</workspace>, "/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 6</etc/ld.so.cache>
[pid   137] 1770646837.583801 close(6</etc/ld.so.cache>) = 0
[pid   137] 1770646837.583913 openat(AT_FDCWD</workspace>, "/lib/x86_64-linux-gnu/libicuuc.so.76", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
[pid   137] 1770646837.584040 openat(AT_FDCWD</workspace>, "/usr/lib/x86_64-linux-gnu/libicuuc.so.76", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
[pid   137] 1770646837.584172 openat(AT_FDCWD</workspace>, "/lib/libicuuc.so.76", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
[pid   137] 1770646837.584351 openat(AT_FDCWD</workspace>, "/usr/lib/libicuuc.so.76", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
[pid   137] 1770646837.584607 openat(AT_FDCWD</workspace>, "/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 6</etc/ld.so.cache>
[pid   137] 1770646837.584878 close(6</etc/ld.so.cache>) = 0
[pid   137] 1770646837.584988 openat(AT_FDCWD</workspace>, "/lib/x86_64-linux-gnu/libicuuc.so.75", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
[pid   137] 1770646837.585114 openat(AT_FDCWD</workspace>, "/usr/lib/x86_64-linux-gnu/libicuuc.so.75", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
[pid   137] 1770646837.585244 openat(AT_FDCWD</workspace>, "/lib/libicuuc.so.75", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
[pid   137] 1770646837.585410 openat(AT_FDCWD</workspace>, "/usr/lib/libicuuc.so.75", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
[pid   137] 1770646837.585664 openat(AT_FDCWD</workspace>, "/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 6</etc/ld.so.cache>
[pid   137] 1770646837.585977 close(6</etc/ld.so.cache>) = 0
[pid   137] 1770646837.586087 openat(AT_FDCWD</workspace>, "/lib/x86_64-linux-gnu/libicuuc.so.74", O_RDONLY|O_CLOEXEC) = 6</usr/lib/x86_64-linux-gnu/libicuuc.so.74.2>
[pid   137] 1770646837.586809 close(6</usr/lib/x86_64-linux-gnu/libicuuc.so.74.2>) = 0
[pid   137] 1770646837.586925 openat(AT_FDCWD</workspace>, "/lib/x86_64-linux-gnu/libicudata.so.74", O_RDONLY|O_CLOEXEC) = 6</usr/lib/x86_64-linux-gnu/libicudata.so.74.2>
[pid   137] 1770646837.587485 close(6</usr/lib/x86_64-linux-gnu/libicudata.so.74.2>) = 0
[pid   137] 1770646837.587587 openat(AT_FDCWD</workspace>, "/lib/x86_64-linux-gnu/libstdc++.so.6", O_RDONLY|O_CLOEXEC) = 6</usr/lib/x86_64-linux-gnu/libstdc++.so.6.0.33>
[pid   137] 1770646837.588280 close(6</usr/lib/x86_64-linux-gnu/libstdc++.so.6.0.33>) = 0
[pid   137] 1770646837.588404 openat(AT_FDCWD</workspace>, "/lib/x86_64-linux-gnu/libgcc_s.so.1", O_RDONLY|O_CLOEXEC) = 6</usr/lib/x86_64-linux-gnu/libgcc_s.so.1>
[pid   137] 1770646837.588959 close(6</usr/lib/x86_64-linux-gnu/libgcc_s.so.1>) = 0
[pid   137] 1770646837.590298 openat(AT_FDCWD</workspace>, "/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 6</etc/ld.so.cache>
[pid   137] 1770646837.590678 close(6</etc/ld.so.cache>) = 0
[pid   137] 1770646837.590820 openat(AT_FDCWD</workspace>, "/lib/x86_64-linux-gnu/libicui18n.so.74", O_RDONLY|O_CLOEXEC) = 6</usr/lib/x86_64-linux-gnu/libicui18n.so.74.2>
[pid   137] 1770646837.591551 close(6</usr/lib/x86_64-linux-gnu/libicui18n.so.74.2>) = 0
[pid   137] 1770646837.592432 write(1</dev/pts/0<char 136:0>>, "\33[?1h\33=", 7) = 7
[pid   137] 1770646837.592944 write(3</dev/pts/0<char 136:0>>, "Hello, World\n", 13Hello, World
) = 13
[pid   139] 1770646837.593311 +++ exited with 0 +++
[pid   138] 1770646837.593343 +++ exited with 0 +++
1770646837.594304 +++ exited with 0 +++

以下は、fd=3はstdout(1)の複製(CLOEXEC付き)を示しており、dotnetランタイムは起動時にstdoutFD=1F_DUPFD_CLOEXECFD=3dup(close-on-exec 付き)していることが分かります。

fcntl(1</dev/pts/0<char 136:0>>, F_DUPFD_CLOEXEC, 0) = 3</dev/pts/0<char 136:0>>

実際にdotnet/runtimeを見に行くと、src/coreclr/pal/src/file/file.cppのinit_std_handle関数にて、以下のようにF_DUPFD_CLOEXECでdupしていることが分かります。コメントに元のFILE*(= stdoutなど)をfcloseしても、元のfd(1) を閉じないためにdupして別fdを持つと書かれているので、まさにstrace結果と一致します。

static HANDLE init_std_handle(HANDLE * pStd, FILE *stream)
{
    CPalThread *pThread = InternalGetCurrentThread();
    PAL_ERROR palError = NO_ERROR;
    IPalObject *pFileObject = NULL;
    IPalObject *pRegisteredFile = NULL;
    IDataLock *pDataLock = NULL;
    CFileProcessLocalData *pLocalData = NULL;
    CObjectAttributes oa;

    HANDLE hFile = INVALID_HANDLE_VALUE;
    int new_fd = -1;

    /* duplicate the FILE *, so that we can fclose() in FILECloseHandle without
       closing the original */
    new_fd = fcntl(fileno(stream), F_DUPFD_CLOEXEC, 0); // dup, but with CLOEXEC
    if(-1 == new_fd)
    {
        ERROR("dup() failed; errno is %d (%s)\n", errno, strerror(errno));
        goto done;
    }

System.Nativeでも意図的にF_DUPFD_CLOEXECでdupしており、dotnetランタイム全体でこの方法を採用していることが分かります。

intptr_t SystemNative_Dup(intptr_t oldfd)
{
    int result;
#if HAVE_F_DUPFD_CLOEXEC
    while ((result = fcntl(ToFileDescriptor(oldfd), F_DUPFD_CLOEXEC, 0)) < 0 && errno == EINTR);
#elif HAVE_F_DUPFD
    while ((result = fcntl(ToFileDescriptor(oldfd), F_DUPFD, 0)) < 0 && errno == EINTR);
    // do CLOEXEC here too
    fcntl(result, F_SETFD, FD_CLOEXEC);
#else
    // The main use cases for dup are setting up the classic Unix dance of setting up file descriptors in advance of performing a fork. Since WASI has no fork, these don't apply.
    // https://github.com/bytecodealliance/wasmtime/blob/b2fefe77148582a9b8013e34fe5808ada82b6efc/docs/WASI-rationale.md#why-no-dup
    result = oldfd;
#endif
    return result;
}

int32_t SystemNative_Unlink(const char* path)
{
    int32_t result;
    while ((result = unlink(path)) < 0 && errno == EINTR);
    return result;
}

#ifdef __NR_memfd_create
#ifndef MFD_CLOEXEC
#define MFD_CLOEXEC 0x0001U
#endif
#ifndef MFD_ALLOW_SEALING
#define MFD_ALLOW_SEALING 0x0002U
#endif
#ifndef F_ADD_SEALS
#define F_ADD_SEALS (1024 + 9)
#endif
#ifndef F_SEAL_WRITE
#define F_SEAL_WRITE 0x0008
#endif
#endif

C# JITのwriteシステムコール

C# JITコンパイラが動作しているときのwriteシステムコールを見てみます。以下は、dotnet runでJITコンパイルを行いながらHello Worldを出力したときのstrace結果です。制御コードがHello Worldの出力先はAOTと同様なのでおいておきましょう。

$ strace -e write ./bin/hello_csharp > /dev/null
write(13, "*", 1)                       = 1
write(14, ".NET Finalizer", 14)         = 14
write(13, "*", 1)                       = 1
write(16, ".NET Tiered Com", 15)        = 15
write(15, "*", 1)                       = 1
write(0, "\33[?1h\33=", 7)              = 7
write(24, "Hello, World\n", 13)         = 13
write(4, "\1", 1)                       = 1
+++ exited with 0 +++

.NET Finalizer.NET Tiered Comなどの文字列がwriteシステムコールで出力されているのが気になりますが、これは.NETがスレッドを作って名前を付けているのがwrite()として見えています。Linuxのスレッド名は15文字まで(終端NULを含めて16バイト制限)なので、次のような名前になります。

  • .NET Finalizer: 14字なのでそのまま
  • .NET Tiered Compilation: 先頭15文字だけなので.NET Tiered Comに切り詰められる

.NET FinalizerはGCのファイナライザースレッドであり、ランタイムとしていつでもファイナライズできる状態を作るために、必ず起動します。.NET Tiered CompilationはJITコンパイラのTiered PGO最適化を担当するスレッドです。NativeAOTではJITがなく、またTiered PGO最適化は行われないため、これらスレッドは起動せず、writeシステムコールも発生しません。

この辺りはstraceでログを見ると、もう少し詳しくわかります。

$ strace -f -ttt -e trace=openat,close,fcntl,write -yy -s 200 ./bin/hello_csharp
1770651503.638424 openat(AT_FDCWD</workspace>, "/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 3</etc/ld.so.cache>
1770651503.638872 close(3</etc/ld.so.cache>) = 0
1770651503.639013 openat(AT_FDCWD</workspace>, "/lib/x86_64-linux-gnu/libdl.so.2", O_RDONLY|O_CLOEXEC) = 3</usr/lib/x86_64-linux-gnu/libdl.so.2>
1770651503.639668 close(3</usr/lib/x86_64-linux-gnu/libdl.so.2>) = 0
1770651503.639779 openat(AT_FDCWD</workspace>, "/lib/x86_64-linux-gnu/librt.so.1", O_RDONLY|O_CLOEXEC) = 3</usr/lib/x86_64-linux-gnu/librt.so.1>
1770651503.640466 close(3</usr/lib/x86_64-linux-gnu/librt.so.1>) = 0
1770651503.640575 openat(AT_FDCWD</workspace>, "/lib/x86_64-linux-gnu/libgcc_s.so.1", O_RDONLY|O_CLOEXEC) = 3</usr/lib/x86_64-linux-gnu/libgcc_s.so.1>
1770651503.641275 close(3</usr/lib/x86_64-linux-gnu/libgcc_s.so.1>) = 0
1770651503.641383 openat(AT_FDCWD</workspace>, "/lib/x86_64-linux-gnu/libpthread.so.0", O_RDONLY|O_CLOEXEC) = 3</usr/lib/x86_64-linux-gnu/libpthread.so.0>
1770651503.642019 close(3</usr/lib/x86_64-linux-gnu/libpthread.so.0>) = 0
1770651503.642173 openat(AT_FDCWD</workspace>, "/lib/x86_64-linux-gnu/libm.so.6", O_RDONLY|O_CLOEXEC) = 3</usr/lib/x86_64-linux-gnu/libm.so.6>
1770651503.642672 close(3</usr/lib/x86_64-linux-gnu/libm.so.6>) = 0
1770651503.642775 openat(AT_FDCWD</workspace>, "/lib/x86_64-linux-gnu/libstdc++.so.6", O_RDONLY|O_CLOEXEC) = 3</usr/lib/x86_64-linux-gnu/libstdc++.so.6.0.33>
1770651503.643511 close(3</usr/lib/x86_64-linux-gnu/libstdc++.so.6.0.33>) = 0
1770651503.643629 openat(AT_FDCWD</workspace>, "/lib/x86_64-linux-gnu/libc.so.6", O_RDONLY|O_CLOEXEC) = 3</usr/lib/x86_64-linux-gnu/libc.so.6>
1770651503.644728 close(3</usr/lib/x86_64-linux-gnu/libc.so.6>) = 0
1770651503.659843 openat(AT_FDCWD</workspace>, "/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 3</etc/ld.so.cache>
1770651503.660212 close(3</etc/ld.so.cache>) = 0
1770651503.660343 openat(AT_FDCWD</workspace>, "/lib/x86_64-linux-gnu/glibc-hwcaps/x86-64-v4/liblttng-ust-tracepoint.so.0", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
1770651503.660624 openat(AT_FDCWD</workspace>, "/lib/x86_64-linux-gnu/glibc-hwcaps/x86-64-v3/liblttng-ust-tracepoint.so.0", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
1770651503.660899 openat(AT_FDCWD</workspace>, "/lib/x86_64-linux-gnu/glibc-hwcaps/x86-64-v2/liblttng-ust-tracepoint.so.0", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
1770651503.661116 openat(AT_FDCWD</workspace>, "/lib/x86_64-linux-gnu/liblttng-ust-tracepoint.so.0", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
1770651503.661325 openat(AT_FDCWD</workspace>, "/usr/lib/x86_64-linux-gnu/glibc-hwcaps/x86-64-v4/liblttng-ust-tracepoint.so.0", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
1770651503.661507 openat(AT_FDCWD</workspace>, "/usr/lib/x86_64-linux-gnu/glibc-hwcaps/x86-64-v3/liblttng-ust-tracepoint.so.0", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
1770651503.661742 openat(AT_FDCWD</workspace>, "/usr/lib/x86_64-linux-gnu/glibc-hwcaps/x86-64-v2/liblttng-ust-tracepoint.so.0", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
1770651503.661973 openat(AT_FDCWD</workspace>, "/usr/lib/x86_64-linux-gnu/liblttng-ust-tracepoint.so.0", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
1770651503.662165 openat(AT_FDCWD</workspace>, "/lib/glibc-hwcaps/x86-64-v4/liblttng-ust-tracepoint.so.0", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
1770651503.662366 openat(AT_FDCWD</workspace>, "/lib/glibc-hwcaps/x86-64-v3/liblttng-ust-tracepoint.so.0", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
1770651503.662548 openat(AT_FDCWD</workspace>, "/lib/glibc-hwcaps/x86-64-v2/liblttng-ust-tracepoint.so.0", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
1770651503.662759 openat(AT_FDCWD</workspace>, "/lib/liblttng-ust-tracepoint.so.0", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
1770651503.662937 openat(AT_FDCWD</workspace>, "/usr/lib/glibc-hwcaps/x86-64-v4/liblttng-ust-tracepoint.so.0", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
1770651503.663160 openat(AT_FDCWD</workspace>, "/usr/lib/glibc-hwcaps/x86-64-v3/liblttng-ust-tracepoint.so.0", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
1770651503.663372 openat(AT_FDCWD</workspace>, "/usr/lib/glibc-hwcaps/x86-64-v2/liblttng-ust-tracepoint.so.0", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
1770651503.663581 openat(AT_FDCWD</workspace>, "/usr/lib/liblttng-ust-tracepoint.so.0", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
1770651503.663842 openat(AT_FDCWD</workspace>, "/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 3</etc/ld.so.cache>
1770651503.664154 close(3</etc/ld.so.cache>) = 0
1770651503.664265 openat(AT_FDCWD</workspace>, "/lib/x86_64-linux-gnu/liblttng-ust-tracepoint.so.0", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
1770651503.664416 openat(AT_FDCWD</workspace>, "/usr/lib/x86_64-linux-gnu/liblttng-ust-tracepoint.so.0", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
1770651503.664611 openat(AT_FDCWD</workspace>, "/lib/liblttng-ust-tracepoint.so.0", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
1770651503.664714 openat(AT_FDCWD</workspace>, "/usr/lib/liblttng-ust-tracepoint.so.0", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
1770651503.666744 openat(AT_FDCWD</workspace>, "/workspace/bin/hello_csharp", O_RDONLY) = 3</workspace/bin/hello_csharp>
1770651503.667483 close(3</workspace/bin/hello_csharp>) = 0
1770651503.668334 openat(AT_FDCWD</workspace>, "/workspace/bin/hello_csharp", O_RDONLY) = 3</workspace/bin/hello_csharp>
1770651503.668997 close(3</workspace/bin/hello_csharp>) = 0
1770651503.669736 openat(AT_FDCWD</workspace>, "/workspace/bin/hello_csharp", O_RDONLY) = 3</workspace/bin/hello_csharp>
1770651503.670377 close(3</workspace/bin/hello_csharp>) = 0
1770651503.671028 openat(AT_FDCWD</workspace>, "/workspace/bin/hello_csharp", O_RDONLY) = 3</workspace/bin/hello_csharp>
1770651503.671721 close(3</workspace/bin/hello_csharp>) = 0
1770651503.673748 openat(AT_FDCWD</workspace>, "/proc/self/mountinfo", O_RDONLY) = 3</proc/143/mountinfo>
1770651503.674430 close(3</proc/143/mountinfo>) = 0
1770651503.674546 openat(AT_FDCWD</workspace>, "/proc/self/cgroup", O_RDONLY) = 3</proc/143/cgroup>
1770651503.674831 close(3</proc/143/cgroup>) = 0
strace: Process 144 attached
[pid   143] 1770651503.677794 fcntl(0</dev/pts/0<char 136:0>>, F_DUPFD_CLOEXEC, 0) = 5</dev/pts/0<char 136:0>>
[pid   143] 1770651503.677926 fcntl(1</dev/pts/0<char 136:0>>, F_DUPFD_CLOEXEC, 0) = 6</dev/pts/0<char 136:0>>
[pid   143] 1770651503.678068 fcntl(2</dev/pts/0<char 136:0>>, F_DUPFD_CLOEXEC, 0) = 7</dev/pts/0<char 136:0>>
[pid   143] 1770651503.679079 openat(AT_FDCWD</workspace>, "/sys/fs/cgroup//cpu.max", O_RDONLY) = 8</sys/fs/cgroup/cpu.max>
[pid   143] 1770651503.679363 close(8</sys/fs/cgroup/cpu.max>) = 0
[pid   143] 1770651503.683922 openat(AT_FDCWD</workspace>, "/dev/urandom", O_RDONLY|O_CLOEXEC) = 9</dev/urandom<char 1:9>>
[pid   143] 1770651503.684119 openat(AT_FDCWD</workspace>, "/proc/143/stat", O_RDONLY) = 10</proc/143/stat>
[pid   143] 1770651503.684495 close(10</proc/143/stat>) = 0
strace: Process 145 attached
[pid   143] 1770651503.687465 openat(AT_FDCWD</workspace>, "/sys/devices/system/cpu/online", O_RDONLY|O_CLOEXEC) = 11</sys/devices/system/cpu/online>
[pid   143] 1770651503.687648 close(11</sys/devices/system/cpu/online>) = 0
[pid   143] 1770651503.688005 openat(AT_FDCWD</workspace>, "/proc/self/mountinfo", O_RDONLY) = 11</proc/143/mountinfo>
[pid   143] 1770651503.688585 close(11</proc/143/mountinfo>) = 0
[pid   143] 1770651503.688690 openat(AT_FDCWD</workspace>, "/proc/self/cgroup", O_RDONLY) = 11</proc/143/cgroup>
[pid   143] 1770651503.688925 close(11</proc/143/cgroup>) = 0
[pid   143] 1770651503.691797 openat(AT_FDCWD</workspace>, "/proc/143/stat", O_RDONLY) = 11</proc/143/stat>
[pid   143] 1770651503.692066 close(11</proc/143/stat>) = 0
[pid   143] 1770651503.692179 openat(AT_FDCWD</workspace>, "/proc/143/stat", O_RDONLY) = 11</proc/143/stat>
[pid   143] 1770651503.692478 close(11</proc/143/stat>) = 0
strace: Process 146 attached
[pid   146] 1770651503.694266 openat(AT_FDCWD</workspace>, "/tmp/clr-debug-pipe-143-4092879-in", O_RDONLYstrace: Process 147 attached
 <unfinished ...>
[pid   143] 1770651503.695169 write(13<pipe:[654821]>, "*", 1) = 1
[pid   143] 1770651503.695294 openat(AT_FDCWD</workspace>, "/proc/143/stat", O_RDONLY <unfinished ...>
[pid   147] 1770651503.695332 close(12<pipe:[654821]> <unfinished ...>
[pid   143] 1770651503.695361 <... openat resumed>) = 14</proc/143/stat>
[pid   147] 1770651503.695388 <... close resumed>) = 0
[pid   147] 1770651503.695429 close(13<pipe:[654821]>) = 0
[pid   143] 1770651503.695560 close(14</proc/143/stat>) = 0
[pid   143] 1770651503.695670 openat(AT_FDCWD</workspace>, "/dev/shm/sem.clrst0000008f00000000003e73cf", O_RDWR|O_NOFOLLOW|O_CLOEXEC) = -1 ENOENT (No such file or directory)
[pid   143] 1770651503.696489 openat(AT_FDCWD</workspace>, "/sys/fs/cgroup//memory.max", O_RDONLY) = 12</sys/fs/cgroup/memory.max>
[pid   143] 1770651503.696794 close(12</sys/fs/cgroup/memory.max>) = 0
[pid   143] 1770651503.697425 openat(AT_FDCWD</workspace>, "/sys/devices/system/cpu/cpu0/cache/index0/size", O_RDONLY) = 12</sys/devices/system/cpu/cpu0/cache/index0/size>
[pid   143] 1770651503.697800 close(12</sys/devices/system/cpu/cpu0/cache/index0/size>) = 0
[pid   143] 1770651503.697912 openat(AT_FDCWD</workspace>, "/sys/devices/system/cpu/cpu0/cache/index0/level", O_RDONLY) = 12</sys/devices/system/cpu/cpu0/cache/index0/level>
[pid   143] 1770651503.698353 close(12</sys/devices/system/cpu/cpu0/cache/index0/level>) = 0
[pid   143] 1770651503.698491 openat(AT_FDCWD</workspace>, "/sys/devices/system/cpu/cpu0/cache/index1/size", O_RDONLY) = 12</sys/devices/system/cpu/cpu0/cache/index1/size>
[pid   143] 1770651503.698789 close(12</sys/devices/system/cpu/cpu0/cache/index1/size>) = 0
[pid   143] 1770651503.698909 openat(AT_FDCWD</workspace>, "/sys/devices/system/cpu/cpu0/cache/index1/level", O_RDONLY) = 12</sys/devices/system/cpu/cpu0/cache/index1/level>
[pid   143] 1770651503.699260 close(12</sys/devices/system/cpu/cpu0/cache/index1/level>) = 0
[pid   143] 1770651503.699371 openat(AT_FDCWD</workspace>, "/sys/devices/system/cpu/cpu0/cache/index2/size", O_RDONLY) = 12</sys/devices/system/cpu/cpu0/cache/index2/size>
[pid   143] 1770651503.699689 close(12</sys/devices/system/cpu/cpu0/cache/index2/size>) = 0
[pid   143] 1770651503.699824 openat(AT_FDCWD</workspace>, "/sys/devices/system/cpu/cpu0/cache/index2/level", O_RDONLY) = 12</sys/devices/system/cpu/cpu0/cache/index2/level>
[pid   143] 1770651503.700127 close(12</sys/devices/system/cpu/cpu0/cache/index2/level>) = 0
[pid   143] 1770651503.700243 openat(AT_FDCWD</workspace>, "/sys/devices/system/cpu/cpu0/cache/index3/size", O_RDONLY) = 12</sys/devices/system/cpu/cpu0/cache/index3/size>
[pid   143] 1770651503.700559 close(12</sys/devices/system/cpu/cpu0/cache/index3/size>) = 0
[pid   143] 1770651503.700679 openat(AT_FDCWD</workspace>, "/sys/devices/system/cpu/cpu0/cache/index3/level", O_RDONLY) = 12</sys/devices/system/cpu/cpu0/cache/index3/level>
[pid   143] 1770651503.700997 close(12</sys/devices/system/cpu/cpu0/cache/index3/level>) = 0
[pid   143] 1770651503.701105 openat(AT_FDCWD</workspace>, "/sys/devices/system/cpu/cpu0/cache/index4/size", O_RDONLY) = -1 ENOENT (No such file or directory)
[pid   143] 1770651503.702578 openat(AT_FDCWD</workspace>, "/proc/meminfo", O_RDONLY) = 12</proc/meminfo>
[pid   143] 1770651503.702830 close(12</proc/meminfo>) = 0
[pid   143] 1770651503.703978 openat(AT_FDCWD</workspace>, "/proc/self/maps", O_RDONLY|O_CLOEXEC) = 12</proc/143/maps>
[pid   143] 1770651503.704811 close(12</proc/143/maps>) = 0
[pid   143] 1770651503.704991 openat(AT_FDCWD</workspace>, "/proc/self/maps", O_RDONLY|O_CLOEXEC) = 12</proc/143/maps>
[pid   143] 1770651503.706019 close(12</proc/143/maps>) = 0
strace: Process 148 attached
[pid   143] 1770651503.707664 openat(AT_FDCWD</workspace>, "/proc/self/task/148/comm", O_RDWR) = 14</proc/143/task/148/comm>
[pid   143] 1770651503.707795 write(14</proc/143/task/148/comm>, ".NET Finalizer", 14) = 14
[pid   143] 1770651503.707905 close(14</proc/143/task/148/comm>) = 0
[pid   143] 1770651503.708034 write(13<pipe:[656760]>, "*", 1) = 1
[pid   148] 1770651503.708204 close(12<pipe:[656760]>) = 0
[pid   148] 1770651503.708355 close(13<pipe:[656760]>) = 0
[pid   143] 1770651503.708851 openat(AT_FDCWD</workspace>, "/workspace/bin/hello_csharp", O_RDONLY) = 12</workspace/bin/hello_csharp>
[pid   143] 1770651503.709311 fcntl(12</workspace/bin/hello_csharp>, F_SETFD, FD_CLOEXEC) = 0
[pid   143] 1770651503.709529 fcntl(12</workspace/bin/hello_csharp>, F_DUPFD_CLOEXEC, 0) = 13</workspace/bin/hello_csharp>
strace: Process 149 attached
[pid   143] 1770651503.739900 openat(AT_FDCWD</workspace>, "/proc/self/task/149/comm", O_RDWR) = 16</proc/143/task/149/comm>
[pid   143] 1770651503.740087 write(16</proc/143/task/149/comm>, ".NET Tiered Com", 15) = 15
[pid   143] 1770651503.740251 close(16</proc/143/task/149/comm>) = 0
[pid   143] 1770651503.740380 write(15<pipe:[654827]>, "*", 1) = 1
[pid   149] 1770651503.740714 close(14<pipe:[654827]>) = 0
[pid   149] 1770651503.740860 close(15<pipe:[654827]>) = 0
[pid   143] 1770651503.759910 openat(AT_FDCWD</workspace>, "/workspace/bin/hello_csharp", O_RDONLY) = 14</workspace/bin/hello_csharp>
[pid   143] 1770651503.760370 fcntl(14</workspace/bin/hello_csharp>, F_SETFD, FD_CLOEXEC) = 0
[pid   143] 1770651503.760596 fcntl(14</workspace/bin/hello_csharp>, F_DUPFD_CLOEXEC, 0) = 15</workspace/bin/hello_csharp>
[pid   143] 1770651503.762460 openat(AT_FDCWD</workspace>, "/workspace/bin/hello_csharp", O_RDONLY) = 16</workspace/bin/hello_csharp>
[pid   143] 1770651503.762883 fcntl(16</workspace/bin/hello_csharp>, F_SETFD, FD_CLOEXEC) = 0
[pid   143] 1770651503.763136 fcntl(16</workspace/bin/hello_csharp>, F_DUPFD_CLOEXEC, 0) = 17</workspace/bin/hello_csharp>
[pid   143] 1770651503.769514 openat(AT_FDCWD</workspace>, "/workspace/bin/hello_csharp", O_RDONLY) = 18</workspace/bin/hello_csharp>
[pid   143] 1770651503.770035 fcntl(18</workspace/bin/hello_csharp>, F_SETFD, FD_CLOEXEC) = 0
[pid   143] 1770651503.770286 fcntl(18</workspace/bin/hello_csharp>, F_DUPFD_CLOEXEC, 0) = 19</workspace/bin/hello_csharp>
[pid   143] 1770651503.779217 openat(AT_FDCWD</workspace>, "/workspace/bin/hello_csharp", O_RDONLY) = 20</workspace/bin/hello_csharp>
[pid   143] 1770651503.779738 fcntl(20</workspace/bin/hello_csharp>, F_SETFD, FD_CLOEXEC) = 0
[pid   143] 1770651503.780015 fcntl(20</workspace/bin/hello_csharp>, F_DUPFD_CLOEXEC, 0) = 21</workspace/bin/hello_csharp>
[pid   143] 1770651503.786433 openat(AT_FDCWD</workspace>, "/workspace/bin/hello_csharp", O_RDONLY) = 22</workspace/bin/hello_csharp>
[pid   143] 1770651503.786897 fcntl(22</workspace/bin/hello_csharp>, F_SETFD, FD_CLOEXEC) = 0
[pid   143] 1770651503.787267 fcntl(22</workspace/bin/hello_csharp>, F_DUPFD_CLOEXEC, 0) = 23</workspace/bin/hello_csharp>
[pid   143] 1770651503.790962 fcntl(1</dev/pts/0<char 136:0>>, F_DUPFD_CLOEXEC, 0) = 24</dev/pts/0<char 136:0>>
[pid   143] 1770651503.799356 openat(AT_FDCWD</workspace>, "/workspace/bin/hello_csharp", O_RDONLY) = 25</workspace/bin/hello_csharp>
[pid   143] 1770651503.799878 fcntl(25</workspace/bin/hello_csharp>, F_SETFD, FD_CLOEXEC) = 0
[pid   143] 1770651503.800107 fcntl(25</workspace/bin/hello_csharp>, F_DUPFD_CLOEXEC, 0) = 26</workspace/bin/hello_csharp>
strace: Process 150 attached
[pid   143] 1770651503.804328 openat(AT_FDCWD</workspace>, "/root/.terminfo/x/xterm", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
[pid   143] 1770651503.805457 openat(AT_FDCWD</workspace>, "/root/.terminfo/78/xterm", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
[pid   143] 1770651503.805598 openat(AT_FDCWD</workspace>, "/etc/terminfo/x/xterm", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
[pid   143] 1770651503.805738 openat(AT_FDCWD</workspace>, "/etc/terminfo/78/xterm", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
[pid   143] 1770651503.805889 openat(AT_FDCWD</workspace>, "/lib/terminfo/x/xterm", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
[pid   143] 1770651503.806050 openat(AT_FDCWD</workspace>, "/lib/terminfo/78/xterm", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
[pid   143] 1770651503.806268 openat(AT_FDCWD</workspace>, "/usr/share/terminfo/x/xterm", O_RDONLY|O_CLOEXEC) = 29</usr/share/terminfo/x/xterm>
[pid   143] 1770651503.807384 openat(AT_FDCWD</workspace>, "/workspace/bin/hello_csharp", O_RDONLY) = 30</workspace/bin/hello_csharp>
[pid   143] 1770651503.807820 fcntl(30</workspace/bin/hello_csharp>, F_SETFD, FD_CLOEXEC) = 0
[pid   143] 1770651503.808029 fcntl(30</workspace/bin/hello_csharp>, F_DUPFD_CLOEXEC, 0) = 31</workspace/bin/hello_csharp>
[pid   143] 1770651503.811379 openat(AT_FDCWD</workspace>, "/workspace/bin/hello_csharp", O_RDONLY) = 32</workspace/bin/hello_csharp>
[pid   143] 1770651503.811898 fcntl(32</workspace/bin/hello_csharp>, F_SETFD, FD_CLOEXEC) = 0
[pid   143] 1770651503.812202 fcntl(32</workspace/bin/hello_csharp>, F_DUPFD_CLOEXEC, 0) = 33</workspace/bin/hello_csharp>
[pid   143] 1770651503.819234 close(29</usr/share/terminfo/x/xterm>) = 0
[pid   143] 1770651503.821484 openat(AT_FDCWD</workspace>, "/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 29</etc/ld.so.cache>
[pid   143] 1770651503.821744 close(29</etc/ld.so.cache>) = 0
[pid   143] 1770651503.821877 openat(AT_FDCWD</workspace>, "/lib/x86_64-linux-gnu/libicuuc.so.90", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
[pid   143] 1770651503.822027 openat(AT_FDCWD</workspace>, "/usr/lib/x86_64-linux-gnu/libicuuc.so.90", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
[pid   143] 1770651503.822180 openat(AT_FDCWD</workspace>, "/lib/libicuuc.so.90", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
[pid   143] 1770651503.822296 openat(AT_FDCWD</workspace>, "/usr/lib/libicuuc.so.90", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
[pid   143] 1770651503.822496 openat(AT_FDCWD</workspace>, "/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 29</etc/ld.so.cache>
[pid   143] 1770651503.822790 close(29</etc/ld.so.cache>) = 0
[pid   143] 1770651503.822908 openat(AT_FDCWD</workspace>, "/lib/x86_64-linux-gnu/libicuuc.so.89", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
[pid   143] 1770651503.823061 openat(AT_FDCWD</workspace>, "/usr/lib/x86_64-linux-gnu/libicuuc.so.89", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
[pid   143] 1770651503.823191 openat(AT_FDCWD</workspace>, "/lib/libicuuc.so.89", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
[pid   143] 1770651503.823323 openat(AT_FDCWD</workspace>, "/usr/lib/libicuuc.so.89", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
[pid   143] 1770651503.823533 openat(AT_FDCWD</workspace>, "/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 29</etc/ld.so.cache>
[pid   143] 1770651503.823862 close(29</etc/ld.so.cache>) = 0
[pid   143] 1770651503.823971 openat(AT_FDCWD</workspace>, "/lib/x86_64-linux-gnu/libicuuc.so.88", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
[pid   143] 1770651503.824115 openat(AT_FDCWD</workspace>, "/usr/lib/x86_64-linux-gnu/libicuuc.so.88", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
[pid   143] 1770651503.824245 openat(AT_FDCWD</workspace>, "/lib/libicuuc.so.88", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
[pid   143] 1770651503.824350 openat(AT_FDCWD</workspace>, "/usr/lib/libicuuc.so.88", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
[pid   143] 1770651503.824537 openat(AT_FDCWD</workspace>, "/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 29</etc/ld.so.cache>
[pid   143] 1770651503.824873 close(29</etc/ld.so.cache>) = 0
[pid   143] 1770651503.824996 openat(AT_FDCWD</workspace>, "/lib/x86_64-linux-gnu/libicuuc.so.87", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
[pid   143] 1770651503.825210 openat(AT_FDCWD</workspace>, "/usr/lib/x86_64-linux-gnu/libicuuc.so.87", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
[pid   143] 1770651503.825320 openat(AT_FDCWD</workspace>, "/lib/libicuuc.so.87", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
[pid   143] 1770651503.825400 openat(AT_FDCWD</workspace>, "/usr/lib/libicuuc.so.87", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
[pid   143] 1770651503.825577 openat(AT_FDCWD</workspace>, "/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 29</etc/ld.so.cache>
[pid   143] 1770651503.825921 close(29</etc/ld.so.cache>) = 0
[pid   143] 1770651503.826037 openat(AT_FDCWD</workspace>, "/lib/x86_64-linux-gnu/libicuuc.so.86", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
[pid   143] 1770651503.826181 openat(AT_FDCWD</workspace>, "/usr/lib/x86_64-linux-gnu/libicuuc.so.86", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
[pid   143] 1770651503.826345 openat(AT_FDCWD</workspace>, "/lib/libicuuc.so.86", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
[pid   143] 1770651503.826517 openat(AT_FDCWD</workspace>, "/usr/lib/libicuuc.so.86", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
[pid   143] 1770651503.826743 openat(AT_FDCWD</workspace>, "/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 29</etc/ld.so.cache>
[pid   143] 1770651503.827013 close(29</etc/ld.so.cache>) = 0
[pid   143] 1770651503.827184 openat(AT_FDCWD</workspace>, "/lib/x86_64-linux-gnu/libicuuc.so.85", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
[pid   143] 1770651503.827308 openat(AT_FDCWD</workspace>, "/usr/lib/x86_64-linux-gnu/libicuuc.so.85", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
[pid   143] 1770651503.827414 openat(AT_FDCWD</workspace>, "/lib/libicuuc.so.85", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
[pid   143] 1770651503.827546 openat(AT_FDCWD</workspace>, "/usr/lib/libicuuc.so.85", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
[pid   143] 1770651503.827743 openat(AT_FDCWD</workspace>, "/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 29</etc/ld.so.cache>
[pid   143] 1770651503.828080 close(29</etc/ld.so.cache>) = 0
[pid   143] 1770651503.828260 openat(AT_FDCWD</workspace>, "/lib/x86_64-linux-gnu/libicuuc.so.84", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
[pid   143] 1770651503.828410 openat(AT_FDCWD</workspace>, "/usr/lib/x86_64-linux-gnu/libicuuc.so.84", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
[pid   143] 1770651503.828527 openat(AT_FDCWD</workspace>, "/lib/libicuuc.so.84", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
[pid   143] 1770651503.828655 openat(AT_FDCWD</workspace>, "/usr/lib/libicuuc.so.84", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
[pid   143] 1770651503.828891 openat(AT_FDCWD</workspace>, "/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 29</etc/ld.so.cache>
[pid   143] 1770651503.829219 close(29</etc/ld.so.cache>) = 0
[pid   143] 1770651503.829330 openat(AT_FDCWD</workspace>, "/lib/x86_64-linux-gnu/libicuuc.so.83", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
[pid   143] 1770651503.829457 openat(AT_FDCWD</workspace>, "/usr/lib/x86_64-linux-gnu/libicuuc.so.83", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
[pid   143] 1770651503.829600 openat(AT_FDCWD</workspace>, "/lib/libicuuc.so.83", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
[pid   143] 1770651503.829721 openat(AT_FDCWD</workspace>, "/usr/lib/libicuuc.so.83", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
[pid   143] 1770651503.829992 openat(AT_FDCWD</workspace>, "/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 29</etc/ld.so.cache>
[pid   143] 1770651503.830294 close(29</etc/ld.so.cache>) = 0
[pid   143] 1770651503.830407 openat(AT_FDCWD</workspace>, "/lib/x86_64-linux-gnu/libicuuc.so.82", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
[pid   143] 1770651503.830538 openat(AT_FDCWD</workspace>, "/usr/lib/x86_64-linux-gnu/libicuuc.so.82", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
[pid   143] 1770651503.830661 openat(AT_FDCWD</workspace>, "/lib/libicuuc.so.82", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
[pid   143] 1770651503.830761 openat(AT_FDCWD</workspace>, "/usr/lib/libicuuc.so.82", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
[pid   143] 1770651503.830942 openat(AT_FDCWD</workspace>, "/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 29</etc/ld.so.cache>
[pid   143] 1770651503.831250 close(29</etc/ld.so.cache>) = 0
[pid   143] 1770651503.831386 openat(AT_FDCWD</workspace>, "/lib/x86_64-linux-gnu/libicuuc.so.81", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
[pid   143] 1770651503.831513 openat(AT_FDCWD</workspace>, "/usr/lib/x86_64-linux-gnu/libicuuc.so.81", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
[pid   143] 1770651503.831643 openat(AT_FDCWD</workspace>, "/lib/libicuuc.so.81", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
[pid   143] 1770651503.831818 openat(AT_FDCWD</workspace>, "/usr/lib/libicuuc.so.81", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
[pid   143] 1770651503.832110 openat(AT_FDCWD</workspace>, "/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 29</etc/ld.so.cache>
[pid   143] 1770651503.832416 close(29</etc/ld.so.cache>) = 0
[pid   143] 1770651503.832537 openat(AT_FDCWD</workspace>, "/lib/x86_64-linux-gnu/libicuuc.so.80", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
[pid   143] 1770651503.832691 openat(AT_FDCWD</workspace>, "/usr/lib/x86_64-linux-gnu/libicuuc.so.80", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
[pid   143] 1770651503.832827 openat(AT_FDCWD</workspace>, "/lib/libicuuc.so.80", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
[pid   143] 1770651503.832943 openat(AT_FDCWD</workspace>, "/usr/lib/libicuuc.so.80", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
[pid   143] 1770651503.833210 openat(AT_FDCWD</workspace>, "/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 29</etc/ld.so.cache>
[pid   143] 1770651503.833542 close(29</etc/ld.so.cache>) = 0
[pid   143] 1770651503.833681 openat(AT_FDCWD</workspace>, "/lib/x86_64-linux-gnu/libicuuc.so.79", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
[pid   143] 1770651503.833836 openat(AT_FDCWD</workspace>, "/usr/lib/x86_64-linux-gnu/libicuuc.so.79", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
[pid   143] 1770651503.833988 openat(AT_FDCWD</workspace>, "/lib/libicuuc.so.79", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
[pid   143] 1770651503.834114 openat(AT_FDCWD</workspace>, "/usr/lib/libicuuc.so.79", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
[pid   143] 1770651503.834362 openat(AT_FDCWD</workspace>, "/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 29</etc/ld.so.cache>
[pid   143] 1770651503.834618 close(29</etc/ld.so.cache>) = 0
[pid   143] 1770651503.834710 openat(AT_FDCWD</workspace>, "/lib/x86_64-linux-gnu/libicuuc.so.78", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
[pid   143] 1770651503.834952 openat(AT_FDCWD</workspace>, "/usr/lib/x86_64-linux-gnu/libicuuc.so.78", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
[pid   143] 1770651503.835083 openat(AT_FDCWD</workspace>, "/lib/libicuuc.so.78", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
[pid   143] 1770651503.835207 openat(AT_FDCWD</workspace>, "/usr/lib/libicuuc.so.78", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
[pid   143] 1770651503.835438 openat(AT_FDCWD</workspace>, "/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 29</etc/ld.so.cache>
[pid   143] 1770651503.835794 close(29</etc/ld.so.cache>) = 0
[pid   143] 1770651503.835904 openat(AT_FDCWD</workspace>, "/lib/x86_64-linux-gnu/libicuuc.so.77", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
[pid   143] 1770651503.836046 openat(AT_FDCWD</workspace>, "/usr/lib/x86_64-linux-gnu/libicuuc.so.77", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
[pid   143] 1770651503.836204 openat(AT_FDCWD</workspace>, "/lib/libicuuc.so.77", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
[pid   143] 1770651503.836366 openat(AT_FDCWD</workspace>, "/usr/lib/libicuuc.so.77", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
[pid   143] 1770651503.836645 openat(AT_FDCWD</workspace>, "/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 29</etc/ld.so.cache>
[pid   143] 1770651503.836936 close(29</etc/ld.so.cache>) = 0
[pid   143] 1770651503.837088 openat(AT_FDCWD</workspace>, "/lib/x86_64-linux-gnu/libicuuc.so.76", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
[pid   143] 1770651503.837257 openat(AT_FDCWD</workspace>, "/usr/lib/x86_64-linux-gnu/libicuuc.so.76", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
[pid   143] 1770651503.837387 openat(AT_FDCWD</workspace>, "/lib/libicuuc.so.76", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
[pid   143] 1770651503.837559 openat(AT_FDCWD</workspace>, "/usr/lib/libicuuc.so.76", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
[pid   143] 1770651503.837851 openat(AT_FDCWD</workspace>, "/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 29</etc/ld.so.cache>
[pid   143] 1770651503.838170 close(29</etc/ld.so.cache>) = 0
[pid   143] 1770651503.838272 openat(AT_FDCWD</workspace>, "/lib/x86_64-linux-gnu/libicuuc.so.75", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
[pid   143] 1770651503.838409 openat(AT_FDCWD</workspace>, "/usr/lib/x86_64-linux-gnu/libicuuc.so.75", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
[pid   143] 1770651503.838562 openat(AT_FDCWD</workspace>, "/lib/libicuuc.so.75", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
[pid   143] 1770651503.838724 openat(AT_FDCWD</workspace>, "/usr/lib/libicuuc.so.75", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
[pid   143] 1770651503.838965 openat(AT_FDCWD</workspace>, "/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 29</etc/ld.so.cache>
[pid   143] 1770651503.839287 close(29</etc/ld.so.cache>) = 0
[pid   143] 1770651503.839394 openat(AT_FDCWD</workspace>, "/lib/x86_64-linux-gnu/libicuuc.so.74", O_RDONLY|O_CLOEXEC) = 29</usr/lib/x86_64-linux-gnu/libicuuc.so.74.2>
[pid   143] 1770651503.840074 close(29</usr/lib/x86_64-linux-gnu/libicuuc.so.74.2>) = 0
[pid   143] 1770651503.840189 openat(AT_FDCWD</workspace>, "/lib/x86_64-linux-gnu/libicudata.so.74", O_RDONLY|O_CLOEXEC) = 29</usr/lib/x86_64-linux-gnu/libicudata.so.74.2>
[pid   143] 1770651503.840985 close(29</usr/lib/x86_64-linux-gnu/libicudata.so.74.2>) = 0
[pid   143] 1770651503.841575 openat(AT_FDCWD</workspace>, "/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 29</etc/ld.so.cache>
[pid   143] 1770651503.841994 close(29</etc/ld.so.cache>) = 0
[pid   143] 1770651503.842160 openat(AT_FDCWD</workspace>, "/lib/x86_64-linux-gnu/libicui18n.so.74", O_RDONLY|O_CLOEXEC) = 29</usr/lib/x86_64-linux-gnu/libicui18n.so.74.2>
[pid   143] 1770651503.842904 close(29</usr/lib/x86_64-linux-gnu/libicui18n.so.74.2>) = 0
[pid   143] 1770651503.847992 write(1</dev/pts/0<char 136:0>>, "\33[?1h\33=", 7) = 7
[pid   143] 1770651503.848572 write(24</dev/pts/0<char 136:0>>, "Hello, World\n", 13Hello, World
) = 13
[pid   143] 1770651503.850376 write(4<pipe:[653798]>, "\1", 1) = 1
[pid   144] 1770651503.850599 close(4<pipe:[653798]>) = 0
[pid   146] 1770651503.851271 <... openat resumed>) = ?
[pid   150] 1770651503.851382 +++ exited with 0 +++
[pid   149] 1770651503.851403 +++ exited with 0 +++
[pid   148] 1770651503.851425 +++ exited with 0 +++
[pid   147] 1770651503.851445 +++ exited with 0 +++
[pid   146] 1770651503.851456 +++ exited with 0 +++
[pid   145] 1770651503.851479 +++ exited with 0 +++
[pid   144] 1770651503.856690 +++ exited with 0 +++
1770651503.856716 +++ exited with 0 +++

ログを見ているとclr-debug-pipeというパイプで診断情報をやり取りしていますが、これは診断用なのでDOTNET_EnableDiagnostics=0を指定すれば止められます。もし診断情報を止めた状態でstraceを取りたい場合は、以下のようにします。システムコール的には1つ減るだけですが、診断情報は不要なら止める判断もあるでしょう。

$ DOTNET_EnableDiagnostics=0 strace -f -ttt -e trace=openat,close,fcntl,write -yy -s 200 ./bin/hello_csharp

まとめ

C#のstraceを追いかけてみましたが、面白かったです。こうやってみていくとstraceのシステムコールが少ないからよい、とは一概には言えないと感じます。

C#のdotnetランタイム(CoreCLR)は、GC最適化やJITコンパイラのために環境情報を収集しており、これにより実行時のパフォーマンスを向上させています。仮にdotnetランタイムが/proc/sysを見に行くシステムコールをしていない場合、それはGC、JITコンパイラのTiered PGO最適化、その他ランタイム最適化のための情報を取得できないことを意味します。CやRustのように、メモリ管理や最適化を手動で行う言語と異なり、C#などのマネージドランタイムは、ランタイムがシステムの状態を把握して最適化を行うことに依存しています。したがって、必要なシステムコールはある程度存在するでしょう。

一方で、C# AOTのシステムコールは起動時のシステムコールを最小限に抑えつつ、必要な最適化情報を取得するよう設計されています。straceで追いかけていて無駄だなーと感じるポイントが少なく、だいぶん頑張っているなーという印象です。このあたりは、AOTとJITの違いがよく出ていますね。

参考

元記事

本記事の再現をGitHub


  1. 共有ライブラリビルドをするには、go install -buildmode=shared stdが必要です。
  2. ちなみにDOTNET_EnableDiagnostics=0で診断系を止めても、strace上は変化がありません。

GitHub Actionsで他のワークフローやリポジトリのアーティファクトをダウンロードする

GitHub Actionsでアーティファクトのアップロードはactions/upload-artifact、ダウンロードはactions/download-artifactで行います。普段は同一ワークフローのジョブ間でアーティファクトを受け渡すのに使っていますが、他のワークフローやリポジトリのアーティファクトをダウンロードしたい場合もあります。

例えば、あるワークフローでネイティブビルドを行っていて10分かかります。そのビルド成果物を別のワークフローで使い回したいときに、改めてビルドすることなくダウンロードできます。ビルドで一番つらいのは時間がかかることなので、ビルド成果物を使い回せると助かりますよね。

今回は、actions/download-artifactでワークフローやリポジトリを跨いで、アーティファクトをダウンロードできるという話です。シラナカッタ。

何ができるのか

actions/download-artifact(v4以降/v3とv4の差分)を使って、他のワークフローやリポジトリのアーティファクトをダウンロードできます。例えば次のように書くと、run-idで指定した別ワークフローの実行履歴11100002222でアップロードされたすべてのアーティファクトを、今のワークフローにダウンロードします。

- uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0
  with:
    run-id: "11100002222"
    github-token: ${{ github.token }}
  • run-id: 対象ワークフローの履歴ページURLの末尾にある数字列。例えば、https://github.com/<ORG>/<REPO>/actions/runs/123456789なら123456789
  • github-token: アクセス権限のあるGitHubトークン。同一リポジトリなら${{ github.token }}を指定すればOK。通常は無指定なので、指定必須
  • name: 省略するとすべてのアーティファクトをダウンロード、特定の名前のアーティファクトだけ欲しい場合は指定

なお、ドキュメントにはactions: read権限をジョブにつけるように書かれていますが、同一リポジトリの場合は権限を指定しなくてもダウンロードできます。

使い方

私のリポジトリguitarrapc/githubactions-labを使って実際の挙動を見つつ使い方を確認しましょう。 今回は、同一リポジトリ内の他ワークフローからアーティファクトをダウンロードします。

アーティファクトをアップロードするワークフローを用意する

まずは、actions/upload-artifactでアップロードするワークフローartifacts-targz.yamlを用意して実行します。(artifacts (tar.gz) #378)

URLから、この時のrun-id21203098493とわかります。

ワークフロー実行結果にoutput.tar.gzが添付されている

name: artifacts (tar.gz)
on:
  workflow_dispatch:
  pull_request:
    branches: [main]
  push:
    branches: [main]

jobs:
  # tar.gz
  upload-targz:
    permissions:
      contents: read
    runs-on: ubuntu-24.04
    timeout-minutes: 3
    steps:
      - name: output
        run: |
          mkdir -p ./output/bin
          echo "hoge" > ./output/hoge.txt
          echo "fuga" > ./output/fuga.txt
          echo "foo" > ./output/bin/foo.txt
          echo "bar" > ./output/bin/bar.txt
          tar -zcvf output.tar.gz ./output/
      - uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
        with:
          name: output.tar.gz
          path: ./output.tar.gz
          retention-days: 1

  download-targz:
    needs: [upload-targz]
    # ... ただのアップロード確認なので省略

アップロードができたら、次に他のワークフローからダウンロードしてみましょう。

他のワークフローのアーティファクトをダウンロードする

次に、上記ワークフローでアップロードしたアーティファクトをダウンロードするワークフローartifacts-other-workflow.yamlを用意します。 run-idを指定した場合は「run-idの履歴」から、指定しなかった場合は「最新の成功ビルド履歴」からアーティファクトをダウンロードします。

name: artifacts (other workflow)
on:
  workflow_dispatch:
    inputs:
      workflow-name:
        description: "Workflow name to download artifacts from"
        required: true
        default: "artifacts-targz.yaml"
      run-id:
        description: "Run ID to download artifacts from (optional)"
        required: false
        default: ""

jobs:
  download-directory:
    permissions:
      contents: read
    runs-on: ubuntu-24.04
    timeout-minutes: 3
    steps:
      - name: List Run Ids of specified workflow
        run: gh run list -w ${{ inputs.workflow-name }} --status completed --limit 5
        env:
          GH_REPO: ${{ github.repository }}
          GH_TOKEN: ${{ github.token }}
      - name: Get latest Run Id of specified workflow
        if: ${{ inputs.run-id == '' }}
        id: get-run-id
        run: |
          run_id=$(gh run list -w ${{ inputs.workflow-name }} --status completed --limit 1 --json databaseId --jq ".[].databaseId")
          echo "run_id=$run_id" | tee -a "$GITHUB_OUTPUT"
        env:
          GH_REPO: ${{ github.repository }}
          GH_TOKEN: ${{ github.token }}
      - name: Get run details
        run: gh run view ${{ inputs.run-id || steps.get-run-id.outputs.run_id }}
        env:
          GH_REPO: ${{ github.repository }}
          GH_TOKEN: ${{ github.token }}
      - uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0
        with:
          run-id: ${{ inputs.run-id || steps.get-run-id.outputs.run_id }}
          github-token: ${{ github.token }}
      - name: ls
        run: ls -lR

実行してみましょう。run-idを指定してもいいですが、今回は空にして最新のビルドからダウンロードさせます。

workflow_dispatchで実行する

実行履歴をみると、指定したワークフローの最新成功ビルド履歴のrun-id21203098493からダウンロードできています。

actions/download-artifactで他ワークフローのアーティファクトをダウンロードできている

Run actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131
  with:
    run-id: 21203098493
    github-token: ***
    merge-multiple: false
    repository: guitarrapc/githubactions-lab
Fetching artifact list for workflow run 21203098493 in repository guitarrapc/githubactions-lab
Found 1 artifact(s)
No input name, artifact-ids or pattern filtered specified, downloading all artifacts
An extra directory with the artifact name will be created for each download
Preparing to download the following artifacts:
- output.tar.gz (ID: 5201415391, Size: 379, Expected Digest: sha256:81f29b05074a1ed328654302ee20a68e087f13788371ad1ef3cdeb2ddf4c0972)
Downloading artifact '5201415391' from 'guitarrapc/githubactions-lab'
Redirecting to blob download url: https://productionresultssa10.blob.core.windows.net/actions-results/751cc8ca-c811-458d-9fb6-fb2a0a957314/workflow-job-run-a66f81bc-92fb-527d-84b2-0d7efafd23ba/artifacts/3dcf0ac43e2ba1e4dda83cc9cad71ef1586214d36718776b88b6e1d93987cce0.zip
Starting download of artifact to: /home/runner/work/githubactions-lab/githubactions-lab
(node:2030) [DEP0005] DeprecationWarning: Buffer() is deprecated due to security and usability issues. Please use the Buffer.alloc(), Buffer.allocUnsafe(), or Buffer.from() methods instead.
(Use `node --trace-deprecation ...` to show where the warning was created)
SHA256 digest of downloaded artifact is 81f29b05074a1ed328654302ee20a68e087f13788371ad1ef3cdeb2ddf4c0972
Artifact download completed successfully.
Total of 1 artifact(s) downloaded
Download artifact has finished successfully

ワークフローを跨いでアーティファクトをダウンロードできるんですねー!

トラブルシュート

いくつか遭遇したトラブルとその対処法を紹介します。

run-idを指定したのにアーティファクトが見つからない

デフォルトのactions/download-artifactは、github-tokenが空のためrun-idだけ指定してもアーティファクトが見つからないと出ます。

例えば、次の設定だとgithub-tokenが無指定なのでアーティファクトが見つかりません。エラーログからはトークンがないことためと分からないのでハマりやすいです。ハマった。

# ❌
- uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0
  with:
    run-id: 1234
Found 0 artifact(s)
No input name, artifact-ids or pattern filtered specified, downloading all artifacts
An extra directory with the artifact name will be created for each download
Total of 0 artifact(s) downloaded
Download artifact has finished successfully

この場合、github-token${{ github.token }}やPATを指定しましょう。

# OK
- uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0
  with:
    run-id: 1234
    github-token: ${{ github.token }}

github-tokenとrun-idを指定したのにアーティファクトが見つからない

別のリポジトリの場合、ワークフローで自動発行されるトークン${{ github.token }}ではアクセスできません。

# ❌
- uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0
  with:
    run-id: 9876
    repository: some/other-repo
    github-token: ${{ github.token }}

この場合、PATかGitHub Appのインストールアクセストークンを使う必要があります。

# OK
- uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0
  with:
    run-id: 9876
    repository: some/other-repo
    github-token: ${{ secrets.YOUR_PAT }}

また、PATにはactions: read権限が必要です。

To elevate permissions for this scenario, you can specify a github-token along with other repository and run identifiers:

公式の案内でactions:read権限が必要と明記されている

権限に問題がないがアーティファクトが見つからない

actions/upload-artifactでアーティファクトをアップロードしたアーティファクトは、一定の期間だけ保持されます。デフォルトで特に指定なければ90日間ですが、ワークフローでretention-daysを指定している場合はその日数になります。

私はよくretention-days: 1にしているため、翌日にはアーティファクトが消えていて見つからなくなります。アーティファクトが見つからない場合は、対象のrun-idのアーティファクトがまだ存在しているか確認してください。

期限が切れている場合、次のようにExpiredと表示されます。

アーティファクトの期限が切れてExpiredと表示されている

まとめ

actions/download-artifact@v3までは同一ワークフローからしか取得できなかったので、てっきり今もかと考えていましたが、v4(2023年12月15日リリース)以降は他のワークフローやリポジトリのアーティファクトもダウンロードできます。

中にはGitHub APIを駆使して取得する例もありますが、アーティファクトをAPIで触るのは割と面倒です。actions/download-artifactを使えるシーンでは積極的に使っていきましょう。

参考

C#/NuGetにおけるSBOMとSLSAの状況

前回記事で、OSS開発者としてSBOMとSLSAの状況を見ました。 今回は、C#/NuGetでSBOMやSLSAはいい感じに使えるのかという点を見ていきます。

結論から言うと、NuGetでSBOMは対応可能ですが、SLSAは現状では機能しません。特にNuGetの署名の仕組みが障壁になっています。具体的に見ていきましょう。

NuGetのSBOM対応

SBOMで紹介したsbom-toolと同じリポジトリから提供されているMicrosoft.Sbom.TargetsNuGetパッケージを組み込んでSBOMを有効にすれば、dotnet packで自動的にsbomを.nupkgに埋め込みます。

dotnet add package Microsoft.Sbom.Targets

基本的な設定方法

参考までに、SkiaSharp.QrCodeでSBOM対応しています。

Microsoft.Sbom.Targetsは、内部的にはMicrosoft.SbomToolを用いてSPDXフォーマットのSBOMを生成します。NuGetパッケージ追加後、SBOM生成を有効にするにはcsproj<GenerateSBOM>true</GenerateSBOM>を設定します。おすすめ設定は次の通りですが、事実上<GenerateSBOM>true</GenerateSBOM>だけで十分です。

<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <TargetFramework>net10.0</TargetFramework>

    <!-- 👇 これを追加 -->
    <GenerateSBOM>true</GenerateSBOM>
    <!-- 👇 以下はバグで機能しない -->
    <SbomGenerationFetchLicenseInformation>true</SbomGenerationFetchLicenseInformation>
    <SbomGenerationEnablePackageMetadataParsing>true</SbomGenerationEnablePackageMetadataParsing>
    <!-- 👇 指定しなければ2.2なのでそれでもいい。3.0は時期尚早 -->
    <SbomGenerationManifestInfo>SPDX:2.2</SbomGenerationManifestInfo>
  </PropertyGroup>

  <ItemGroup>
    <PackageVersion Include="Microsoft.Sbom.Targets" Version="4.1.5" />
  </ItemGroup>
</Project>

他にも.csprojには様々なSBOM生成オプションを設定できます。

dotnet packでnuget用のパッケージを生成すると、.nupkgにSBOMが含まれます。デフォルトなら、_manifest/spdx_2.2/manifest.spdx.jsonにSBOMマニフェストが含まれているはずです1

$ dotnet pack -c Release
$ ls -l src/SkiaSharp.QrCode/bin/Release
total 592
-rw-rw-r--    1 guitarrapc guitarrapc    174157 Jan 20 00:23 SkiaSharp.QrCode.1.0.0.nupkg
-rw-rw-r--    1 guitarrapc guitarrapc    123261 Jan 18 19:07 SkiaSharp.QrCode.1.0.0.snupkg
drwxrwxr-x    2 guitarrapc guitarrapc         0 Jan 18 19:07 net10.0
drwxrwxr-x    2 guitarrapc guitarrapc         0 Jan 18 19:07 net8.0
drwxrwxr-x    2 guitarrapc guitarrapc         0 Jan 18 19:07 netstandard2.0
drwxrwxr-x    2 guitarrapc guitarrapc         0 Jan 18 19:07 netstandard2.1

$ unzip -l src/SkiaSharp.QrCode/bin/Release/SkiaSharp.QrCode.1.0.0.nupkg |   Length      Date    Time    Name
---------  ---------- -----   ----
    33143  15/01/2026 08:33   README.md
     2212  18/01/2026 19:07   SkiaSharp.QrCode.nuspec
      527  18/01/2026 19:07   [Content_Types].xml
      510  18/01/2026 19:07   _rels/.rels
    85504  18/01/2026 10:07   lib/net10.0/SkiaSharp.QrCode.dll
    86016  18/01/2026 10:07   lib/net8.0/SkiaSharp.QrCode.dll
    91648  18/01/2026 10:07   lib/netstandard2.0/SkiaSharp.QrCode.dll
    87552  18/01/2026 10:07   lib/netstandard2.1/SkiaSharp.QrCode.dll
    19578  20/01/2026 00:23   _manifest/spdx_2.2/manifest.spdx.json
       64  20/01/2026 00:23   _manifest/spdx_2.2/manifest.spdx.json.sha256
      694  18/01/2026 19:07   package/services/metadata/core-properties/4056208419134658ab90d43eda3a51e4.psmdcp
---------                     -------
   407448                     11 files

中身は次のようになっています。一部要素を省略していますが、全文は折りたたんでおくので、必要があれば展開してください。

SBOMの中身全文(クリックで展開)

{
  "files": [
    {
      "fileName": "./lib/net8.0/SkiaSharp.QrCode.dll",
      "SPDXID": "SPDXRef-File--lib-net8.0-SkiaSharp.QrCode.dll-A1F20BD93B1A73FE0D4F34E57FE5E83CF16E748C",
      "checksums": [
        {
          "algorithm": "SHA256",
          "checksumValue": "64884baa46df69ae1eed437e8a47e271399ffe215dad7577eb7106f0a429010f"
        },
        {
          "algorithm": "SHA1",
          "checksumValue": "a1f20bd93b1a73fe0d4f34e57fe5e83cf16e748c"
        }
      ],
      "licenseConcluded": "NOASSERTION",
      "licenseInfoInFiles": [
        "NOASSERTION"
      ],
      "copyrightText": "NOASSERTION"
    },
    {
      "fileName": "./lib/netstandard2.1/SkiaSharp.QrCode.dll",
      "SPDXID": "SPDXRef-File--lib-netstandard2.1-SkiaSharp.QrCode.dll-0442B620957CC8FE998F26BD186AF7DBB6478C4A",
      "checksums": [
        {
          "algorithm": "SHA256",
          "checksumValue": "26c0489039142d6afd6fc6d65361f1d4b2160762571ed4f9283c311d02776069"
        },
        {
          "algorithm": "SHA1",
          "checksumValue": "0442b620957cc8fe998f26bd186af7dbb6478c4a"
        }
      ],
      "licenseConcluded": "NOASSERTION",
      "licenseInfoInFiles": [
        "NOASSERTION"
      ],
      "copyrightText": "NOASSERTION"
    },
    {
      "fileName": "./SkiaSharp.QrCode.nuspec",
      "SPDXID": "SPDXRef-File--SkiaSharp.QrCode.nuspec-E7ABF4725060CDB3E4F6D0F981A6D650D559A7A7",
      "checksums": [
        {
          "algorithm": "SHA256",
          "checksumValue": "7b6eb335fcda9fed0857249bad273623f420cdaa14fb888a215275663544b8f6"
        },
        {
          "algorithm": "SHA1",
          "checksumValue": "e7abf4725060cdb3e4f6d0f981a6d650d559a7a7"
        }
      ],
      "licenseConcluded": "NOASSERTION",
      "licenseInfoInFiles": [
        "NOASSERTION"
      ],
      "copyrightText": "NOASSERTION"
    },
    {
      "fileName": "./lib/netstandard2.0/SkiaSharp.QrCode.dll",
      "SPDXID": "SPDXRef-File--lib-netstandard2.0-SkiaSharp.QrCode.dll-C1454A9087D55424B935EF97EA7128FBE6144C36",
      "checksums": [
        {
          "algorithm": "SHA256",
          "checksumValue": "5906d89879bdc064f05f6d8bcfbc1fe71ad09c7e063f96cef46d442aa8c3eeea"
        },
        {
          "algorithm": "SHA1",
          "checksumValue": "c1454a9087d55424b935ef97ea7128fbe6144c36"
        }
      ],
      "licenseConcluded": "NOASSERTION",
      "licenseInfoInFiles": [
        "NOASSERTION"
      ],
      "copyrightText": "NOASSERTION"
    },
    {
      "fileName": "./_rels/.rels",
      "SPDXID": "SPDXRef-File---rels-.rels-9D0981B47DA2D0B8488D17207B3F4CC3F2C99E91",
      "checksums": [
        {
          "algorithm": "SHA256",
          "checksumValue": "a6a9610719dbe9bd2ef2283df0e4c54e71b65003c5a0a2940d54936dc99c78d9"
        },
        {
          "algorithm": "SHA1",
          "checksumValue": "9d0981b47da2d0b8488d17207b3f4cc3f2c99e91"
        }
      ],
      "licenseConcluded": "NOASSERTION",
      "licenseInfoInFiles": [
        "NOASSERTION"
      ],
      "copyrightText": "NOASSERTION"
    },
    {
      "fileName": "./README.md",
      "SPDXID": "SPDXRef-File--README.md-41D1013908E6838C84BB5B113F0BAAEE042E1346",
      "checksums": [
        {
          "algorithm": "SHA256",
          "checksumValue": "de4b28cb1a9f33a300652715220e5eee98796cae1aef6447f01d286664307bf9"
        },
        {
          "algorithm": "SHA1",
          "checksumValue": "41d1013908e6838c84bb5b113f0baaee042e1346"
        }
      ],
      "licenseConcluded": "NOASSERTION",
      "licenseInfoInFiles": [
        "NOASSERTION"
      ],
      "copyrightText": "NOASSERTION"
    },
    {
      "fileName": "./lib/net10.0/SkiaSharp.QrCode.dll",
      "SPDXID": "SPDXRef-File--lib-net10.0-SkiaSharp.QrCode.dll-99204E5B36AA5D3E01AB797497A43D3DE479B2A6",
      "checksums": [
        {
          "algorithm": "SHA256",
          "checksumValue": "d05d2cc4445c0d521993ccede87b1bacf3998defc3a2f24133895cdac132a890"
        },
        {
          "algorithm": "SHA1",
          "checksumValue": "99204e5b36aa5d3e01ab797497a43d3de479b2a6"
        }
      ],
      "licenseConcluded": "NOASSERTION",
      "licenseInfoInFiles": [
        "NOASSERTION"
      ],
      "copyrightText": "NOASSERTION"
    },
    {
      "fileName": "./package/services/metadata/core-properties/c74095c9b98b496db5bfeb7f0ec286d6.psmdcp",
      "SPDXID": "SPDXRef-File--package-services-metadata-core-properties-c74095c9b98b496db5bfeb7f0ec286d6.psmdcp-F931762800D5440480186EB37E0469B74BF2C8AF",
      "checksums": [
        {
          "algorithm": "SHA256",
          "checksumValue": "0f530c34dffec3ae38de28b1549202ac49e08ff22e9ad4b6a6c83c350c8e8ca9"
        },
        {
          "algorithm": "SHA1",
          "checksumValue": "f931762800d5440480186eb37e0469b74bf2c8af"
        }
      ],
      "licenseConcluded": "NOASSERTION",
      "licenseInfoInFiles": [
        "NOASSERTION"
      ],
      "copyrightText": "NOASSERTION"
    },
    {
      "fileName": "./[Content_Types].xml",
      "SPDXID": "SPDXRef-File---Content-Types-.xml-231C3DFEA27B519CD97D24A63F7B8C8B99AD464E",
      "checksums": [
        {
          "algorithm": "SHA256",
          "checksumValue": "778a7d54d9a1c7caf79ab7002592349d933aae8edf8385f1fc56178d7a069143"
        },
        {
          "algorithm": "SHA1",
          "checksumValue": "231c3dfea27b519cd97d24a63f7b8c8b99ad464e"
        }
      ],
      "licenseConcluded": "NOASSERTION",
      "licenseInfoInFiles": [
        "NOASSERTION"
      ],
      "copyrightText": "NOASSERTION"
    }
  ],
  "packages": [
    {
      "name": "SkiaSharp.QrCode",
      "SPDXID": "SPDXRef-Package-E36A5F0F0B31C3EA820F73BD5612ECBB45B859A2CFBC28719CDA2F76CC599F45",
      "downloadLocation": "NOASSERTION",
      "filesAnalyzed": false,
      "licenseConcluded": "NOASSERTION",
      "licenseDeclared": "NOASSERTION",
      "copyrightText": "NOASSERTION",
      "versionInfo": "1.0.0",
      "externalRefs": [
        {
          "referenceCategory": "PACKAGE-MANAGER",
          "referenceType": "purl",
          "referenceLocator": "pkg:nuget/SkiaSharp.QrCode@1.0.0"
        }
      ],
      "supplier": "Organization: guitarrapc"
    },
    {
      "name": "Microsoft.NETCore.Platforms",
      "SPDXID": "SPDXRef-Package-846C7B671CE0E884005EF626B209AD4D24EBA1FF032B6BA0242D62EC1793AA97",
      "downloadLocation": "NOASSERTION",
      "filesAnalyzed": false,
      "licenseConcluded": "NOASSERTION",
      "licenseDeclared": "NOASSERTION",
      "copyrightText": "NOASSERTION",
      "versionInfo": "1.1.0",
      "externalRefs": [
        {
          "referenceCategory": "PACKAGE-MANAGER",
          "referenceType": "purl",
          "referenceLocator": "pkg:nuget/Microsoft.NETCore.Platforms@1.1.0"
        }
      ],
      "supplier": "NOASSERTION"
    },
    {
      "name": "PolySharp",
      "SPDXID": "SPDXRef-Package-E27E4A39B1271EC6E0119F8F3C1165DDB0F5493D9EB9DBC79586516D68EF6D27",
      "downloadLocation": "NOASSERTION",
      "filesAnalyzed": false,
      "licenseConcluded": "NOASSERTION",
      "licenseDeclared": "NOASSERTION",
      "copyrightText": "NOASSERTION",
      "versionInfo": "1.15.0",
      "externalRefs": [
        {
          "referenceCategory": "PACKAGE-MANAGER",
          "referenceType": "purl",
          "referenceLocator": "pkg:nuget/PolySharp@1.15.0"
        }
      ],
      "supplier": "NOASSERTION"
    },
    {
      "name": "System.Runtime.CompilerServices.Unsafe",
      "SPDXID": "SPDXRef-Package-C924E25709BAC7772763FD535719B02BFD9AE676EB73B3DA4C7058E9A501DC30",
      "downloadLocation": "NOASSERTION",
      "filesAnalyzed": false,
      "licenseConcluded": "NOASSERTION",
      "licenseDeclared": "NOASSERTION",
      "copyrightText": "NOASSERTION",
      "versionInfo": "4.5.3",
      "externalRefs": [
        {
          "referenceCategory": "PACKAGE-MANAGER",
          "referenceType": "purl",
          "referenceLocator": "pkg:nuget/System.Runtime.CompilerServices.Unsafe@4.5.3"
        }
      ],
      "supplier": "NOASSERTION"
    },
    {
      "name": "System.Numerics.Vectors",
      "SPDXID": "SPDXRef-Package-A3FB7F39F2BD82992BF718EC7C174635F4F24789BA41C19787B833205839923D",
      "downloadLocation": "NOASSERTION",
      "filesAnalyzed": false,
      "licenseConcluded": "NOASSERTION",
      "licenseDeclared": "NOASSERTION",
      "copyrightText": "NOASSERTION",
      "versionInfo": "4.4.0",
      "externalRefs": [
        {
          "referenceCategory": "PACKAGE-MANAGER",
          "referenceType": "purl",
          "referenceLocator": "pkg:nuget/System.Numerics.Vectors@4.4.0"
        }
      ],
      "supplier": "NOASSERTION"
    },
    {
      "name": "Microsoft.Sbom.Targets",
      "SPDXID": "SPDXRef-Package-187EFE2F1CB79DC28F70C62C1D8E0B6D840AF97A56D8D381EC4C4E7A968129CB",
      "downloadLocation": "NOASSERTION",
      "filesAnalyzed": false,
      "licenseConcluded": "NOASSERTION",
      "licenseDeclared": "NOASSERTION",
      "copyrightText": "NOASSERTION",
      "versionInfo": "4.1.5",
      "externalRefs": [
        {
          "referenceCategory": "PACKAGE-MANAGER",
          "referenceType": "purl",
          "referenceLocator": "pkg:nuget/Microsoft.Sbom.Targets@4.1.5"
        }
      ],
      "supplier": "NOASSERTION"
    },
    {
      "name": "System.Memory",
      "SPDXID": "SPDXRef-Package-A2645140C49BA822DFA302147B90231A2DD8246C825B2E5561535F2BFFD43192",
      "downloadLocation": "NOASSERTION",
      "filesAnalyzed": false,
      "licenseConcluded": "NOASSERTION",
      "licenseDeclared": "NOASSERTION",
      "copyrightText": "NOASSERTION",
      "versionInfo": "4.5.5",
      "externalRefs": [
        {
          "referenceCategory": "PACKAGE-MANAGER",
          "referenceType": "purl",
          "referenceLocator": "pkg:nuget/System.Memory@4.5.5"
        }
      ],
      "supplier": "NOASSERTION"
    },
    {
      "name": "SkiaSharp.NativeAssets.Win32",
      "SPDXID": "SPDXRef-Package-2B4D1AA306B3CA8ABA7A05C4A7C904C5E5001C8925991DA65BCC1D72239E7257",
      "downloadLocation": "NOASSERTION",
      "filesAnalyzed": false,
      "licenseConcluded": "NOASSERTION",
      "licenseDeclared": "NOASSERTION",
      "copyrightText": "NOASSERTION",
      "versionInfo": "3.119.1",
      "externalRefs": [
        {
          "referenceCategory": "PACKAGE-MANAGER",
          "referenceType": "purl",
          "referenceLocator": "pkg:nuget/SkiaSharp.NativeAssets.Win32@3.119.1"
        }
      ],
      "supplier": "NOASSERTION"
    },
    {
      "name": "SkiaSharp",
      "SPDXID": "SPDXRef-Package-B4E80DA3E23BEEE3B1AAC22F912F2F4FF044AC3CF7DB457719854D84E12E9CB6",
      "downloadLocation": "NOASSERTION",
      "filesAnalyzed": false,
      "licenseConcluded": "NOASSERTION",
      "licenseDeclared": "NOASSERTION",
      "copyrightText": "NOASSERTION",
      "versionInfo": "3.119.1",
      "externalRefs": [
        {
          "referenceCategory": "PACKAGE-MANAGER",
          "referenceType": "purl",
          "referenceLocator": "pkg:nuget/SkiaSharp@3.119.1"
        }
      ],
      "supplier": "NOASSERTION"
    },
    {
      "name": "System.Buffers",
      "SPDXID": "SPDXRef-Package-CB3E43ED2FAAF926BACC8A3E5A5219246BD37049F5C72FD776C3144C1DBAB0A3",
      "downloadLocation": "NOASSERTION",
      "filesAnalyzed": false,
      "licenseConcluded": "NOASSERTION",
      "licenseDeclared": "NOASSERTION",
      "copyrightText": "NOASSERTION",
      "versionInfo": "4.5.1",
      "externalRefs": [
        {
          "referenceCategory": "PACKAGE-MANAGER",
          "referenceType": "purl",
          "referenceLocator": "pkg:nuget/System.Buffers@4.5.1"
        }
      ],
      "supplier": "NOASSERTION"
    },
    {
      "name": "NETStandard.Library",
      "SPDXID": "SPDXRef-Package-7FB8593FF8500851C8134780E1D4FFFA51D20C3770D3B4BBE0CE4C31BA6A9CE0",
      "downloadLocation": "NOASSERTION",
      "filesAnalyzed": false,
      "licenseConcluded": "NOASSERTION",
      "licenseDeclared": "NOASSERTION",
      "copyrightText": "NOASSERTION",
      "versionInfo": "2.0.3",
      "externalRefs": [
        {
          "referenceCategory": "PACKAGE-MANAGER",
          "referenceType": "purl",
          "referenceLocator": "pkg:nuget/NETStandard.Library@2.0.3"
        }
      ],
      "supplier": "NOASSERTION"
    },
    {
      "name": "SkiaSharp.NativeAssets.macOS",
      "SPDXID": "SPDXRef-Package-7337C35C8B5D91C470B5BE65044F09AEEB0C686397B7DDD632D96214A8BB0E77",
      "downloadLocation": "NOASSERTION",
      "filesAnalyzed": false,
      "licenseConcluded": "NOASSERTION",
      "licenseDeclared": "NOASSERTION",
      "copyrightText": "NOASSERTION",
      "versionInfo": "3.119.1",
      "externalRefs": [
        {
          "referenceCategory": "PACKAGE-MANAGER",
          "referenceType": "purl",
          "referenceLocator": "pkg:nuget/SkiaSharp.NativeAssets.macOS@3.119.1"
        }
      ],
      "supplier": "NOASSERTION"
    },
    {
      "name": "SkiaSharp.QrCode",
      "SPDXID": "SPDXRef-RootPackage",
      "downloadLocation": "NOASSERTION",
      "packageVerificationCode": {
        "packageVerificationCodeValue": "7d8ab491469685618111f6c2a9d6a3cf51efcc6a"
      },
      "filesAnalyzed": true,
      "licenseConcluded": "NOASSERTION",
      "licenseInfoFromFiles": [
        "NOASSERTION"
      ],
      "licenseDeclared": "NOASSERTION",
      "copyrightText": "NOASSERTION",
      "versionInfo": "1.0.0",
      "externalRefs": [
        {
          "referenceCategory": "PACKAGE-MANAGER",
          "referenceType": "purl",
          "referenceLocator": "pkg:swid/guitarrapc/spdx.org/SkiaSharp.QrCode@1.0.0?tag_id=bae17d65-a7f6-4eee-b220-1671b3afb182"
        }
      ],
      "supplier": "Organization: guitarrapc",
      "hasFiles": [
        "SPDXRef-File--lib-net10.0-SkiaSharp.QrCode.dll-99204E5B36AA5D3E01AB797497A43D3DE479B2A6",
        "SPDXRef-File---rels-.rels-9D0981B47DA2D0B8488D17207B3F4CC3F2C99E91",
        "SPDXRef-File--SkiaSharp.QrCode.nuspec-E7ABF4725060CDB3E4F6D0F981A6D650D559A7A7",
        "SPDXRef-File--lib-net8.0-SkiaSharp.QrCode.dll-A1F20BD93B1A73FE0D4F34E57FE5E83CF16E748C",
        "SPDXRef-File---Content-Types-.xml-231C3DFEA27B519CD97D24A63F7B8C8B99AD464E",
        "SPDXRef-File--README.md-41D1013908E6838C84BB5B113F0BAAEE042E1346",
        "SPDXRef-File--lib-netstandard2.1-SkiaSharp.QrCode.dll-0442B620957CC8FE998F26BD186AF7DBB6478C4A",
        "SPDXRef-File--package-services-metadata-core-properties-c74095c9b98b496db5bfeb7f0ec286d6.psmdcp-F931762800D5440480186EB37E0469B74BF2C8AF",
        "SPDXRef-File--lib-netstandard2.0-SkiaSharp.QrCode.dll-C1454A9087D55424B935EF97EA7128FBE6144C36"
      ]
    }
  ],
  "externalDocumentRefs": [],
  "relationships": [
    {
      "relationshipType": "DEPENDS_ON",
      "relatedSpdxElement": "SPDXRef-Package-2B4D1AA306B3CA8ABA7A05C4A7C904C5E5001C8925991DA65BCC1D72239E7257",
      "spdxElementId": "SPDXRef-Package-B4E80DA3E23BEEE3B1AAC22F912F2F4FF044AC3CF7DB457719854D84E12E9CB6"
    },
    {
      "relationshipType": "DESCRIBES",
      "relatedSpdxElement": "SPDXRef-RootPackage",
      "spdxElementId": "SPDXRef-DOCUMENT"
    },
    {
      "relationshipType": "DEPENDS_ON",
      "relatedSpdxElement": "SPDXRef-Package-7337C35C8B5D91C470B5BE65044F09AEEB0C686397B7DDD632D96214A8BB0E77",
      "spdxElementId": "SPDXRef-Package-B4E80DA3E23BEEE3B1AAC22F912F2F4FF044AC3CF7DB457719854D84E12E9CB6"
    },
    {
      "relationshipType": "DEPENDS_ON",
      "relatedSpdxElement": "SPDXRef-Package-A2645140C49BA822DFA302147B90231A2DD8246C825B2E5561535F2BFFD43192",
      "spdxElementId": "SPDXRef-Package-B4E80DA3E23BEEE3B1AAC22F912F2F4FF044AC3CF7DB457719854D84E12E9CB6"
    },
    {
      "relationshipType": "DEPENDS_ON",
      "relatedSpdxElement": "SPDXRef-Package-7FB8593FF8500851C8134780E1D4FFFA51D20C3770D3B4BBE0CE4C31BA6A9CE0",
      "spdxElementId": "SPDXRef-RootPackage"
    },
    {
      "relationshipType": "DEPENDS_ON",
      "relatedSpdxElement": "SPDXRef-Package-187EFE2F1CB79DC28F70C62C1D8E0B6D840AF97A56D8D381EC4C4E7A968129CB",
      "spdxElementId": "SPDXRef-RootPackage"
    },
    {
      "relationshipType": "DEPENDS_ON",
      "relatedSpdxElement": "SPDXRef-Package-A3FB7F39F2BD82992BF718EC7C174635F4F24789BA41C19787B833205839923D",
      "spdxElementId": "SPDXRef-Package-B4E80DA3E23BEEE3B1AAC22F912F2F4FF044AC3CF7DB457719854D84E12E9CB6"
    },
    {
      "relationshipType": "DEPENDS_ON",
      "relatedSpdxElement": "SPDXRef-Package-A3FB7F39F2BD82992BF718EC7C174635F4F24789BA41C19787B833205839923D",
      "spdxElementId": "SPDXRef-Package-A2645140C49BA822DFA302147B90231A2DD8246C825B2E5561535F2BFFD43192"
    },
    {
      "relationshipType": "DEPENDS_ON",
      "relatedSpdxElement": "SPDXRef-Package-C924E25709BAC7772763FD535719B02BFD9AE676EB73B3DA4C7058E9A501DC30",
      "spdxElementId": "SPDXRef-Package-B4E80DA3E23BEEE3B1AAC22F912F2F4FF044AC3CF7DB457719854D84E12E9CB6"
    },
    {
      "relationshipType": "DEPENDS_ON",
      "relatedSpdxElement": "SPDXRef-Package-C924E25709BAC7772763FD535719B02BFD9AE676EB73B3DA4C7058E9A501DC30",
      "spdxElementId": "SPDXRef-Package-A2645140C49BA822DFA302147B90231A2DD8246C825B2E5561535F2BFFD43192"
    },
    {
      "relationshipType": "DEPENDS_ON",
      "relatedSpdxElement": "SPDXRef-Package-E27E4A39B1271EC6E0119F8F3C1165DDB0F5493D9EB9DBC79586516D68EF6D27",
      "spdxElementId": "SPDXRef-RootPackage"
    },
    {
      "relationshipType": "DEPENDS_ON",
      "relatedSpdxElement": "SPDXRef-Package-B4E80DA3E23BEEE3B1AAC22F912F2F4FF044AC3CF7DB457719854D84E12E9CB6",
      "spdxElementId": "SPDXRef-RootPackage"
    },
    {
      "relationshipType": "DEPENDS_ON",
      "relatedSpdxElement": "SPDXRef-Package-846C7B671CE0E884005EF626B209AD4D24EBA1FF032B6BA0242D62EC1793AA97",
      "spdxElementId": "SPDXRef-Package-7FB8593FF8500851C8134780E1D4FFFA51D20C3770D3B4BBE0CE4C31BA6A9CE0"
    },
    {
      "relationshipType": "DEPENDS_ON",
      "relatedSpdxElement": "SPDXRef-Package-CB3E43ED2FAAF926BACC8A3E5A5219246BD37049F5C72FD776C3144C1DBAB0A3",
      "spdxElementId": "SPDXRef-Package-B4E80DA3E23BEEE3B1AAC22F912F2F4FF044AC3CF7DB457719854D84E12E9CB6"
    },
    {
      "relationshipType": "DEPENDS_ON",
      "relatedSpdxElement": "SPDXRef-Package-CB3E43ED2FAAF926BACC8A3E5A5219246BD37049F5C72FD776C3144C1DBAB0A3",
      "spdxElementId": "SPDXRef-Package-A2645140C49BA822DFA302147B90231A2DD8246C825B2E5561535F2BFFD43192"
    },
    {
      "relationshipType": "DEPENDS_ON",
      "relatedSpdxElement": "SPDXRef-Package-E36A5F0F0B31C3EA820F73BD5612ECBB45B859A2CFBC28719CDA2F76CC599F45",
      "spdxElementId": "SPDXRef-RootPackage"
    }
  ],
  "spdxVersion": "SPDX-2.2",
  "dataLicense": "CC0-1.0",
  "SPDXID": "SPDXRef-DOCUMENT",
  "name": "SkiaSharp.QrCode 1.0.0",
  "documentNamespace": "http://spdx.org/spdxdocs/SkiaSharp.QrCode/SkiaSharp.QrCode/1.0.0/JuLWYucADEOK3MtH3Acl-Q",
  "creationInfo": {
    "created": "2026-01-15T09:03:53Z",
    "creators": [
      "Organization: guitarrapc",
      "Tool: Microsoft.SBOMTool-4.1.5"
    ]
  },
  "documentDescribes": [
    "SPDXRef-RootPackage"
  ]
}

{
  "files": [
    {
      "fileName": "./lib/net8.0/SkiaSharp.QrCode.dll",
      "SPDXID": "SPDXRef-File--lib-net8.0-SkiaSharp.QrCode.dll-A1F20BD93B1A73FE0D4F34E57FE5E83CF16E748C",
      "checksums": [
        {
          "algorithm": "SHA256",
          "checksumValue": "64884baa46df69ae1eed437e8a47e271399ffe215dad7577eb7106f0a429010f"
        },
        {
          "algorithm": "SHA1",
          "checksumValue": "a1f20bd93b1a73fe0d4f34e57fe5e83cf16e748c"
        }
      ],
      "licenseConcluded": "NOASSERTION",
      "licenseInfoInFiles": [
        "NOASSERTION"
      ],
      "copyrightText": "NOASSERTION"
    },
    {
      "fileName": "./lib/netstandard2.1/SkiaSharp.QrCode.dll",
      "SPDXID": "SPDXRef-File--lib-netstandard2.1-SkiaSharp.QrCode.dll-0442B620957CC8FE998F26BD186AF7DBB6478C4A",
      "checksums": [
        {
          "algorithm": "SHA256",
          "checksumValue": "26c0489039142d6afd6fc6d65361f1d4b2160762571ed4f9283c311d02776069"
        },
        {
          "algorithm": "SHA1",
          "checksumValue": "0442b620957cc8fe998f26bd186af7dbb6478c4a"
        }
      ],
      "licenseConcluded": "NOASSERTION",
      "licenseInfoInFiles": [
        "NOASSERTION"
      ],
      "copyrightText": "NOASSERTION"
    },
    // ... 省略
  ],
  "packages": [
    {
      "name": "SkiaSharp.QrCode",
      "SPDXID": "SPDXRef-Package-E36A5F0F0B31C3EA820F73BD5612ECBB45B859A2CFBC28719CDA2F76CC599F45",
      "downloadLocation": "NOASSERTION",
      "filesAnalyzed": false,
      "licenseConcluded": "NOASSERTION",
      "licenseDeclared": "NOASSERTION",
      "copyrightText": "NOASSERTION",
      "versionInfo": "1.0.0",
      "externalRefs": [
        {
          "referenceCategory": "PACKAGE-MANAGER",
          "referenceType": "purl",
          "referenceLocator": "pkg:nuget/SkiaSharp.QrCode@1.0.0"
        }
      ],
      "supplier": "Organization: guitarrapc"
    },
    // ... 省略
    {
      "name": "SkiaSharp",
      "SPDXID": "SPDXRef-Package-B4E80DA3E23BEEE3B1AAC22F912F2F4FF044AC3CF7DB457719854D84E12E9CB6",
      "downloadLocation": "NOASSERTION",
      "filesAnalyzed": false,
      "licenseConcluded": "NOASSERTION",
      "licenseDeclared": "NOASSERTION",
      "copyrightText": "NOASSERTION",
      "versionInfo": "3.119.1",
      "externalRefs": [
        {
          "referenceCategory": "PACKAGE-MANAGER",
          "referenceType": "purl",
          "referenceLocator": "pkg:nuget/SkiaSharp@3.119.1"
        }
      ],
      "supplier": "NOASSERTION"
    },
    // ... 省略
    {
      "name": "SkiaSharp.QrCode",
      "SPDXID": "SPDXRef-RootPackage",
      "downloadLocation": "NOASSERTION",
      "packageVerificationCode": {
        "packageVerificationCodeValue": "7d8ab491469685618111f6c2a9d6a3cf51efcc6a"
      },
      "filesAnalyzed": true,
      "licenseConcluded": "NOASSERTION",
      "licenseInfoFromFiles": [
        "NOASSERTION"
      ],
      "licenseDeclared": "NOASSERTION",
      "copyrightText": "NOASSERTION",
      "versionInfo": "1.0.0",
      "externalRefs": [
        {
          "referenceCategory": "PACKAGE-MANAGER",
          "referenceType": "purl",
          "referenceLocator": "pkg:swid/guitarrapc/spdx.org/SkiaSharp.QrCode@1.0.0?tag_id=bae17d65-a7f6-4eee-b220-1671b3afb182"
        }
      ],
      "supplier": "Organization: guitarrapc",
      "hasFiles": [
        "SPDXRef-File--lib-net10.0-SkiaSharp.QrCode.dll-99204E5B36AA5D3E01AB797497A43D3DE479B2A6",
        "SPDXRef-File---rels-.rels-9D0981B47DA2D0B8488D17207B3F4CC3F2C99E91",
        "SPDXRef-File--SkiaSharp.QrCode.nuspec-E7ABF4725060CDB3E4F6D0F981A6D650D559A7A7",
        "SPDXRef-File--lib-net8.0-SkiaSharp.QrCode.dll-A1F20BD93B1A73FE0D4F34E57FE5E83CF16E748C",
        "SPDXRef-File---Content-Types-.xml-231C3DFEA27B519CD97D24A63F7B8C8B99AD464E",
        "SPDXRef-File--README.md-41D1013908E6838C84BB5B113F0BAAEE042E1346",
        "SPDXRef-File--lib-netstandard2.1-SkiaSharp.QrCode.dll-0442B620957CC8FE998F26BD186AF7DBB6478C4A",
        "SPDXRef-File--package-services-metadata-core-properties-c74095c9b98b496db5bfeb7f0ec286d6.psmdcp-F931762800D5440480186EB37E0469B74BF2C8AF",
        "SPDXRef-File--lib-netstandard2.0-SkiaSharp.QrCode.dll-C1454A9087D55424B935EF97EA7128FBE6144C36"
      ]
    }
  ],
  "externalDocumentRefs": [],
  "relationships": [
    {
      "relationshipType": "DEPENDS_ON",
      "relatedSpdxElement": "SPDXRef-Package-2B4D1AA306B3CA8ABA7A05C4A7C904C5E5001C8925991DA65BCC1D72239E7257",
      "spdxElementId": "SPDXRef-Package-B4E80DA3E23BEEE3B1AAC22F912F2F4FF044AC3CF7DB457719854D84E12E9CB6"
    },
    {
      "relationshipType": "DESCRIBES",
      "relatedSpdxElement": "SPDXRef-RootPackage",
      "spdxElementId": "SPDXRef-DOCUMENT"
    },
    // ... 省略
  ],
  "spdxVersion": "SPDX-2.2",
  "dataLicense": "CC0-1.0",
  "SPDXID": "SPDXRef-DOCUMENT",
  "name": "SkiaSharp.QrCode 1.0.0",
  "documentNamespace": "http://spdx.org/spdxdocs/SkiaSharp.QrCode/SkiaSharp.QrCode/1.0.0/JuLWYucADEOK3MtH3Acl-Q",
  "creationInfo": {
    "created": "2026-01-15T09:03:53Z",
    "creators": [
      "Organization: guitarrapc",
      "Tool: Microsoft.SBOMTool-4.1.5"
    ]
  },
  "documentDescribes": [
    "SPDXRef-RootPackage"
  ]
}

既知の問題

SBOM生成してみて分かっている問題をまとめておきます。

Microsoft.Sbom.Targetsは外部コントリビュータを受け付けない

Microsoft.Sbom.Targetsは、セキュリティ的な理由から、外部コントリビュータを受け付けていないことが明言されています。

This project does not accept open-source contributions due to the sensitive, regulatory nature of SBOMs. If you are external to Microsoft and need modifications to the tool, you are welcome to fork and maintain a version of the tool.

Microsoft.Sbom.Targetsでライセンス情報を含められない

Microsoft.Sbom.Targetsは4.1.5時点では、NuGetパッケージのライセンス情報をSBOMに含めることができません。

本来、<SbomGenerationFetchLicenseInformation>SbomGenerationEnablePackageMetadataParsingtrueに設定すると、NuGetパッケージのライセンス情報をSBOMに含めることができます。ただ、現在この2つのオプションはパラメーターを渡し忘れているようでうまく動作していません。修正PRを作ったものの、前述の通り外部コントリビュータを受け付けないため、リポジトリオーナーのIssue修正対応待ちです。

ちなみにCLIであるsbom-toolではライセンス情報が出るので、最悪CLIで生成してdotnet packで含まれるようにするのも手です。sbom-toolをインストールして実行してみましょう。

いくつかの方法でインストールできるので、好きな方法を使ってください。

# Homebrew
$ brew install sbom-tool

# winget
$ winget install Microsoft.SbomTool

# dotnet tool
$ dotnet tool install -g Microsoft.Sbom.Tool

dotnetビルドしてから、そのパスに対して実行します。-li trueでライセンス情報を含め、-pm trueでパッケージメタデータを解析します。これでライセンス情報を含むSBOMが生成されます。

$ sbom-tool generate -b .\src\SkiaSharp.QrCode\bin\Release\net10.0 -bc . -pn SkiaSharp.QrCode -pv 0.13.0 -ps guitarrapc -li true -pm true
                                                   Detection Summary
┌─────────────────────────────┬─────────────────────────────┬─────────────────────────────┬────────────────────────────┐
│ Component Detector Id       │ Detection Time              │ # Components Found          │ # Explicitly Referenced    │
├─────────────────────────────┼─────────────────────────────┼─────────────────────────────┼────────────────────────────┤
│ CocoaPods                   │ 0.067 seconds               │ 0                           │ 0                          │
│ ConanLock                   │ 0.047 seconds               │ 0                           │ 0                          │
│ DotNet                      │ 1.1 seconds                 │ 11                          │ 0                          │
│ Go                          │ 0.066 seconds               │ 0                           │ 0                          │
│ Gradle                      │ 0.066 seconds               │ 0                           │ 0                          │
│ Ivy (Beta)                  │ 0.065 seconds               │ 0                           │ 0                          │
│ Linux                       │ 0.0019 seconds              │ 0                           │ 0                          │
│ LinuxApplicationLayer       │ 0.0019 seconds              │ 0                           │ 0                          │
│ (Beta)                      │                             │                             │                            │
│ MvnCli                      │ 0.046 seconds               │ 0                           │ 0                          │
│ Npm                         │ 0.047 seconds               │ 0                           │ 0                          │
│ NpmLockfile3                │ 0.047 seconds               │ 0                           │ 0                          │
│ NpmWithRoots                │ 0.047 seconds               │ 0                           │ 0                          │
│ NuGet                       │ 0.052 seconds               │ 1                           │ 0                          │
│ NuGetPackagesConfig         │ 0.047 seconds               │ 0                           │ 0                          │
│ NuGetProjectCentric         │ 0.41 seconds                │ 143                         │ 37                         │
│ PipReport                   │ 0.77 seconds                │ 0                           │ 0                          │
│ Pnpm                        │ 0.047 seconds               │ 0                           │ 0                          │
│ Poetry (Beta)               │ 0.047 seconds               │ 0                           │ 0                          │
│ Ruby                        │ 0.047 seconds               │ 0                           │ 0                          │
│ RustSbom                    │ 0.048 seconds               │ 0                           │ 0                          │
│ SPDX22SBOM                  │ 0.046 seconds               │ 0                           │ 0                          │
│ UvLock (Beta)               │ 0.047 seconds               │ 0                           │ 0                          │
│ Vcpkg                       │ 0.047 seconds               │ 0                           │ 0                          │
│ Yarn                        │ 0.047 seconds               │ 0                           │ 0                          │
│ ─────────────────────────── │ ─────────────────────────── │ ─────────────────────────── │ ────────────────────────── │
│ Total                       │ 1.1 seconds                 │ 155                         │ 37                         │
└─────────────────────────────┴─────────────────────────────┴─────────────────────────────┴────────────────────────────┘

$ ls -l ./src/SkiaSharp.QrCode/bin/Release/net10.0/_manifest/spdx_2.2
total 200
-rw-rw-r--    1 guitarrapc guitarrapc    199815 Jan 20 00:45 manifest.spdx.json
-rw-rw-r--    1 guitarrapc guitarrapc        64 Jan 20 00:45 manifest.spdx.json.sha256

生成されたmanifest.spdx.jsonのpackagesを確認すると、ライブラリのライセンス情報が含まれています。

{
  "files": [
    // ... 省略
  ],
  "packages": [
    {
      "name": "Microsoft.DotNet.ILCompiler",
      "SPDXID": "SPDXRef-Package-EC2544D7D5F9A1A227AAE004E2FFF4112D97161B3D683BEC01A80C8BE6AE3AB2",
      "downloadLocation": "NOASSERTION",
      "filesAnalyzed": false,
      "licenseConcluded": "MIT", // <- ライセンス情報が含まれている
      "licenseDeclared": "MIT",  // <- ライセンス情報が含まれている
      "copyrightText": "NOASSERTION",
      "versionInfo": "10.0.1",
      "externalRefs": [
        {
          "referenceCategory": "PACKAGE-MANAGER",
          "referenceType": "purl",
          "referenceLocator": "pkg:nuget/Microsoft.DotNet.ILCompiler@10.0.1"
        }
      ],
      "supplier": "Organization: Microsoft"
    },
    {
      "name": "Microsoft.NET.ILLink.Tasks",
      "SPDXID": "SPDXRef-Package-4AA951B851143838491BE4038EAF79448F24E4A0D7870538FDCB0652BF421C54",
      "downloadLocation": "NOASSERTION",
      "filesAnalyzed": false,
      "licenseConcluded": "MIT", // <- ライセンス情報が含まれている
      "licenseDeclared": "MIT",  // <- ライセンス情報が含まれている
      "copyrightText": "NOASSERTION",
      "versionInfo": "10.0.1",
      "externalRefs": [
        {
          "referenceCategory": "PACKAGE-MANAGER",
          "referenceType": "purl",
          "referenceLocator": "pkg:nuget/Microsoft.NET.ILLink.Tasks@10.0.1"
        }
      ],
      "supplier": "Organization: Microsoft"
    },
    // ... 省略
  ],
  // ... 省略
}

NuGetのSLSA対応状況

NuGetはSLSAに対応しておらず、NuGetパッケージ更新中にSLSA検証してくれません。Issueは立っているのですが、2026年1月時点では着手されていません。

GitHub自身はSLSAに対応しているので、GitHub Actionsでビルドしているならアテステーションは生成できます。ただ、後述するNuGetがnupkgに署名追加する問題を考えると、SLSAは今やっても特にメリットが生まれにくい状況です。

GitHubのSLSAを有効にする

参考までに、SkiaSharp.QrCodeでGitHub SLSAに対応しています。リリース用のワークフローが整備されているなら、permissionsとattestation生成ステップを追加するだけです。

SLSAの対応例

Workflow全文も出します。私はbuild-dotnetジョブでパッケージ生成 & アテステーション生成、create-releaseジョブでNuGetへのアップロード & GitHubリリース作成を行うようにしています。

name: release

on:
  push:
    tags:
      - "[0-9]+.[0-9]+.[0-9]+*" # only tag

jobs:
  build-dotnet:
    # 👇 これを追加
    permissions:
      attestations: write
      contents: read
      id-token: write
    runs-on: ubuntu-24.04
    timeout-minutes: 15
    steps:
      - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
        with:
          persist-credentials: false
      - uses: guitarrapc/actions/.github/actions/setup-dotnet@main
        with:
          restore-wasm-workload: true
          dotnet-version: |
            10.0.x
            8.0.x
      # build
      - run: dotnet build -c Release -p:Version="${GIT_TAG}"
        env:
          GIT_TAG: ${{ github.ref_name }}
      # pack
      - run: dotnet pack -c Release -p:Version="${GIT_TAG}" -o ./publish
        env:
          GIT_TAG: ${{ github.ref_name }}

      # 👇 これを追加
      # attestations
      - name: Generate artifact attestation
        uses: actions/attest-build-provenance@977bb373ede98d70efdf65b84cb5f73e068dcc2a # v3.0.0
        with:
          subject-path: "./publish/*.nupkg"

      - uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
        with:
          name: nuget
          path: ./publish/
          retention-days: 1
          if-no-files-found: error

  create-release:
    needs: [build-dotnet]
    permissions:
      contents: write
      id-token: write # for NuGet Trusted Publish
    runs-on: ubuntu-24.04
    timeout-minutes: 5
    steps:
      - uses: guitarrapc/actions/.github/actions/setup-dotnet@main
      # nuget
      - name: NuGet login (OIDC → temp API key)
        uses: NuGet/login@d22cc5f58ff5b88bf9bd452535b4335137e24544 # v1.1.0
        id: login
        with:
          user: ${{ secrets.SYNCED_NUGET_USER }}
      - uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0
        with:
          name: nuget
          path: ./nuget
      - name: List Nuget
        run: ls -al ./nuget
      # release
      - name: Create Release
        uses: guitarrapc/actions/.github/actions/create-release@main
        with:
          tag: ${{ github.ref_name }}
          title: ${{ github.ref_name }}
          gh-token: ${{ secrets.GITHUB_TOKEN }}
      # upload nuget
      - run: dotnet nuget push "./nuget/*.nupkg" --skip-duplicate -s https://api.nuget.org/v3/index.json -k "${NUGET_KEY}"
        env:
          NUGET_KEY: ${{ steps.login.outputs.NUGET_API_KEY }}

具体的に説明します。

nupkgを生成するbuild-dotnetジョブにid-token: write権限を与えて署名できるようにし、attestations: write権限を与えてアテステーションを生成します。もしReusable Workflowsを使っているなら、呼び出し側ワークフローと、Reusable Workflow両方で権限を設定してください。

jobs:
  build-dotnet:
    permissions:
      attestations: write
      contents: read
      id-token: write

パッケージ改ざんされていないことを保証するため、dotnet pack直後にactions/attest-build-provenanceアクションでアテステーションを生成します。.nupkgのパスをglob指定できます。

# attestations
- name: Generate artifact attestation
  uses: actions/attest-build-provenance@977bb373ede98d70efdf65b84cb5f73e068dcc2a # v3.0.0
  with:
    subject-path: "./publish/*.nupkg"

あとは、GitHubがアテステーションを生成して、いい感じにしてくれます。GitHub ActionsのステップサマリにAttestationへのリンクが表示されます。

ジョブサマリーにAttestationへのリンクが表示される

また、アクション一覧ページからもAttestationsへのリンクが表示されます。

Actions一覧にAttestationsへのリンク

アテステーションはの表示です。

Attestationの表示

ダウンロードするとJSON形式で取得できます。

{
  "mediaType": "application/vnd.dev.sigstore.bundle.v0.3+json",
  "verificationMaterial": {
    "tlogEntries": [
      {
        "logIndex": "835217970",
        "logId": {
          "keyId": "wNI9atQGlz+VWfO6LRygH4QUfY/8W4RFwiT5i5WRgB0="
        },
        "kindVersion": {
          "kind": "dsse",
          "version": "0.0.1"
        },
        "integratedTime": "1768840664",
        "inclusionPromise": {
          "signedEntryTimestamp": "MEYCIQCWAvjFytP+rZXykjvnDsvfTCYZ3eg4WY60TX2EouvptwIhAOArdsnifSjU4BWzC1PwSVFytCk1zcsF6csOZ1FGJhNp"
        },
        "inclusionProof": {
          "logIndex": "713313708",
          "rootHash": "C+YXcG3cfQl9Nt5NhgJb9/a+STS3CQpNGClct3Eg7s0=",
          "treeSize": "713313726",
          "hashes": [
            "isMZTB4pijmD9DhEMUNnGx7LrUX4/3xE22jlto9feQk=",
            "YFb8sqG+Xxbtj3RnT9JWSUweX/30likZ1dhVACCFurU=",
            "D5KzRFYiGngedRCyw2X6YkRI3LmCNLjQH+O1l1sIYJE=",
            "vs1Uz3jRDfkBxuy6zMgRYab9PcDg7++K9iNNHauUlFQ=",
            "0ZzQ4h4W64Ag0vIijb5LPEhhAa7SL8ymU8Lrk0Zj9XM=",
            "NTlN3nIekisb3pmZCH8rrM460Rs1bcJQH1JpdPGmG2o=",
            "3JqUIuWn4UBv2aMZw5lUXv8g6CUqPxu7KWUcBvzAl1s=",
            "RwTGvP9HH06LMfaBZeMYp1fIGDGMg0dbGaZvyt1L0/Y=",
            "EjIUBtrmRy2IqbdKc5Ke7PZKXF3jsG1NMZffwO/GNcw=",
            "ShqwFMnR5RIJ0f9oW9JBTNns25EHyq+5HGjupP/P2Rg=",
            "FdXCR6D6osRQQrTQbKq2kg4TCNotPROY6hrjqR3oFJE=",
            "Da7pWtfttPyV5553iZ/1ojqTqxzHV1f8OS/kCXEaSIc=",
            "vk+Sc7c1laTnH9uCSqZ0Un3rutG4UGrLDkm3cECOZUU=",
            "WFmkMhmL2tOzDi6lp4zGgaCzwux2vGOM44v1vr1wuDs=",
            "F9MSQ5SmoFr+hoADclpdFY52/TLfHDnNPYb9ZNYO5gI=",
            "T4DqWD42hAtN+vX8jKCWqoC4meE4JekI9LxYGCcPy1M="
          ],
          "checkpoint": {
            "envelope": "rekor.sigstore.dev - 1193050959916656506\n713313726\nC+YXcG3cfQl9Nt5NhgJb9/a+STS3CQpNGClct3Eg7s0=\n\n— rekor.sigstore.dev wNI9ajBFAiADtpkKBajYEBDRCC5GyLd3P9mEhPXGSx27O9n8YVrVrgIhAIV0oFk+yvFnoQxLtmaVkZhKMJPg1HnKGJr6Xj04IWfg\n"
          }
        },
        "canonicalizedBody": "eyJhcGlWZXJzaW9uIjoiMC4wLjEiLCJraW5kIjoiZHNzZSIsInNwZWMiOnsiZW52ZWxvcGVIYXNoIjp7ImFsZ29yaXRobSI6InNoYTI1NiIsInZhbHVlIjoiYjI4OGVmY2NhODVkZjFhZjhhZThiNjU5MzI1NjFlZjIzOTVmM2UxNWNmNWM1NjNkNDEwM2QxMjQ5Njg0OGM1MSJ9LCJwYXlsb2FkSGFzaCI6eyJhbGdvcml0aG0iOiJzaGEyNTYiLCJ2YWx1ZSI6ImQzYjlkNjg4ZTNjM2EwMjdlYmFjZDk3ZjJjMTIwMTFkOWNkMTFiOTI1OWVjYjAwMDFlZWM5NzE2OTBhYzE3MzIifSwic2lnbmF0dXJlcyI6W3sic2lnbmF0dXJlIjoiTUVRQ0lETGpQK0RuYnhiajVZem1rWXpGZ2hGcktNQ3NIRVAxamNTZnNBL3B1WXdZQWlCVGxGVlRhcVZreFBXZjJvMkNNUTZwaGJ0MDJ5TVh1cERkTlBsTHFhc2Nzdz09IiwidmVyaWZpZXIiOiJMUzB0TFMxQ1JVZEpUaUJEUlZKVVNVWkpRMEZVUlMwdExTMHRDazFKU1VjdlZFTkRRbTlUWjBGM1NVSkJaMGxWUTNNd1NtMU1LeTl3ZHpGcGIwZHlUR2h5UVhCQmEzSmhZemRaZDBObldVbExiMXBKZW1vd1JVRjNUWGNLVG5wRlZrMUNUVWRCTVZWRlEyaE5UV015Ykc1ak0xSjJZMjFWZFZwSFZqSk5ValIzU0VGWlJGWlJVVVJGZUZaNllWZGtlbVJIT1hsYVV6RndZbTVTYkFwamJURnNXa2RzYUdSSFZYZElhR05PVFdwWmQwMVVSVFZOVkZsNlRucFJNRmRvWTA1TmFsbDNUVlJGTlUxVVdUQk9lbEV3VjJwQlFVMUdhM2RGZDFsSUNrdHZXa2w2YWpCRFFWRlpTVXR2V2tsNmFqQkVRVkZqUkZGblFVVnNWR2xLSzFFeFYwRlBXVEZLVFVwWlpHZHNlRWQwT0dZeWRFMTVRVGhzT0dWcGN6UUtiV1ZKYzNWaVNtNU1PWGhFUldSQ2VtMDVURkZMWkhOb2MxWTFhR0ZLU21GTGJGa3JUblZFYTI5a016RnhOMFpCSzJGUFEwSmhUWGRuWjFkbVRVRTBSd3BCTVZWa1JIZEZRaTkzVVVWQmQwbElaMFJCVkVKblRsWklVMVZGUkVSQlMwSm5aM0pDWjBWR1FsRmpSRUY2UVdSQ1owNVdTRkUwUlVablVWVkRURlF3Q2k5S1JYRXhkVTFCWkc5dUx5OUZXR3RuVlVOcFZYUkpkMGgzV1VSV1VqQnFRa0puZDBadlFWVXpPVkJ3ZWpGWmEwVmFZalZ4VG1wd1MwWlhhWGhwTkZrS1drUTRkMkYzV1VSV1VqQlNRVkZJTDBKSFJYZFlORnBrWVVoU01HTklUVFpNZVRsdVlWaFNiMlJYU1hWWk1qbDBUREprTVdGWVVtaGpia3BvWTBkTmRncFZNblJ3V1ZaT2IxbFlTbmRNYkVaNVVUSTVhMXBUT0hWYU1td3dZVWhXYVV3elpIWmpiWFJ0WWtjNU0yTjVPWGxhVjNoc1dWaE9iRXh1YkdoaVYzaEJDbU50Vm0xamVUbHZXbGRHYTJONU9YUlpWMngxVFVSclIwTnBjMGRCVVZGQ1p6YzRkMEZSUlVWTE1tZ3daRWhDZWs5cE9IWmtSemx5V2xjMGRWbFhUakFLWVZjNWRXTjVOVzVoV0ZKdlpGZEtNV015Vm5sWk1qbDFaRWRXZFdSRE5XcGlNakIzU0hkWlMwdDNXVUpDUVVkRWRucEJRa0ZuVVZKa01qbDVZVEphY3dwaU0yUm1Xa2RzZW1OSFJqQlpNbWQzVG1kWlMwdDNXVUpDUVVkRWRucEJRa0YzVVc5YWFrRXpUbFJXYTAxRVVYbFBWMFUxVFVSa2EwNXFXVE5QVjFsNkNscFhVWGxOZWxGNFRYcG9hRTlIV1hoWmVtUnNUVVJqZWxsVVFWWkNaMjl5UW1kRlJVRlpUeTlOUVVWRlFrRmtlVnBYZUd4WldFNXNUVU5yUjBOcGMwY0tRVkZSUW1jM09IZEJVVlZGUnpKa01XRllVbWhqYmtwb1kwZE5kbFV5ZEhCWlZrNXZXVmhLZDB4c1JubFJNamxyV2xSQlpFSm5iM0pDWjBWRlFWbFBMd3BOUVVWSFFrRTVlVnBYV25wTU1taHNXVmRTZWt3eU1XaGhWelIzVDNkWlMwdDNXVUpDUVVkRWRucEJRa05CVVhSRVEzUnZaRWhTZDJONmIzWk1NMUoyQ21FeVZuVk1iVVpxWkVkc2RtSnVUWFZhTW13d1lVaFdhV1JZVG14amJVNTJZbTVTYkdKdVVYVlpNamwwVFVjd1IwTnBjMGRCVVZGQ1p6YzRkMEZSYTBVS1dIZDRaR0ZJVWpCalNFMDJUSGs1Ym1GWVVtOWtWMGwxV1RJNWRFd3laREZoV0ZKb1kyNUthR05IVFhaVk1uUndXVlpPYjFsWVNuZE1iRVo1VVRJNWF3cGFVemgxV2pKc01HRklWbWxNTTJSMlkyMTBiV0pIT1ROamVUbDVXbGQ0YkZsWVRteE1ibXhvWWxkNFFXTnRWbTFqZVRsdldsZEdhMk41T1hSWlYyeDFDazFFWjBkRGFYTkhRVkZSUW1jM09IZEJVVzlGUzJkM2IxcHFRVE5PVkZaclRVUlJlVTlYUlRWTlJHUnJUbXBaTTA5WFdYcGFWMUY1VFhwUmVFMTZhR2dLVDBkWmVGbDZaR3hOUkdONldWUkJaRUpuYjNKQ1owVkZRVmxQTDAxQlJVeENRVGhOUkZka2NHUkhhREZaYVRGdllqTk9NRnBYVVhkUVoxbExTM2RaUWdwQ1FVZEVkbnBCUWtSQlVYZEVRelZ2WkVoU2QyTjZiM1pNTW1Sd1pFZG9NVmxwTldwaU1qQjJXak5XY0dSSFJubGpiVVozV1hrNVZHRXliR2hWTW1ob0NtTnVRWFZWV0VwRVlqSlNiRTFFWjBkRGFYTkhRVkZSUW1jM09IZEJVVEJGUzJkM2IxcHFRVE5PVkZaclRVUlJlVTlYUlRWTlJHUnJUbXBaTTA5WFdYb0tXbGRSZVUxNlVYaE5lbWhvVDBkWmVGbDZaR3hOUkdONldWUkJaa0puYjNKQ1owVkZRVmxQTDAxQlJVOUNRa1ZOUkROS2JGcHVUWFpoUjFab1draE5kZ3BpVjBad1ltcEJXa0puYjNKQ1owVkZRVmxQTDAxQlJWQkNRWE5OUTFSRk1rNXFRWGxQVkdNeFRYcEJkRUpuYjNKQ1owVkZRVmxQTDAxQlJWRkNRamhOQ2toWGFEQmtTRUo2VDJrNGRsb3liREJoU0ZacFRHMU9kbUpUT1c1a1Yyd3dXVmhLZVZsWVFtcE5RbU5IUTJselIwRlJVVUpuTnpoM1FWSkZSVU5SZDBnS1RYcG5NVTVxVFRGTlJFSjBRbWR2Y2tKblJVVkJXVTh2VFVGRlUwSkdPRTFZVjJnd1pFaENlazlwT0haYU1td3dZVWhXYVV4dFRuWmlVemx1WkZkc01BcFpXRXA1V1ZoQ2Frd3hUbkpoVjBaVVlVZEdlV05ETlZKamEwNTJXa2RWZGt4dFpIQmtSMmd4V1drNU0ySXpTbkphYlhoMlpETk5kbU50Vm5OYVYwWjZDbHBUTlRWWlZ6RnpVVWhLYkZwdVRYWmhSMVpvV2toTmRtSlhSbkJpYWtFMFFtZHZja0puUlVWQldVOHZUVUZGVkVKRGIwMUxSMWwzVG5wVk1WcEVRVEFLVFdwc2FFOVVRVE5hUkZreVRucHNiVTB5Vm10TmFrMHdUVlJOTkZsVWFHMU5WMDB6V2xSQk0wMHlSWGRKVVZsTFMzZFpRa0pCUjBSMmVrRkNSa0ZSVkFwRVFrWXpZak5LY2xwdGVIWmtNVGxyWVZoT2QxbFlVbXBoUkVKcFFtZHZja0puUlVWQldVOHZUVUZGVmtKR1VVMVZiV2d3WkVoQ2VrOXBPSFphTW13d0NtRklWbWxNYlU1MllsTTVibVJYYkRCWldFcDVXVmhDYWt3eFRuSmhWMFpVWVVkR2VXTkROVkpqYTA1MldrZFZkbGxYVGpCaFZ6bDFZM2s1ZVdSWE5Yb0tUSHBKZUUxVVVURk5SRkY0VDFSRmVVd3lSakJrUjFaMFkwaFNla3g2UlhkR1oxbExTM2RaUWtKQlIwUjJla0ZDUm1kUlNVUkJXbmRrVjBwellWZE5kd3BuV1c5SFEybHpSMEZSVVVJeGJtdERRa0ZKUldaQlVqWkJTR2RCWkdkRVpGQlVRbkY0YzJOU1RXMU5Xa2hvZVZwYWVtTkRiMnR3WlhWT05EaHlaaXRJQ21sdVMwRk1lVzUxYW1kQlFVRmFkbGhJY0ZrMlFVRkJSVUYzUWtoTlJWVkRTVUl3T1ZoTGVGSnFjVEp4UldVck9HOWtWV1ZsWkVOT2RXRm9VV0pZV2s4S00xRk5UVmxIYkRSWGFHNVpRV2xGUVRSUWRFVlBiRkZKYmtoRmRGSnBUVVozUzFsRVdVeE1Nemd3VVcxak9XVkhNWFUzY0M5S01VTllSemgzUTJkWlNRcExiMXBKZW1vd1JVRjNUVVJhZDBGM1drRkpkME5MZURoaGRtdExjbFpXVFdKWWNGTk9TbU5NYzNGdllsRkNPUzlTTm5jNGJqaDVSeXRNTkZGRWMySlZDbGsxWkN0blkweHlSV3hQVjFWSVRXcG1kRXBSUVdwQlFuWmpaMlZhWkc1alJrMWljVGM0VERkYVRqazBiSEVyZFhkc2IxSnViV1JyWlROclEwOVVRbFlLUzJwT1RVWjFaMGx5WVhrdmFVWTViM0ZyTkdabWR6UTlDaTB0TFMwdFJVNUVJRU5GVWxSSlJrbERRVlJGTFMwdExTMEsifV19fQ=="
      }
    ],
    "timestampVerificationData": {},
    "certificate": {
      "rawBytes": "MIIG/TCCBoSgAwIBAgIUCs0JmL+/pw1ioGrLhrApAkrac7YwCgYIKoZIzj0EAwMwNzEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MR4wHAYDVQQDExVzaWdzdG9yZS1pbnRlcm1lZGlhdGUwHhcNMjYwMTE5MTYzNzQ0WhcNMjYwMTE5MTY0NzQ0WjAAMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAElTiJ+Q1WAOY1JMJYdglxGt8f2tMyA8l8eis4meIsubJnL9xDEdBzm9LQKdshsV5haJJaKlY+NuDkod31q7FA+aOCBaMwggWfMA4GA1UdDwEB/wQEAwIHgDATBgNVHSUEDDAKBggrBgEFBQcDAzAdBgNVHQ4EFgQUCLT0/JEq1uMAdon//EXkgUCiUtIwHwYDVR0jBBgwFoAU39Ppz1YkEZb5qNjpKFWixi4YZD8wawYDVR0RAQH/BGEwX4ZdaHR0cHM6Ly9naXRodWIuY29tL2d1aXRhcnJhcGMvU2tpYVNoYXJwLlFyQ29kZS8uZ2l0aHViL3dvcmtmbG93cy9yZWxlYXNlLnlhbWxAcmVmcy9oZWFkcy9tYWluMDkGCisGAQQBg78wAQEEK2h0dHBzOi8vdG9rZW4uYWN0aW9ucy5naXRodWJ1c2VyY29udGVudC5jb20wHwYKKwYBBAGDvzABAgQRd29ya2Zsb3dfZGlzcGF0Y2gwNgYKKwYBBAGDvzABAwQoZjA3NTVkMDQyOWE5MDdkNjY3OWYzZWQyMzQxMzhhOGYxYzdlMDczYTAVBgorBgEEAYO/MAEEBAdyZWxlYXNlMCkGCisGAQQBg78wAQUEG2d1aXRhcnJhcGMvU2tpYVNoYXJwLlFyQ29kZTAdBgorBgEEAYO/MAEGBA9yZWZzL2hlYWRzL21haW4wOwYKKwYBBAGDvzABCAQtDCtodHRwczovL3Rva2VuLmFjdGlvbnMuZ2l0aHVidXNlcmNvbnRlbnQuY29tMG0GCisGAQQBg78wAQkEXwxdaHR0cHM6Ly9naXRodWIuY29tL2d1aXRhcnJhcGMvU2tpYVNoYXJwLlFyQ29kZS8uZ2l0aHViL3dvcmtmbG93cy9yZWxlYXNlLnlhbWxAcmVmcy9oZWFkcy9tYWluMDgGCisGAQQBg78wAQoEKgwoZjA3NTVkMDQyOWE5MDdkNjY3OWYzZWQyMzQxMzhhOGYxYzdlMDczYTAdBgorBgEEAYO/MAELBA8MDWdpdGh1Yi1ob3N0ZWQwPgYKKwYBBAGDvzABDAQwDC5odHRwczovL2dpdGh1Yi5jb20vZ3VpdGFycmFwYy9Ta2lhU2hhcnAuUXJDb2RlMDgGCisGAQQBg78wAQ0EKgwoZjA3NTVkMDQyOWE5MDdkNjY3OWYzZWQyMzQxMzhhOGYxYzdlMDczYTAfBgorBgEEAYO/MAEOBBEMD3JlZnMvaGVhZHMvbWFpbjAZBgorBgEEAYO/MAEPBAsMCTE2NjAyOTc1MzAtBgorBgEEAYO/MAEQBB8MHWh0dHBzOi8vZ2l0aHViLmNvbS9ndWl0YXJyYXBjMBcGCisGAQQBg78wAREECQwHMzg1NjM1MDBtBgorBgEEAYO/MAESBF8MXWh0dHBzOi8vZ2l0aHViLmNvbS9ndWl0YXJyYXBjL1NraWFTaGFycC5RckNvZGUvLmdpdGh1Yi93b3JrZmxvd3MvcmVsZWFzZS55YW1sQHJlZnMvaGVhZHMvbWFpbjA4BgorBgEEAYO/MAETBCoMKGYwNzU1ZDA0MjlhOTA3ZDY2NzlmM2VkMjM0MTM4YThmMWM3ZTA3M2EwIQYKKwYBBAGDvzABFAQTDBF3b3JrZmxvd19kaXNwYXRjaDBiBgorBgEEAYO/MAEVBFQMUmh0dHBzOi8vZ2l0aHViLmNvbS9ndWl0YXJyYXBjL1NraWFTaGFycC5RckNvZGUvYWN0aW9ucy9ydW5zLzIxMTQ1MDQxOTEyL2F0dGVtcHRzLzEwFgYKKwYBBAGDvzABFgQIDAZwdWJsaWMwgYoGCisGAQQB1nkCBAIEfAR6AHgAdgDdPTBqxscRMmMZHhyZZzcCokpeuN48rf+HinKALynujgAAAZvXHpY6AAAEAwBHMEUCIB09XKxRjq2qEe+8odUeedCNuahQbXZO3QMMYGl4WhnYAiEA4PtEOlQInHEtRiMFwKYDYLL380Qmc9eG1u7p/J1CXG8wCgYIKoZIzj0EAwMDZwAwZAIwCKx8avkKrVVMbXpSNJcLsqobQB9/R6w8n8yG+L4QDsbUY5d+gcLrElOWUHMjftJQAjABvcgeZdncFMbq78L7ZN94lq+uwloRnmdke3kCOTBVKjNMFugIray/iF9oqk4ffw4="
    }
  },
  "dsseEnvelope": {
    "payload": "eyJfdHlwZSI6Imh0dHBzOi8vaW4tdG90by5pby9TdGF0ZW1lbnQvdjEiLCJzdWJqZWN0IjpbeyJuYW1lIjoiU2tpYVNoYXJwLlFyQ29kZS4wLjAuMC1kZXYubnVwa2ciLCJkaWdlc3QiOnsic2hhMjU2IjoiZGQzNDEyZWVhZTM2MzgwOGViM2M0YTNmOGZkMzI2YThmMTM2ZDE5NGM4YzZmYmU0NDU1OTc1Yjc3NGQ4OWU3ZiJ9fV0sInByZWRpY2F0ZVR5cGUiOiJodHRwczovL3Nsc2EuZGV2L3Byb3ZlbmFuY2UvdjEiLCJwcmVkaWNhdGUiOnsiYnVpbGREZWZpbml0aW9uIjp7ImJ1aWxkVHlwZSI6Imh0dHBzOi8vYWN0aW9ucy5naXRodWIuaW8vYnVpbGR0eXBlcy93b3JrZmxvdy92MSIsImV4dGVybmFsUGFyYW1ldGVycyI6eyJ3b3JrZmxvdyI6eyJyZWYiOiJyZWZzL2hlYWRzL21haW4iLCJyZXBvc2l0b3J5IjoiaHR0cHM6Ly9naXRodWIuY29tL2d1aXRhcnJhcGMvU2tpYVNoYXJwLlFyQ29kZSIsInBhdGgiOiIuZ2l0aHViL3dvcmtmbG93cy9yZWxlYXNlLnlhbWwifX0sImludGVybmFsUGFyYW1ldGVycyI6eyJnaXRodWIiOnsiZXZlbnRfbmFtZSI6IndvcmtmbG93X2Rpc3BhdGNoIiwicmVwb3NpdG9yeV9pZCI6IjE2NjAyOTc1MyIsInJlcG9zaXRvcnlfb3duZXJfaWQiOiIzODU2MzUwIiwicnVubmVyX2Vudmlyb25tZW50IjoiZ2l0aHViLWhvc3RlZCJ9fSwicmVzb2x2ZWREZXBlbmRlbmNpZXMiOlt7InVyaSI6ImdpdCtodHRwczovL2dpdGh1Yi5jb20vZ3VpdGFycmFwYy9Ta2lhU2hhcnAuUXJDb2RlQHJlZnMvaGVhZHMvbWFpbiIsImRpZ2VzdCI6eyJnaXRDb21taXQiOiJmMDc1NWQwNDI5YTkwN2Q2Njc5ZjNlZDIzNDEzOGE4ZjFjN2UwNzNhIn19XX0sInJ1bkRldGFpbHMiOnsiYnVpbGRlciI6eyJpZCI6Imh0dHBzOi8vZ2l0aHViLmNvbS9ndWl0YXJyYXBjL1NraWFTaGFycC5RckNvZGUvLmdpdGh1Yi93b3JrZmxvd3MvcmVsZWFzZS55YW1sQHJlZnMvaGVhZHMvbWFpbiJ9LCJtZXRhZGF0YSI6eyJpbnZvY2F0aW9uSWQiOiJodHRwczovL2dpdGh1Yi5jb20vZ3VpdGFycmFwYy9Ta2lhU2hhcnAuUXJDb2RlL2FjdGlvbnMvcnVucy8yMTE0NTA0MTkxMi9hdHRlbXB0cy8xIn19fX0=",
    "payloadType": "application/vnd.in-toto+json",
    "signatures": [
      {
        "sig": "MEQCIDLjP+Dnbxbj5YzmkYzFghFrKMCsHEP1jcSfsA/puYwYAiBTlFVTaqVkxPWf2o2CMQ6phbt02yMXupDdNPlLqascsw=="
      }
    ]
  }
}

nupkgのSLSAを検証する

SLSAの検証を確認しましょう。slsa-verifierなどありますが、ghを使うのが一番簡単でしょう。ghの場合、gh attestation verifyコマンドで検証できます。

gh attestation verify <path/to/artifact/to/verify> -R <org/repo>

例えば、SkiaSharp.QrCodeのdevパッケージをGitHub Actionsで生成、生成したnupkgをダウンロードして検証してみましょう。

$ gh attestation verify --repo guitarrapc/SkiaSharp.QrCode "SkiaSharp.QrCode.0.0.0-dev.nupkg"
Loaded digest sha256:dd3412eeae363808eb3c4a3f8fd326a8f136d194c8c6fbe4455975b774d89e7f for file://SkiaSharp.QrCode.0.0.0-dev.nupkg
Loaded 1 attestation from GitHub API

The following policy criteria will be enforced:
- Predicate type must match:................ https://slsa.dev/provenance/v1
- Source Repository Owner URI must match:... https://github.com/guitarrapc
- Source Repository URI must match:......... https://github.com/guitarrapc/SkiaSharp.QrCode
- Subject Alternative Name must match regex: (?i)^https://github.com/guitarrapc/SkiaSharp.QrCode/
- OIDC Issuer must match:................... https://token.actions.githubusercontent.com

✓ Verification succeeded!

The following 1 attestation matched the policy criteria

- Attestation #1
  - Build repo:..... guitarrapc/SkiaSharp.QrCode
  - Build workflow:. .github/workflows/release.yaml@refs/heads/main
  - Signer repo:.... guitarrapc/SkiaSharp.QrCode
  - Signer workflow: .github/workflows/release.yaml@refs/heads/main

GitHub Actionsで生成したものなので、アテステーションと一致しており検証が成功しました。

もしローカルのnupkgを検証すると該当するアテステーションが失敗します。これで、ローカルでビルドしたnupkgをあたかもリリースパッケージのように見せかけても、SLSA検証に失敗することがわかります。

$ gh attestation verify --repo guitarrapc/SkiaSharp.QrCode "./src/SkiaSharp.QrCode/bin/Release/SkiaSharp.QrCode.1.0.0.nupkg"
Loaded digest sha256:aa2f5f3c67d7b5f09839a388cff25da7c5a22e3ecf68cb22d4d4f190198c70e0 for file://src\SkiaSharp.QrCode\bin\Release\SkiaSharp.QrCode.1.0.0.nupkg
✗ Loading attestations from GitHub API failed

Error: failed to fetch attestations from guitarrapc/SkiaSharp.QrCode: HTTP 404: Not Found (https://api.github.com/repos/guitarrapc/SkiaSharp.QrCode/attestations/sha256:aa2f5f3c67d7b5f09839a388cff25da7c5a22e3ecf68cb22d4d4f190198c70e0?per_page=30)

NuGetが.nupkgに署名する問題

せっかく作ったGitHub SLSAですが、NuGetはアップロードされたパッケージに対して署名するためアップロードした.nupkgファイルとNuGetサーバーからダウンロードした.nupkgファイルは異なります2。ということは、ビルド用に生成したSLSA Attestationと一致せずgh attestation verify検証が失敗します。

NuGetサーバーからダウンロードした.nupkgファイルの中身を確認してみましょう。中に.signature.p7sが含まれているのがわかります。

$ curl -L -o SkiaSharp.QrCode.0.12.0.nupkg "https://www.nuget.org/api/v2/package/SkiaSharp.QrCode/0.12.0"
$ unzip -l skiasharp.qrcode.0.12.0.nupkg
Archive:  skiasharp.qrcode.0.12.0.nupkg
  Length      Date    Time    Name
---------  ---------- -----   ----
      506  28/11/2025 14:24   _rels/.rels
     2176  28/11/2025 14:24   SkiaSharp.QrCode.nuspec
    85504  28/11/2025 14:24   lib/net10.0/SkiaSharp.QrCode.dll
    86016  28/11/2025 14:24   lib/net8.0/SkiaSharp.QrCode.dll
    91648  28/11/2025 14:24   lib/netstandard2.0/SkiaSharp.QrCode.dll
    87552  28/11/2025 14:24   lib/netstandard2.1/SkiaSharp.QrCode.dll
    33143  28/11/2025 14:23   README.md
      520  28/11/2025 14:24   [Content_Types].xml
      687  28/11/2025 14:24   package/services/metadata/core-properties/d303c5891bb440bba97102727f1ff08b.psmdcp
    12984  28/11/2025 06:27   .signature.p7s
---------                     -------
   400736                     10 files

dotnet packで生成したnupkgファイルには.signature.p7sは含まれていません。

unzip -l SkiaSharp.QrCode.0.0.0-dev.nupkg
Archive:  SkiaSharp.QrCode.0.0.0-dev.nupkg
  Length      Date    Time    Name
---------  ---------- -----   ----
      520  19/01/2026 16:37   [Content_Types].xml
     2179  19/01/2026 16:37   SkiaSharp.QrCode.nuspec
    33143  19/01/2026 16:36   README.md
      506  19/01/2026 16:37   _rels/.rels
    86016  19/01/2026 16:37   lib/net8.0/SkiaSharp.QrCode.dll
    87552  19/01/2026 16:37   lib/netstandard2.1/SkiaSharp.QrCode.dll
    91648  19/01/2026 16:37   lib/netstandard2.0/SkiaSharp.QrCode.dll
    85504  19/01/2026 16:37   lib/net10.0/SkiaSharp.QrCode.dll
    19077  19/01/2026 16:37   _manifest/spdx_2.2/manifest.spdx.json
       64  19/01/2026 16:37   _manifest/spdx_2.2/manifest.spdx.json.sha256
      690  19/01/2026 16:37   package/services/metadata/core-properties/be58e68591294e81841f7c0e290b7f4f.psmdcp
---------                     -------
   406899                     11 files

この問題はIssueで報告されていますが、NuGet側の対応はちょっとどうするのか読めない状況です。

ワークアラウンドしては、NuGetサーバーからダウンロードしたnupkgファイルの.signature.p7sを削除してからSLSA検証します。これはNixOSがやっている方法です。

    runCommand src.name
      {
        inherit src;
        nativeBuildInputs = [ zip ];
      }
      ''
        zip "$src" --temp-path "$TMPDIR" --output-file "$out" --delete .signature.p7s || {
          (( $? == 12 ))
          install -Dm644 "$src" "$out"
        }
      '';

コマンドでやるなら次のようになります。

# まだリリースされていないですが...
$ curl -L -o SkiaSharp.QrCode.0.0.13.nupkg "https://www.nuget.org/api/v2/package/SkiaSharp.QrCode/0.13.0"
$ zip -d SkiaSharp.QrCode.0.0.13.nupkg .signature.p7s

# その後でSLSA検証
$ gh attestation verify --owner guitarrapc "SkiaSharp.QrCode.0.0.13.nupkg"

いずれにしても、NuGetクライアントのワークフロー的にはSLSA検証は機能しないですし、この署名は根深そうに見えます。

正直、NuGetがアップロードされた.nupkgに署名ファイルを追加しているのはちょっと危なそうにも見えます。署名ファイルだけで信頼されてしまうと、.nupkgのアップロード過程を攻撃されると、悪意のあるコードを含む.nupkgが署名付きで配布されてしまうリスクがありそうですだからです。SLSAのようなビルドの信頼性を確保する仕組みと矛盾しているように見えるので、将来どうするのか注目しています。

Unity

Unityは特にSBOMやSLSAには対応していません。ディスカッションは上がっていますが、Adminのコメントを見る限り現時点では特に対応予定はないようです。

Unity 6.3以降、tarballに署名して組織で共有できるようになりますが、これはSBOM/SLSAとは別の仕組みです。

まとめ

C#的には、SBOMは今から対応しておいても良いですが、SLSAはNuGetの対応を待つのが賢明です。Unityは無視でOKです。

C#

SBOMは組み込んでもいいでしょう。Microsoft.Sbom.Targetsを入れるとnupkgにsbom含められるので意識せずに担保できます。今は、ライセンス出力周りが機能していないので修正待ちです。

SLSAは今やっても特にメリットが生まれにくい状況です。NuGetの対応を待つのが賢明です。GitHubのアテステーションは生成できますが、実用性は低いです。

Unity

現時点では、SBOM・SLSAの仕組みは提供されていません。無視でOK。

参考

対応参考

NuGet/Unity Issue

GitHub

NuGet

Blog


  1. デフォルトのSPDXバージョンが2.2なのでパスはspdx_2.2ですが、3.0に変更するとパスはspdx_3.0になります。
  2. この署名はNuGetクライアントがパッケージの整合性を検証するために使われます。

OSS開発者にとってのSBOMとSLSAの状況

ソフトウェアのサプライチェインを担保する手段としてSBOMとSLSAがあります。 SBOM(Software Bill of Materials)は、そのソフトウェアの構成要素をリスト化したもの、SLSA(Supply chain Levels for Software Artifacts)は、ソフトウェア成果物の工程がどの程度信頼できるかを段階的に定義したフレームワークです。

今回は、2026年1月時点におけるSBOMとSLSAの状況について調べたことのメモです。

モチベーション

ここ数年OSSソフトウェアを利用した攻撃の1つとしてサプライチェイン攻撃を見かける頻度が上がっています。攻撃の最終目標は仮想通貨プラットフォームへの侵入だったりするようですが、OSSソフトウェアは他のOSSソフトウェアに依存していることが多いという性質から、攻撃者はその依存関係を悪用して攻撃を仕掛けることができます。

OSSライブラリ作者として自分が開発しているOSSライブラリがサプライチェイン攻撃に巻き込まれた際、その影響を早く確認し、また利用者に正当なライブラリであることを証明するのに役立つのがSBOMとSLSAです。これらを組み合わせることで、そもそもライブラリに影響しているのか確認しやすくし、またライブラリ利用者が正当なライブラリか検証しやすくなります。

今回の調査は、今後OSSライブラリ回りでSBOMやSLSAが要請されるようになる可能性もあるため、それに備えて現状のSBOMとSLSAの状況を調べておくのを動機としています。この調査を元に、次回はC#ライブラリにおけるSBOMとSLSAについてみていきます。

サプライチェインとSBOMとSLSA

ソフトウェアのサプライチェインは2つの視点があります。SBOMとSLSAはそれぞれ別領域で、サプライチェインという視点では両者が揃うことで、ソフトウェアの信頼性を高めることができます。

  1. そのソフトウェアは何でできているのか。構成しているソフトウェアは何なのか部品を明確にする -> SBOM
  2. そのソフトウェアがどのように作られたか(provenance)を、アテステーションなど検証可能な形で示す -> SLSA

つまりSBOMで何が入っているかをリストアップし、ハッシュで改ざんされてないか、SLSAのアテステーションで「そのハッシュが正規のビルド工程から生成されたという主張」を署名付きで提供するイメージです。これを提供したいかどうかが判断基準になります。

SBOMの例としてSPDX2.2フォーマットの一例を見てみましょう。filesに配布時含まれるファイル群、packagesにソフトウェアを構成するパッケージ群が記載されます。NOASSERTIONはSBOM発行時に指定しないなどの理由で情報がない場合に使われます。なるほど、確かにSBOMを見れば、そのソフトウェアが何でできているのか把握できます。

{
  // 含まれるファイル群
  "files": [
    {
      "fileName": "./lib/libfoo.so",
      "SPDXID": "SPDXRef-File--lib-Example.libfoo-AAAAAAA",
      "checksums": [
        {
          "algorithm": "SHA256",
          "checksumValue": "ハッシュ値"
        },
        {
          "algorithm": "SHA1",
          "checksumValue": "ハッシュ値"
        }
      ],
      "licenseConcluded": "NOASSERTION",
      "licenseInfoInFiles": [
        "NOASSERTION"
      ],
      "copyrightText": "NOASSERTION"
    }
  ],
  // 構成するパッケージ群
  "packages": [
    {
      "name": "Example.Package",
      "SPDXID": "SPDXRef-Package-Example.Package",
      "downloadLocation": "NOASSERTION",
      "filesAnalyzed": false,
      "licenseConcluded": "NOASSERTION",
      "licenseDeclared": "NOASSERTION",
      "copyrightText": "NOASSERTION",
      "versionInfo": "1.0.0",
      "externalRefs": [
        {
          "referenceCategory": "PACKAGE-MANAGER",
          "referenceType": "purl",
          "referenceLocator": "pkg:foo/Example.Package@1.0.0"
        }
      ],
      "supplier": "NOASSERTION"
    },
  ],
  "externalDocumentRefs": [],
  "relationships": [],
  // SBOMドキュメント情報
  "spdxVersion": "SPDX-2.2",
  "dataLicense": "CC0-1.0",
  "SPDXID": "SPDXRef-DOCUMENT",
  "name": "example-project-1.0.0",
  "documentNamespace": "http://spdx.org/spdxdocs/example-project-1.0.0-12345678",
  // SBOM作成情報
  "creationInfo": {
    "created": "2026-01-16T12:00:00Z",
    "creators": [
      "Organization: ExampleOrg",
      "Tool: ExampleSBOMGenerator-1.0"
    ]
  },
  "documentDescribes": []
}

SLSAの例としてGitHub Actionsのアテステーションレポートを見てみましょう。ジョブでアテステーションレポートをid-token署名付きで生成、アテステーション一覧からたどることができます。ここには、いつ(日付)、だれが(実行者)、どこで(GitHubホステッド環境)、どのように(どの時点のどのワークフロー)そのソフトウェアを生成したかが記載されます。たしかにこれがあれば、そのソフトウェアがGitHub Actionsで生成されたことがわかります。

SLSAの例

SBOMで構成要素を把握し、SLSAでその生成プロセスを保証することで、ソフトウェアの信頼性を高めることができるという考え方とわかります。

OSSライブラリ開発者にとってのメリット

OSSライブラリの開発者としては、自身のOSSライブラリの構成を把握し、配布物が外部から検証可能な形で提供できることに一定のメリットがあります。

  1. SBOMがあれば、サプライチェイン攻撃に巻き込まれたとき、いつの時点のライブラリが影響を受けたのか特定しやすくなる
  2. SLSAがあれば、自身のライブラリが正当に生成されたものであることを示せる

サプライチェイン攻撃が発生したときにSBOMを確認することで、いつの時点のライブラリが影響を受けたのか特定しやすくなります。また、SBOM発行時の設定次第では、構成するライブラリにMPLやGPLライセンスなど自身が意図しないライセンスのライブラリが含まれているか確認できます。

SLSAはソフトウェアのサプライチェインのセキュリティレベルを評価するフレームワークで、レベル1(低)からレベル4(高)まであります。SLSAレベルが高いほどソフトウェアが改ざんされていないことを外部から検証しやすくなります。従来からSHA256などハッシュ値による検証はありましたが、そのハッシュ値が正当に生成されたものであるかまでは検証できません。それに対してSLSAは、例えばGitHub Actionsでビルドしてリリースしている場合に、ソフトウェアが生成されたから配布されるまでのプロセスとセットにすることで、ただのハッシュ値検証よりも強力にパッケージの正当性を保証します。

もう少し詳しくSBOMとSLSAについて見ていきましょう。

SBOM

SBOMは、そのソフトウェア(あるいはコンテナイメージ)に含まれる依存関係を、機械可読な形で列挙するものです。SBOMのフォーマットとしてSPDXとCycloneDXがよく使われますが、いずれにしてもSBOMには次の情報が記載されます。

  • 使われているライブラリとそのバージョン
  • 直接依存・間接依存の区別
  • ライセンス情報
  • ハッシュ

SBOMには、Source SBOMとBuild SBOMの2種類があります。Source SBOMは、ソースコードの依存関係から生成されるSBOMで、例えばロックファイル(packages.lock.json)から生成されます。一方、Build SBOMは、ビルド成果物から生成されるSBOMで、実際に含まれているライブラリを正確に反映します。

ただし、SBOMはSBOMファイルが正しいかは保証せず、改ざんされた場合もSBOM自体では判別できず、誰が作成したかの保証もできません。それって困りますよね? ということで、成果物がどうやって生成されたかを保証するSLSAが補完的に欲しくなります。

SBOMの生成ツール

OSSなら、microsoft/sbom-toolanchore/syftがよく利用されています。両方ともCLIツールを提供していますが、sbom-toolは.NET向けのMSBuildタスクがありNuGetパッケージ作成時に自動的に含めることができます。

また、GitHubもリポジトリ > Insights > Dependency graph > Generate SBOMからSBOMを生成できます。Export SBOMしたことはなくても、Dependency graphページをみたことがある人は多いんじゃないでしょうか。

SBOMをエキスポート

SBOMのフォーマット

SBOMのフォーマットとしてSPDXCycloneDXがよく使われます。SPDXはLinux Foundationが策定したフォーマットで、オープンソースソフトウェアのライセンス情報を管理するために設計されました。一方、CycloneDXはOWASPが策定したフォーマットで、セキュリティに重点を置いています。どちらも広く使われていますが、SPDXはライセンス情報に強みがあり、CycloneDXはセキュリティ情報に強みがあります。

CycloneDXは脆弱性情報やVEXなどセキュリティ用途の拡張が充実しており、開発中のソフトウェアのSBOM生成に向いています。一方、SPDXはライセンス情報に強みがあり、配布物に同梱するSBOMとして向いています。どっちかのフォーマットに統一するという感じではないようです。OSSライブラリ開発者としては、配布物にSBOMを同梱する場合はSPDXを選ぶとよいでしょう。

なお、SPDXにはバージョン2.2/2.33.0がありますが、現時点では2.2/2.3対応ツールが主流なため3.0に手を出すのはまだ早い印象です。3.0はデータモデルが刷新されており、2.xの文書を3.0として扱うには変換が必要です。

SBOMの課題

使っていて感じた課題がいくつかあります。

  • ツールによって生成結果が異なる
  • 生成時に指定しなかった情報はNOASSERTIONになる
  • SPDX v3.0はいつから使えるのか

SBOMはフォーマットがあるにも関わらず、ツールによって生成結果が微妙に異なっており一貫したフォーマット提供に課題があります。ツールによって含まれるコンポーネント数が違うのはしょうがないとしても、依存階層が一致しないとか困ったものです。

また、SBOM生成時にライセンスなどを指定しない限りはNOASSERTIONになります。このため、必要な情報を含めるにはどうすればいいのかはツールごとに調べる必要があります。ライセンス情報はSigstoreなどから収集することが多いようなので、ライブラリやツールのバージョン途中からライセンス変わったらどうなるの?新しいバージョンでライセンス変わった時に反映はいつ?など気になる点があります。

SPDX v3.0は2024年にリリースされましたが、一年たっても試験対応ポジションのツールが多いのも気になります。SBOMツールには外部からのコントリビュートは明示的に拒否するポリシーをもつケースもあることから、セキュリティに関わるので時間がかかっても安定したフォーマット出力を優先する気配は感じます。

SLSA

SLSAは、その成果物が信頼できる工程で作られたことを段階的に保障することを目的としたフレームワークです。SLSA v1.0には4つのレベルがあり、レベルが上がるほど信頼性が高くなります。

  • Level 1: ビルド来歴(Provenance)がある
  • Level 2: ビルド来歴(Provenance)が署名されており、ホステッドビルド環境で生成されている
  • Level 3: 改ざんに強いビルド基盤
  • Level 4: 再現可能・検証可能

GitHub Actionsを使うことで、ビルド来歴提供やビルド自動化が実現できるのでSLSAレベル1と2は比較的簡単に達成できます。Provenanceを具体化したものが、先のアテステーションレポートでありレベル2の提供です。レベル3にはビルド基盤が改ざんしにくくなる工夫を要します。GitHubでのSLSA対応はドキュメントが用意されているので参考になります。

ただ、アテステーションレポートがあるかといってソフトウェアが安全である保障はないことには注意が必要です。SLSAでソフトウェアのソースコードとビルド手順へのリンクは提供されますが、ソフトウェア自体のリスク判断は行いません。例えば、悪意のあるコードがソースコードに含まれている場合、そのソースコードをビルドした成果物はSLSAレベル4であっても安全とは言えません。

SLSAの検証ツール

SLSAアテステーションレポートを検証するツールとして、ghコマンドやslsa-verifierがあります。GitHub Actionsを使っているなら、ghコマンドを用いるのが簡単です。

# Verify an artifact linked with a repository
$ gh attestation verify example.bin --repo github/example

# Verify an OCI image using attestations stored on disk
$ gh attestation verify oci://<image-uri> --owner github --bundle sha256:foo.jsonl

# Verify an artifact signed with a reusable workflow
$ gh attestation verify example.bin --owner github --signer-repo actions/example

slsa-verifierでもGitHubアテステーションレポートは検証できます。

$ curl -sSO https://bcr.bazel.build/modules/aspect_rules_lint/1.3.4/MODULE.bazel
$ curl -sSO https://bcr.bazel.build/modules/aspect_rules_lint/1.3.4/MODULE.bazel.intoto.jsonl
$ slsa-verifier verify-github-attestation --source-uri github.com/aspect-build/rules_lint --builder-id https://github.com/bazel-contrib/publish-to-bcr/.github/workflows/publish.yaml --attestation-path MODULE.bazel.intoto.jsonl MODULE.bazel

SLSAレベル3は現実的なのか

SLSAレベル3の改ざんに強いビルド基盤とは、誰も変更できないことを意味するのではなく、変更された場合にその事実を後から検証できる、という意味である点には注意が必要です。言い換えると、「そのビルド成果物が事前に定義されたビルド手順から人の手による介入なく生成されたものであることを後から検証できる」ことを意味します。

SLSAレベル2以上はホスト環境を求めていますが、これはビルド環境の完全性をGitHubなどクラウド事業者の責任範囲において、代わりにユーザーが直接触れないことを信頼の根拠にしていると捉えられます。GitHub ActionsのホステッドランナーはMicrosoftが管理しており、ユーザーはその環境に直接アクセスできません。つまり、ビルド環境の完全性をMicrosoftに依存することで、ユーザーはビルド環境が改ざんされていないことを信頼できます。

誤解していたこと

改ざんに強いは「ビルドステップ自体を改ざんできない」という意味ではないことは注意が必要です。私はこれを当初誤解していました。GitHub ActionsでSLSA3を達成するためのドキュメントが提供されています。これはビルドにReusable Workflowを使うことを求めていますが、ビルドステップを別リポジトリで管理するのが直接的にビルド自体が改ざんできなくなるかというと、個人的には疑問があります。そのリポジトリがプライベートリポジトリである場合、「ビルドステップは外部から改ざんするのは非常に困難である」と言えそうですが、パブリックリポジトリである場合、誰でもプルリクエストを送れるためマージ・レビュー次第では「ビルドステップが改ざんに強いであるとは言い切れない」です。改ざんに強いはあくまでも外部からのPRを受け付けない、内部の変更に対しても注意を払うしかないということになります。

実際にできそうなライン

GitHubドキュメントを見る限り、OSSライブラリ開発者としてSLSAレベル3を達成する以下の条件を満たすのは現実的なラインと考えています。

  • ビルドステップをReusable Workflowで管理する
  • アテステーションレポートを生成する

GitHub Actionsでアテステーションを生成するのは、ジョブに権限をつけて1つステップを追加するだけで済みます。これを丸っとReusable Workflowにしておくイメージです。

on:
  # Reusable Workflowとして呼び出される
  workflow_call:

jobs:
  build:
    # 権限が必要
    permissions:
      attestations: write
      contents: read
      id-token: write
    runs-on: ubuntu-24.04
    steps:
      # ...ビルドステップで成果物を生成する
      - name: Build lib...
        run: ...

      # アテステーションを生成すると、自動的にGitHubに登録される
      - name: Generate artifact attestation
        uses: actions/attest-build-provenance@977bb373ede98d70efdf65b84cb5f73e068dcc2a # v3.0.0
        with:
          subject-path: "./publish/libfoo.so"

しかし、Reusable Workflowに分けても、それを改ざんされたら意味がないのでなんだかちぐはぐ感は否めない気もします。

改ざんとイミュータブルリリース

改ざんについて考えると、同一リリースやタグを上書き可能な場合、後から改ざんしてアテステーションを差し替える余地があるように思えます。例えば、GitHub Releasesで同一タグを上書きできる設定になっている場合、ビルド工程を丸っと乗っ取られた場合に、同じタグやリリースを後から上書きされる可能性があります。権限次第でアテステーションも削除ができますしね。これを仕組みで防ぐには、イミュータブルリリースが必要になります。

幸いGitHubはImmutable Releasesをサポートしているので、これを有効にすることで同一タグの上書きを防ぐことができます。SLSAうんぬんに関わらず、この設定は有効にしておくのがよさそうです。

SBOMとSLSAの活用例

SBOMとSLSAを使った活用例はいくつか見かけるので紹介します。

SBOMをライセンスリスク管理に使う

SBOMでライセンス情報も出力することで、ライセンスリスク管理が可能になります。例えば、MITで配布したいライブラリに、GPLライセンスや商用ライセンスが混入することは避けたいでしょう。しかし、ライブラリの依存関係が複雑になると、どのライブラリがどのライセンスなのか把握するのは困難です。SBOMを使うことで、ライセンス情報を自動的に収集し、ライセンスリスクを管理できます。

SLSAをアーティファクトのハッシュ値検証に使う

SLSAアテステーションが発行されていると、単にハッシュ値を配布するだけでなく「ハッシュが何のか + そのハッシュがどの工程で生成されたか」まで含めて検証できるようになります。よくリリースアーティファクトにlibfoo.solibfoo.so.sha256のように提供されているハッシュ値は、ダウンロード途中で改ざんされた際に改ざんを検出できるものの、そのハッシュ値自体が改ざんされていた場合には検出できません。つまり、攻撃者がlibfoo.solibfoo.so.sha256の両方を改ざんした場合、利用者がそれを検出できる仕組みではありません。

SLSAアテステーションレポートには、対象アーティファクトのハッシュ値が含まれ、利用者はそのビルドステップで生成されたものであることを検証できます。

例えば、aquaがこれを使ってインストールするパッケージの改ざんチェックを行っています。

まとめ

個人的に調べたSBOMとSLSAの状況についてのメモ書きでした。

OSSライブラリ開発者としては、SBOMを配布物に同梱することは悪くなさそうです。ただGitHubのInsightsから見てもらったり、Dependabotに任せることができている現状からすると、SBOMを積極的に活用する場面はまだ少ない印象です。標準ビルドで自動的に生成されるなら、配布パッケージに入れても損はないかなという温度感。

SLSAに関しては、GitHub Actionsを使っている場合はアテステーションレポートを生成するのは簡単なので、OSSライブラリの信頼性を高めるために導入自体は悪くなさそうです。GitHub Actionsを利用しているOSSライブラリ開発者としては、SLSAレベル3 + アテステーションレポートを添えるのは難しくない感触です。

SBOM/SLSAに関わらず、リリースが後から上書きされるのは利用者からすると疑いしかないので、SBOM/SLSAに関わらずGitHubのイミュータブルリリースは有効にしておくのがよさそうです。

参考

GitHub

規格

ドキュメント

ブログ

NuGetのロックファイルは使うべきなのか

NuGetにはロックファイル(packages.lock.json)を用いてリストアする機能があります。npmではpackage-lock.jsonが当たり前に使われていますが、C#のプロジェクトでロックファイルを使っている例はあまり見かけません。

最近SBOMについて調べる中で、なぜNuGetのロックファイルがあまり使われていないのか、そもそも使うべきなのかを考えてみました。この記事では、NuGetのロックファイルの仕組みと、C#におけるパッケージ管理の文化的な背景から、ロックファイルの必要性について考察します。

ロックファイルとは

ロックファイルとは、プロジェクトが依存するパッケージのバージョンを固定化するためのファイルです。

Microsoft Learnを見ると、プロジェクトが依存するパッケージには、「直接依存するもの(トップレベル・直接/Top-level or Direct)」と「間接的に依存するもの(トランジティブ・推移的/Transitive)」があります。 イメージしやすいようにnpmで例えると、@modelcontextprotocol/sdkパッケージを入れるとします。 この場合、@modelcontextprotocol/sdkが直接依存するパッケージで、@modelcontextprotocol/sdkが依存している@hono/node-serverajvなどは間接的に依存するパッケージです。

npmで@modelcontextprotocol/sdkの間接的に依存するパッケージが確認できる

ロックファイルは、あるパッケージをインストールしたときのバージョンと、そのパッケージを導入したときに推移的にインストールされたパッケージのバージョンを記録します。これにより、同じプロジェクトを別の環境でセットアップしたときに、同じバージョンのパッケージがインストールされることを保証します。

NuGetのロックファイル

NuGetにもロックファイルを利用する機能がありますが、デフォルトでは無効になっています。ロックファイルを利用するには.csproj<RestorePackagesWithLockFile>true</RestorePackagesWithLockFile>に設定して、プロジェクトをリストア(dotnet restore)します。すると、.csprojがあるパスにpackages.lock.jsonというファイルが生成されます。

  <PropertyGroup>
    <RestorePackagesWithLockFile>true</RestorePackagesWithLockFile>
  </PropertyGroup>

試してみましょう。プロジェクト追加 → 初回のパッケージ追加 → リストア → ロックファイル追加後のリストアを順に実行します。今回は私の書いているライブラリであるSkiaSharp.QrCodeパッケージを使用します。NuGetを見るとSkiaSharpSkiaSharp.NativeAssets.macOS/SkiaSharp.NativeAssets.Win32に依存していることがわかります。

NuGetで確認できるSkiaSharp.QrCodeパッケージの依存関係。SkiaSharpやNativeAssetsパッケージに依存していることがわかる

まずはコンソールプロジェクトを作成し、SkiaSharp.QrCodeパッケージを追加してリストアします。この時点ではロックファイルは生成されていません。

$ mkdir -p ConsoleApp3 && cd ConsoleApp3
$ dotnet new console -n ConsoleApp3
$ dotnet package add SkiaSharp.QrCode
$ dotnet restore
Restore complete (0.9s)

Build succeeded in 1.1s

$ ls -la
ls -la
total 16
drwxrwxr-x    3 guitarrapc   guitarrapc      0 Jan 14 16:58 .
drwxrwxr-x    8 guitarrapc   guitarrapc   4096 Jan 14 16:58 ..
-rw-rw-r--    1 guitarrapc   guitarrapc    356 Jan 14 16:59 ConsoleApp3.csproj
-rw-rw-r--    1 guitarrapc   guitarrapc    105 Jan 14 16:58 Program.cs
drwxrwxr-x    2 guitarrapc   guitarrapc   4096 Jan 14 17:00 obj

続けて、<RestorePackagesWithLockFile>true</RestorePackagesWithLockFile>を追加して再度リストアします。すると、packages.lock.jsonファイルが生成されます。

$ cat <<EOF > ConsoleApp3.csproj
<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net10.0</TargetFramework>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>

    <RestorePackagesWithLockFile>true</RestorePackagesWithLockFile>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="SkiaSharp.QrCode" Version="0.12.0" />
  </ItemGroup>

</Project>
EOF

$ dotnet restore
Restore complete (0.6s)

Build succeeded in 1.1s

$ ls -la
ls -la
total 24
drwxrwxr-x    3 guitarrapc   guitarrapc   4096 Jan 14 17:03 .
drwxrwxr-x    8 guitarrapc   guitarrapc   4096 Jan 14 16:58 ..
-rw-rw-r--    1 guitarrapc   guitarrapc    427 Jan 14 17:03 ConsoleApp3.csproj
-rw-rw-r--    1 guitarrapc   guitarrapc    105 Jan 14 16:58 Program.cs
drwxrwxr-x    2 guitarrapc   guitarrapc   4096 Jan 14 17:03 obj
-rw-rw-r--    1 guitarrapc   guitarrapc   1314 Jan 14 17:03 packages.lock.json  # <- 追加!

ロックファイルの中身を見ると、プロジェクトで直接参照しているパッケージと、間接的に参照しているパッケージが区別されつつ、各パッケージのバージョンが記録されています。

  • プロジェクトで直接参照させたパッケージSkiaSharp.QrCodeには"type": "Direct"が指定され、最新バージョンが利用
  • SkiaSharp.QrCodeライブラリが依存しているSkiaSharpSkiaSharp.NativeAssets.Win32などのパッケージには"type": "Transitive"が指定
$ cat packages.lock.json
{
  "version": 1,
  "dependencies": {
    "net10.0": {
      "SkiaSharp.QrCode": {
        "type": "Direct",
        "requested": "[0.12.0, )",
        "resolved": "0.12.0",
        "contentHash": "DTSyBl/rJXcGbSuIzkv20pkTTPUaZbFmouWrOtHG0a2Ide0IsbU9o1mUJb1HsiOgUEK6aAX2+MzP0n7GPssiSA==",
        "dependencies": {
          "SkiaSharp": "3.119.1",
          "SkiaSharp.NativeAssets.Win32": "3.119.1",
          "SkiaSharp.NativeAssets.macOS": "3.119.1"
        }
      },
      "SkiaSharp": {
        "type": "Transitive",
        "resolved": "3.119.1",
        "contentHash": "+Ru1BTSZQne3Vp+vbSb50Ke3Nlc3ZnItxx4+751J9WZ8YzLKAV/n+9DAo4zFTyeCI//ueT63c+VybmTTpYBEiw==",
        "dependencies": {
          "SkiaSharp.NativeAssets.Win32": "3.119.1",
          "SkiaSharp.NativeAssets.macOS": "3.119.1"
        }
      },
      "SkiaSharp.NativeAssets.macOS": {
        "type": "Transitive",
        "resolved": "3.119.1",
        "contentHash": "6hR3BdLhApjDxR1bFrJ7/lMydPfI01s3K+3WjIXFUlfC0MFCFCwRzv+JtzIkW9bDXs7XUVQS+6EVf0uzCasnGQ=="
      },
      "SkiaSharp.NativeAssets.Win32": {
        "type": "Transitive",
        "resolved": "3.119.1",
        "contentHash": "8C4GSXVJqSr0y3Tyyv5jz6MJSTVUyYkMjeKrzK+VyZPGLo89MNoUEclVuYahzOCDdtbfXrd2HtxXfDuvoSXrUw=="
      }
    }
  }
}

ロックファイルを使ったリストア

ロックファイルを使用している場合、dotnet restoreコマンドはpackages.lock.jsonファイルを参照して、NuGetの依存を再評価しつつ指定されたバージョンのパッケージをインストールします。この時パッケージが取得できなかったなど必要があれば、ロックファイルのバージョンは更新されます。不変じゃないのはnpmのpackage-lock.jsonと同じです。

npm ciのように、ロックファイルに記録されたバージョンを厳密に再現する場合、dotnet restore --locked-modeコマンドを使うか、<RestoreLockedMode>true</RestoreLockedMode>を設定します。npm同様、CIではこのオプションを有効にするのがいいでしょう。

ローカルでは通常のdotnet restoreを実行し、CI(GitHub Actionsを想定)ではロックファイルに厳密に従うようにするなら次のように設定します。これにより、異なる環境であっても同じバージョンのパッケージが保証されます。

  <PropertyGroup>
    <RestorePackagesWithLockFile>true</RestorePackagesWithLockFile>
    <RestoreLockedMode Condition="'$(CI)' == 'true'">true</RestoreLockedMode>
  </PropertyGroup>

ロックファイルとCentral Package Managementの組み合わせ

Central Package Management(以降CPM)は、複数プロジェクトのパッケージバージョンをDirectory.Packages.propsで一元管理する機能です。ロックファイルとCPMを組み合わせた場合の動作を確認してみましょう。

ロックファイルはCPMが有効でも特別な対応はしません。つまり、Directory.Packages.propsでバージョンを一元管理していても、ロックファイルは個々の.csprojパスに生成されます。実際に試してみます。

ConsoleApp4とConsoleApp5の2つのプロジェクトを持つソリューションを作成し、Directory.Packages.propsSkiaSharp.QrCodeのバージョンを一元管理します。ロックファイルpackages.lock.json、Directory.Packages.propsのパスではなく各プロジェクトに生成されることを確認します。

まずはルートにDirectory.Build.propsDirectory.Packages.propsを作成し、ロックファイルとCentral Package Managementを有効にします。

$ cat <<EOF > Directory.Build.props
<Project>
  <PropertyGroup>
    <RestorePackagesWithLockFile>true</RestorePackagesWithLockFile>
    <RestoreLockedMode Condition="'$(CI)' == 'true'">true</RestoreLockedMode>
  </PropertyGroup>
</Project>
EOF

$ cat <<EOF > Directory.Packages.props
<Project>
  <PropertyGroup>
    <ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
    <CentralPackageTransitivePinningEnabled>true</CentralPackageTransitivePinningEnabled>
  </PropertyGroup>
  <ItemGroup>
    <PackageVersion Include="SkiaSharp.QrCode" Version="0.12.0" />
  </ItemGroup>
</Project>
EOF

続いて、2つのコンソールプロジェクトを作成し、SkiaSharp.QrCodeパッケージを追加してリストアします。ロックファイルはDirectory.Packages.propsではなく各プロジェクトに生成されます。1

$ mkdir -p src/ConsoleApp4 && cd src/ConsoleApp4
$ dotnet new console
$ dotnet package add SkiaSharp.QrCode

$ cd ../../
$ mkdir -p src/ConsoleApp5 && cd src/ConsoleApp5
$ dotnet new console
$ dotnet package add SkiaSharp.QrCode

$ cd ../../
$ dotnet new sln -f slnx
$ dotnet sln add src/ConsoleApp4/ConsoleApp4.csproj
$ dotnet sln add src/ConsoleApp5/ConsoleApp5.csproj
$ dotnet restore
Restore complete (1.4s)

Build succeeded in 1.7s

$ ls -laR
.:
total 20
drwxrwxr-x    3 guitarrapc   guitarrapc   4096 Jan 14 17:41 .
drwxrwxr-x    8 guitarrapc   guitarrapc   4096 Jan 14 16:58 ..
-rw-rw-r--    1 guitarrapc   guitarrapc    210 Jan 14 17:38 Directory.Build.props
-rw-rw-r--    1 guitarrapc   guitarrapc    327 Jan 14 17:39 Directory.Packages.props
-rw-rw-r--    1 guitarrapc   guitarrapc    181 Jan 14 17:43 lockfile.slnx
drwxrwxr-x    4 guitarrapc   guitarrapc      0 Jan 14 17:35 src

./src:
total 12
drwxrwxr-x    4 guitarrapc   guitarrapc      0 Jan 14 17:35 .
drwxrwxr-x    3 guitarrapc   guitarrapc   4096 Jan 14 17:41 ..
drwxrwxr-x    3 guitarrapc   guitarrapc   4096 Jan 14 17:43 ConsoleApp4
drwxrwxr-x    3 guitarrapc   guitarrapc   4096 Jan 14 17:43 ConsoleApp5

./src/ConsoleApp4:
total 20
drwxrwxr-x    3 guitarrapc   guitarrapc   4096 Jan 14 17:43 .
drwxrwxr-x    4 guitarrapc   guitarrapc      0 Jan 14 17:35 ..
-rw-rw-r--    1 guitarrapc   guitarrapc    324 Jan 14 17:36 ConsoleApp4.csproj
-rw-rw-r--    1 guitarrapc   guitarrapc         105 Jan 14 17:35 Program.cs
drwxrwxr-x    2 guitarrapc   guitarrapc   4096 Jan 14 17:43 obj
-rw-rw-r--    1 guitarrapc   guitarrapc     66 Jan 14 17:43 packages.lock.json

./src/ConsoleApp5:
total 20
drwxrwxr-x    3 guitarrapc   guitarrapc   4096 Jan 14 17:43 .
drwxrwxr-x    4 guitarrapc   guitarrapc      0 Jan 14 17:35 ..
-rw-rw-r--    1 guitarrapc   guitarrapc    324 Jan 14 17:36 ConsoleApp5.csproj
-rw-rw-r--    1 guitarrapc   guitarrapc    105 Jan 14 17:36 Program.cs
drwxrwxr-x    2 guitarrapc   guitarrapc   4096 Jan 14 17:43 obj
-rw-rw-r--    1 guitarrapc   guitarrapc     66 Jan 14 17:43 packages.lock.json

CPMなので.csprojファイルの中身を見てもパッケージのバージョン指定はありません。

$ cat ./src/ConsoleApp4/ConsoleApp4.csproj
<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net10.0</TargetFramework>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="SkiaSharp.QrCode" />
  </ItemGroup>

</Project>

$ cat ./src/ConsoleApp4/packages.lock.json
{
  "version": 2,
  "dependencies": {
    "net10.0": {
      "SkiaSharp.QrCode": {
        "type": "Direct",
        "requested": "[0.12.0, )",
        "resolved": "0.12.0",
        "contentHash": "DTSyBl/rJXcGbSuIzkv20pkTTPUaZbFmouWrOtHG0a2Ide0IsbU9o1mUJb1HsiOgUEK6aAX2+MzP0n7GPssiSA==",
        "dependencies": {
          "SkiaSharp": "3.119.1",
          "SkiaSharp.NativeAssets.Win32": "3.119.1",
          "SkiaSharp.NativeAssets.macOS": "3.119.1"
        }
      },
      "SkiaSharp": {
        "type": "Transitive",
        "resolved": "3.119.1",
        "contentHash": "+Ru1BTSZQne3Vp+vbSb50Ke3Nlc3ZnItxx4+751J9WZ8YzLKAV/n+9DAo4zFTyeCI//ueT63c+VybmTTpYBEiw==",
        "dependencies": {
          "SkiaSharp.NativeAssets.Win32": "3.119.1",
          "SkiaSharp.NativeAssets.macOS": "3.119.1"
        }
      },
      "SkiaSharp.NativeAssets.macOS": {
        "type": "Transitive",
        "resolved": "3.119.1",
        "contentHash": "6hR3BdLhApjDxR1bFrJ7/lMydPfI01s3K+3WjIXFUlfC0MFCFCwRzv+JtzIkW9bDXs7XUVQS+6EVf0uzCasnGQ=="
      },
      "SkiaSharp.NativeAssets.Win32": {
        "type": "Transitive",
        "resolved": "3.119.1",
        "contentHash": "8C4GSXVJqSr0y3Tyyv5jz6MJSTVUyYkMjeKrzK+VyZPGLo89MNoUEclVuYahzOCDdtbfXrd2HtxXfDuvoSXrUw=="
      }
    }
  }
}

プロジェクトごとに異なるパッケージを参照することもある2ので挙動としては理解できますが、packages.lock.jsonの役割的にはDirectory.Packages.propsのパスに1つだけ生成される方が自然な気はします。ただ、.csprojでパッケージをオーバーライドする場合もあるので、今の設計のままになりそうです。ソリューションレベルやリポジトリレベルのロックファイルについてIssueも立っていますが、現時点では対応の予定はないようです。

ロックファイルを使っている例

個人的にはロックファイルは使いませんが、ロックファイルが使われる例もあります。例えば、GitHub ActionsでNuGetのパッケージキャッシュを利用するactions/cacheがロックファイルを使ったサンプルを提示しています。サンプルは、ロックファイルをキャッシュキーに含めることで、パッケージの変更があった場合のみキャッシュを更新させます。

- uses: actions/cache@9255dc7a253b0ccc959486e2bca901246202afeb # v5.0.1
  with:
    path: ~/.nuget/packages
    key: ${{ runner.os }}-nuget-${{ hashFiles('**/packages.lock.json') }}
    restore-keys: |
      ${{ runner.os }}-nuget-

ただ、先にあげたようにCentral Package Managementを使っている場合、プロジェクトごとにロックファイルができます。だったら、Directory.Packages.propsでバージョンが1.1.1のように指定されているはずなので、Directory.Packages.props自体をキャッシュキーに含めたほうがより明示的に更新タイミングが分かるのとキャッシュ効率もほぼ変わらないと予測できます。

- uses: actions/cache@9255dc7a253b0ccc959486e2bca901246202afeb # v5.0.1
  with:
    path: ~/.nuget/packages
    key: ${{ runner.os }}-nuget-${{ hashFiles('Directory.Packages.props') }}
    restore-keys: |
      ${{ runner.os }}-nuget-

実際にロックファイルをキャッシュキーにしている記事を見ても、キャッシュによる効果はあまり感じられなかったと書かれています。プロジェクトで利用しているパッケージのボリュームによりますが、GitHub Actionsのキャッシュリストアは早くないので、キャッシュヒット率が上がっても劇的に早くならないのは納得できます。このため私は、GitHub ActionsでNuGetのキャッシュは使っていません。

C#でロックファイルは必要か

本題です。C#でロックファイルは必要なのでしょうか? 個人的にはロックファイルはあまり必要ないと考えています。それは、C#はNuGetのパッケージをバージョン直指定する文化があり、推移的パッケージの解決も「競合した場合最も低いバージョンを選ぶルール」があるため、決定論的にパッケージバージョンが決定されるからです。

実際、GitHubでRestorePackagesWithLockFileをキーに検索すると7300件程度と、C#リポジトリ全体が6.9M件あることからすると少ないです。このことから、C#のプロジェクトでロックファイルを使う文化があまり根付いていないことがわかります。

npmとNuGetの文化の違い

ロックファイルが特に有効なのは、パッケージの依存関係がレンジ指定されている場合です。npmでは、^1.2.3~1.2.3のようにレンジ指定することが一般的です。このため、ロックファイルを使わないと、同じリポジトリをクローンしても、リストアタイミングで異なるパッケージバージョンがインストールされる可能性を持っています。ロックファイルを使うことで、同じバージョンのパッケージを確実にインストールできます。

一方、NuGetの文化としてレンジ指定することがなく、バージョンが直接指定されます。また、推移的な依存パッケージで競合があった場合、最も低いバージョンを選ぶよう解決されるルールです。このためロックファイルがなくとも、.csprojやDirectory.Packages.propsで直接バージョンが指定されている限りは決定論的(deterministic)にバージョンが決定されます。

C#でバージョン直指定なのはNuGetのUI/UXがそうであることに起因してそうです。NuGetにおいては、レンジ指定を維持するよりバージョン指定することを促す体験で一貫しています。

例えば、dotnet package addでパッケージを追加してもバージョンは指定されます。

# バージョン指定を省略した場合、自動的に最新バージョンが指定される
$ dotnet package add SkiaSharp.QrCode

# バージョンを指定することも可能だが、最新バージョンを指定するなら不要
$ dotnet package add SkiaSharp.QrCode --version 0.12.0

Visual StudioやRiderのNuGet Package Managerでパッケージをインストール・アップグレードする際もバージョンを指定するようになっており、レンジ指定をサポートしていません。

Visual StudioのManage NuGet Packageでもバージョンを指定する

npmのようにバージョンをレンジ/ワイルドカード指定をするには.csprojを直接手で編集する必要があり、ほとんどの人は使いません。

直接.csprojの編集が必要

レンジ指定していても、Dependabotで自動更新させるとバージョンは直指定されます。

SBOMの視点から

SBOMの視点から見ると、ロックファイルpackages.lock.jsonはSource SBOMであって補助的な役割に過ぎません。SBOMにおいて最も重要なのはBuild SBOMであり、C#でもビルド時にobj/project.assets.jsonへ出力します。

$ dotnet build -c Release
$ ls -l ./src/ConsoleApp4/obj
total 64
-rw-rw-r--    1 guitarrapc   guitarrapc  22356 Jan 14 17:46 ConsoleApp4.csproj.nuget.dgspec.json
-rw-rw-r--    1 guitarrapc   guitarrapc   1304 Jan 14 17:43 ConsoleApp4.csproj.nuget.g.props
-rw-rw-r--    1 guitarrapc   guitarrapc    150 Jan 14 17:43 ConsoleApp4.csproj.nuget.g.targets
drwxrwxr-x    3 guitarrapc   guitarrapc      0 Jan 14 18:14 Debug
-rw-rw-r--    1 guitarrapc   guitarrapc  28542 Jan 14 17:46 project.assets.json
-rw-rw-r--    1 guitarrapc   guitarrapc    684 Jan 14 17:46 project.nuget.cache

project.assets.jsonファイルには、ビルドに使用されるすべてのパッケージとそのバージョンが含まれています。これにより、SBOMを生成する際により正確な依存関係情報を取得できます3。実際、SBOMツールの[sbom-tool]やsynkCycloneDXはNuGetに対してはproject.assets.jsonを参照しています。

まとめ

C#においてロックファイルはデフォルトで無効になっており、実際に使われている例もあまり見かけません。個人的には、パッケージをバージョン直指定する文化と、決定論的なバージョン解決の仕組みにより、ロックファイルを使うメリットは小さいと考えています。今後ソフトウェアサプライチェインのセキュリティがより重要になる中で、ロックファイルの役割も見直される可能性はあります。しかし現時点では、C#のエコシステムにおいて決定論的な保証ができないケースを思いつきません。

ただし、以下のようなケースでは検討の余地があります。

  • 推移的な依存関係がどう変わったかを細かく追いかけたい場合
  • CIでのキャッシュ戦略として活用する場合(ただし、Central Package Management使用時はDirectory.Packages.propsで十分)

参考

ドキュメント

ブログ

GitHub


  1. .NET SDK 10.0.102以降でdotnet new sln -f slnxが利用可能です
  2. CPMを使っていてプロジェクトでバージョンをオーバーライドすると、気づくことが難しいこともあり私は極力避けたほうがいいと考えています
  3. 他のファイルも組み合わせますが、ビルド時に入るファイル一覧として重要