いしてっく

C# や .NET に関する話題を中心に

C# Dojo 001: `Index` 構造体の基本 (問題編)

はじめに

C# Dojo シリーズでは、 C# のさまざまな機能の理解を深めることを目的に、基本的な練習問題と解説を提供していきたいと考えています。 いつまで続くかわかりませんが、ご活用いただけますと嬉しいです! しばらくは Index, Range, Span<T> まわりの話題が中心になると思います。

初回の今回は、 Index 構造体の基本を紹介します。解答は後日別の記事に投稿します。

今回のお題

与えられた配列の末尾から数えて n 個目の要素を取り出す処理を書いてください(配列の最後の要素を「末尾から数えて 1 個目」とします)。

メソッド定義の例:

int GetFromEnd(int[] array, int indexFromLast) { /* ... */ }

入力例: array = [1, 2, 3, 4, 5, 6], indexFromLast = 3
出力例: 4 // 末尾から数えて 3 つめ

基礎知識

Index 構造体を使うと、配列や文字列、 Span<T> 内の「位置」を表すことができます。 Index には、 2 種類の使い方があります。

  • 先頭からの位置を表す
  • 末尾からの位置を表す

配列のインデックスを復習しよう

先頭からの位置だけでなく、末尾からの位置を混乱なく扱うために、配列の「インデックス」が何を意味しているのかを復習しましょう。

Index 構造体登場以前から、C# の配列では、 a[1] のようにして、整数を指定して特定の位置の要素にアクセスできました。 この 1 は「配列の先頭からの距離が 1」ということを表しています。つまり、 a[1] は先頭からの距離が 1 の地点から始まる要素、を表しているのです。

インデックスというのはあくまで「距離」を表すものであり、各要素に対して直接割り当てられている番号ではないことに注意しておきましょう。

(先頭からのインデックスを考えている限りはどちらの考え方でも問題なくインデックスを扱うことができます。このあと末尾からのインデックスを考えるときに両者の違いがはっきりするでしょう。)

Start-relative indexing: 先頭からの位置を表す

これは「ふつうの」インデックスです。最初の要素を取るには a[0] のようにして、次の要素は a[1] 、その次は a[2]... というふうになります。

つまり、「先頭からの距離」を指定する方法が Start-relative indexing です。

この場合、 Index 構造体を使った位置指定の方法は以下のようになります。

int a = { 1, 2, 3, 4, 5 };

Index index = 1; // 先頭からの距離が 1 の場所を表すインデックス
int second = a[index]; // 先頭からの距離が 1 の要素 (= 2) を取り出す

Index 構造体を使わずに、以下のように書いても同じことです(こちらのほうが馴染み深いでしょう)。

int second = a[1];

End-relative indexing: 末尾からの位置を表す

こちらは見慣れない方も多いかもしれません。 End-relative indexing では、末尾からの距離を使って位置を表現します。

Start-relative indexing では先頭からの距離をそのまま書いていたのですが、 End-relative indexing では、 ^1 のように距離に ^ をつけて表現します。 ^1 というのは、「末尾からの距離が 1 の位置」ということになります。

int a = { 1, 2, 3, 4, 5 };

Index index = ^1; // 末尾からの距離が 1 の場所を表すインデックス
int second = a[index]; // 末尾からの距離が 1 の要素 (= 5) つまり、最後の要素を取り出す

Index 構造体を明示的に使わずに、以下のように書くこともできます。

int second = a[^1]; 

このとき注意してほしいのが、 一番最後の要素は a[^0] ではなく、 a[^1] になるという点です。

先頭の要素が a[0] で、末尾の要素が a[^1] というのは奇妙に思える方は、インデックスが「距離」を表しているということを念頭にもう一度考えてみましょう。

End-relative indexing では、「配列の末尾」からの距離を表します。ここで、 ^0 は配列の終わりを表します。

^0 は配列の終わりですから、 ^0 の部分になにか要素が格納されているということはありません。最後の要素は ^1 の地点から始まっているのです。

Index の例

  • 先頭からの距離が 3
int[] a = {1, 2, 3, 4, 5};
int element = a[3]; // 4
  • 末尾からの距離が 3
int[] a = {1, 2, 3, 4, 5};
int element = a[^3]; // 3

攻撃者の標的となった NuGet との付き合い方

パッケージレジストリに対するサプライチェーン攻撃の話は npm や Python 界隈ではよく目にするが、われわれ .NET ユーザにとってはどこか他の世界の話だったかもしれない。しかし、最近は攻撃者の目が NuGet にも向けられているようだ。

不正なパッケージに弱い NuGet

これまで NuGet 上の悪意のあるパッケージの存在や、パッケージに深刻な脆弱性が含まれている可能性には、あまり目を向けてこなかった人も多いかもしれない。わたしのチームではフロントエンドに TypeScript, バックエンドに C# というような構成にしているが、 npm パッケージについては(確認しきれないほどの) security alert が頻繁に発生するのに対し、 NuGet パッケージに対する alert は目にする機会が少ない。もちろん JavaScript には prototype pollution という特有の問題があるし、npm はパッケージが細かく依存関係が複雑になりがちであるので NuGet と単純な比較はできないが、この事実はこれまで NuGet が攻撃者の標的にされにくかったということも示唆している気がする。

NuGet が攻撃に対して脆弱であることの一例として、 init.ps1 の問題がある。 VisualStudio で NuGet パッケージをインストールしたとき、 NuGet パッケージ内に tools/init.ps1 というスクリプトが含まれていれば、VisualStudio は自動的にこのスクリプトを実行する。JFrog によれば、2023年3月にはこの仕組みを悪用して不正な実行ファイルをダウンロードさせる攻撃が発生したようだ

利用者としての NuGet パッケージとの付き合いかた

init.ps1 の問題がなかったとしても、 NuGet パッケージへの依存はもっと慎重に扱うべきかもしれない。現在はさまざまな NuGet パッケージが提供されており、非常に便利なものも多いし、これらのパッケージなしで効率的に仕事を進めることは困難だろう。しかし NuGet パッケージを使うということは、そのパッケージの開発者がわたしたちのアプリケーションに対して直接的に関与でき、任意のコードを潜り込ませることができるということであり、それが攻撃者にとって絶好の機会を与える可能性もある。

NuGet パッケージを安全に使用するために、以下のようなことを検討しても良いかもしれない。

よくわからないパッケージをインストールしない

まず基本中の基本としてはこれである。 開発者の中にはパッケージ ID にそれっぽい名前がついていたりいい感じの機能を提供しているように見えると、すぐにパッケージをインストールしてしまう人も多い。しかしこのような行動には多くのリスクが伴うことを忘れないでほしい。もしパッケージに悪意のあるコードが含まれていたら?深刻な脆弱性があったら?深刻なバグが放置されているかも?など、考えればリスクはいくらでも思いつくものである。見に覚えのある人は、以下の項目も参考にしてほしい。

typosquatting 攻撃に注意する

不正なパッケージの中には正規のパッケージと酷似したパッケージ ID を使用しているものがある。たとえば、上記の JFrog の記事で紹介されていたものとしては Asip.Net.Core などがあった。このような場合にパッケージ ID のみで正規のものか判断するのは困難なことがあり、他の項目も含めて慎重に扱う必要がある。

パッケージ名をコピペして検索エンジンに入力し、解説記事を探してみる

これは会社の同僚に教えてもらった方法であるが、シンプルで手間がかからないにもかかわらず非常に強力な対策となりうる。そもそも解説記事が全然見つからないようなパッケージはあまり広く利用されていないだろうし、セキュリティの問題を度外視しても情報が手に入りにくいものはチームとして使い続けていくのは難しいだろう。

メンテナンスが停止したパッケージは慎重に扱う

いくら便利であってもメンテナンスが数年前に放棄されているようなパッケージをそのまま参照するのはリスクが大きいと思う。まずこのようなパッケージは深刻なバグや脆弱性を発見しても修正してもらえる可能性は限りなく 0 に近い。そのようなパッケージに依存することは、アプリケーションにとって深刻な脅威となりうる。また、開発者が GitHub や NuGet のアカウント自体の管理を放棄している場合には、そのアカウントが乗っ取られることで不正なコードが仕込まれる可能性も考慮しなければならない。 なにをもって「メンテナンスされていない」とみなすかは微妙なところもあると思うが、個人的には以下のようなパッケージに対しては導入に対してきわめて慎重になることが多い。

  • contributor が一人だけであるか、きわめて少ない
    • 今後メンテナンスされなくなる可能性が高い
  • ソースコードが公開されていない
  • 最後のコミットまたはリリースが 3 年以上前である

このようなパッケージについては、代替を探す、自作する、fork して使うなどいくらでも方法はあるのでより安全な方法をとるようにしたい。

インストール前にパッケージのページの NuGetパッケージエクスプローラで、疑わしいファイルが含まれていないかパッケージを検査する

これは前述の JFrog のブログ記事で紹介されていた方法である。 nuget.org の各パッケージのページにはパッケージエクスプローラへのリンクがあり、これを使うと NuGet パッケージのファイル構成を確認できるようだ。これによって、悪意のある init.ps1 などが含まれていないか確認することが望ましい。

ダウンロード数やバージョン数を確認する

これも JFrog で紹介されていた方法である。長期間にわたって複数のバージョンがリリースされており、それぞれダウンロード数がそれなりにあれば、開発が活発で広く使われている安定したパッケージである可能性がある。ただし、ダウンロード数はいくらでもかさ増しできるため、これだけを理由にパッケージを信頼するのは危険である。

パッケージ名の横のチェックマークを確認する

NuGet には パッケージIDプレフィックスの予約 という機能がある。この機能を使うと、パッケージの所有者がパッケージ ID のプレフィックスを「予約」しておき、他の人にそのプレフィックスが使われないようにしたり、公式のパッケージとサードパーティのものを区別できるようにすることができる。

たとえば、 xunit はパッケージ ID プレフィックスを予約しており、 NuGet のページにはチェックマークが表示されている。しかし、xunit.categories というパッケージは xunit の所有者とはまったくの別人がリリースしているものであり、このパッケージ ID プレフィックスの委任を受けておらず、チェックマークが表示されていない。

このようにして、チェックマークの有無によってパッケージIDプレフィックスの所有者がリリースしたものであるのか、たまたまパッケージ ID プレフィックスが同じだけで全く無関係の人物がリリースしたものであるのかをある程度見分けることができる。

パッケージ ID プレフィックスの予約はすべての OSS 開発者が実施しているものではなく、またこのチェックマークの有無によってそのパッケージを信頼できるかどうかを決定できるわけではないが、ひとつの指標としては機能するように思う。

さいごに

NuGet パッケージを慎重に扱うことは、セキュリティだけでなくさまざまなリスクに対応するために重要である。 NuGet ユーザとして、この機会にパッケージの利用方法を見直してみるのも良いのではないか。 NuGet を安全に使うためのプラクティスが研究されていけば、.NET をより安心して広く利用してもらい、エコシステムがさらに発展することにも寄与すると思う。

Polyglot Notebooks を試すメモ

C# でちょっとしたコードの確認とか実行に使えるものって、何があるのだろう。 以前 Jupyter を使って C# のノートブックを作ったことがあるが、あまり体験が良くなく、すぐに LINQPad で大量のタブと格闘する日々に戻ってしまった。

この間、Polyglot Notebooks という VS Code拡張機能を見つけた。これは .NET Interactive をベースとした多言語対応のノートブックを提供するものだとのこと。Microsoft が作っており、 C#, F#, JavaScript, PowerShell, SQL, KQL, HTML, Mermaid をサポートするとのことで、 C# のサポートに期待できる。これは入れてみるしかないとのことで、これからしばらく触ってみることにする。

インストール

.NET 7.0 SDKVS Code はすでにインストール済みである。

2.2.207 [C:\Program Files\dotnet\sdk]
3.1.426 [C:\Program Files\dotnet\sdk]
6.0.400 [C:\Program Files\dotnet\sdk]
6.0.407 [C:\Program Files\dotnet\sdk]
7.0.100-rc.1.22431.12 [C:\Program Files\dotnet\sdk]
7.0.104 [C:\Program Files\dotnet\sdk]
7.0.202 [C:\Program Files\dotnet\sdk]
7.0.300-preview.23122.5 [C:\Program Files\dotnet\sdk]

Marketplace の Polyglot Notebooks のページ からインストールしてみた。

コマンドパレットから Polyglot Notebook: Create new blank notebook を実行し、言語で C# を選択すると、ノートブックが起動した。

いろいろ試す

stackalloc

stackalloc を使用したコードをよく書くので、これが動かないとどうにもならない。

残念。ノートブックの性質上、ノートブックにそのまま書いた変数はフィールドになっているのかも。

では、メソッド内で使えばどうか。

動いた。このあたりは予想通り。

GitHub Copilot

GitHub Copilot を有効にしている場合はどのような挙動になるか。

Copilot もきちんと動作するようだ。候補が出てきたら、 Tab キーを押せば入力される。 ノートブックで Copilot が使えると生産性が爆上がりしそうだ。

NuGet パッケージ

NuGet パッケージを使う場合、 #r nuget magic command を使うと良いようだ。

#r "nuget: MessagePack"

using MessagePack;

var bytes = MessagePackSerializer.Serialize(new[] { 1, 2, 3 });

詳細は Working with NuGet packages に記載がある。

おわりに

慣れれば結構便利に使えそうである。もう少し試したら追記していきたい。