dotnet

Activity

Stars and mentions · Last 30 days, Oct 7 – Nov 5

GitHub stars931
Mentions119
014284256Oct 7Oct 21Nov 5
UTC days · repositories dotnet owns56 repositories

Top repositories

dotnet’s repositories ranked by new forks, as on trending.

Loading

Contributes to

Repositories outside dotnet’s account where dotnet opened or reviewed pull requests in the past 365 days.

Who noticed dotnet

Everyone who starred or forked dotnet’s repositories since the archive began in 2011.

Notable accounts

Ordered by the stars their own repositories have received.

Biggest fans

Accounts that starred the most of these repositories.

Breakout weeks

Each repository’s best week of new stars, with the release published around it.

Mentions

119posts
Nov 61 post
  1. 【.NET/Unity】C# の型制約とボックス化をハックする — IL と手動スタック配置 ! この記事は、C# とランタイムの挙動を調べるための実験です。unsafe な操作やランタイム内部の構造に依存する実装を含みます。手動でボックスをスタックに置く方法は、実用を推奨しません。 はじめに 最近(2026 年 11 月 6 日現在)、Omochaya さんの 「【C# / Unity】型制約なしゼロアロケでDisposeする方法」 を読みました。 型制約のない T に対して、ボックスを再利用して Dispose を呼ぶ方法です。Unsafe.Unbox<T> でボックスの中身を置き換え、呼び出し後の変更を元の変数に書き戻します。 以前 .NET で、IL(中間言語)を書いて型制約なしで Dispose を呼んだことがあります。オブジェクトを手動でスタックに置く実験もしていて、元記事を読んだときに、このボックスもスタックに置けるのではと思いました。 そこで、型制約なしの IL 呼び出しを .NET と Unity で改めて試し、ボックスをスタックに置く方法と比べてみました。呼び出しごとの割り当てを省きながら、Dispose による変更も残せるかを調べます。 実験コードと再実行手順は、リポジトリにまとめています。 Akeit0/csharp-boxing-research Windows x64 で、.NET 10 / 11 RC の CoreCLR・Native AOT と、Unity 6.3 / Unity 7 alpha の Development Player を使いました。 IL で型制約なしに Dispose を呼ぶ C# では、where T : IDisposable があれば ref T から Dispose を呼べます。 static void Dispose<T>(ref T value) where T : IDisposable => value.Dispose(); この呼び出しに使われるのが constrained. 命令です。構造体自身が Dispose を実装していれば、ボックスを作らず、元の値への参照を渡して呼べます。命令の規則 では、IL を自分で書いて where T : IDisposable を外したらどうでしょうか。 InlineIL.Fody で書く InlineIL.Fody で、ビルド時に IL を埋め込みます。 using System; using System.Runtime.CompilerServices; using InlineIL; using static InlineIL.IL.Emit; public static class IlCalls { [MethodImpl(MethodImplOptions.NoInlining)] public static void Dispose<T>(ref T value) { Ldarg(nameof(value)); Constrained(typeof(T)); Callvirt(new MethodRef(typeof(IDisposable), nameof(IDisposable.Dispose))); } } 出力される本体は次の 4 命令です。メタデータにも型制約はありません。 ldarg.0 constrained. !!T callvirt instance void System.IDisposable::Dispose() ret この Dispose に構造体を渡してみます。 Counter value = new() { Count = 41 }; IlCalls.Dispose(ref value); // value.Count == 42 public struct Counter : IDisposable { public int Count; [MethodImpl(MethodImplOptions.NoInlining)] public void Dispose() => Count++; } 元の変数の Count が 41 から 42 になりました。.NET 10 / 11 RC の CoreCLR・Native AOT と、今回の Unity の全バックエンドで動いています。 .NET 10 の JIT コンパイラーが生成した機械語は Counter.Dispose への直接の call でした。IL2CPP の C++ も Counter* をそのまま渡しています。1 実装を判定してから呼ぶ 型制約を外したので、IDisposable を実装しない型も渡せてしまいます。試しに int を直接渡すと、.NET 10 / 11 RC の CoreCLR は EntryPointNotFoundException、Native AOT はプロセス終了(0xC0000602)になりました。2 呼び出す前に T が IDisposable を実装しているか確かめるのが、TryDispose です。 static class Traits<T> { internal static readonly bool IsDisposable = typeof(IDisposable).IsAssignableFrom(typeof(T)); } public static void TryDispose<T>(ref T value) { if (Traits<T>.IsDisposable) Dispose(ref value); } 判定結果は T ごとにキャッシュします。TryDispose(ref value) に Counter を渡すと Dispose を呼び、int を渡すと何もしません。 IL の実装 型制約はどこでチェックされるか 呼び出し先に where T : IDisposable が付いている場合も試しました。制約のない T で IL から呼ぶ場合と、Reflection で型引数を指定する場合を比べます。Reflection では MakeGenericMethod でメソッドを作り、Invoke で呼び出します。 操作 CoreCLR(.NET / Unity 7) Native AOT(.NET 10 / 11 RC) Unity 6.3 Mono Unity 6.3 / 7 IL2CPP 外側の無制約 T を制約付きメソッドへ渡し、Counter で実行 VerificationException 成功 成功 成功 IDisposable 制約付きで定数 123 を返すメソッドへ、int を IL で渡す VerificationException 123 123 123 同じ定数メソッドを Reflection で int にして呼ぶ ArgumentException ArgumentException ArgumentException 123 制約付き Dispose を MakeGenericMethod で int にする ArgumentException ArgumentException ArgumentException 作成を受理 その Dispose<int> を Invoke する 作成時に拒否 作成時に拒否 作成時に拒否 アクセス違反 0xC0000005 CoreCLR では、実際に渡した Counter が制約を満たしていても、呼び出し側の T に制約がないため拒否されました。 制約の比較コード / 環境ごとの実測ログ ボックス化とスタック配置 IL では、元の値への参照を渡して Dispose を呼びました。インターフェイス型に変換して呼ぶ場合も、変更を残しながらヒープへの割り当てを省けるでしょうか。 C# のキャストでボックス化が消えるとき 型制約のない T を ref で受け取って Dispose を呼ぶなら、C# でもこう書けます。 if (value is IDisposable) ((IDisposable)value).Dispose(); Counter を渡すと、この書き方は 0 B/回でした。if (value is IDisposable disposable) でパターン変数から呼ぶ形は 24 B/回です。呼び出し側と Counter.Dispose に NoInlining を付けて測った結果で、.NET 10 / 11 RC の CoreCLR と Native AOT で共通でした。測定ログ パターン変数を使う形では、型判定に使ったボックスで Dispose を呼びます。上のコードは、キャストで作るボックスを呼び出しにしか使いません。CoreCLR の JIT は、この形で呼び出し先が分かるとボックス化を省けます。今回の生成コードは、スタック上に Counter のコピーを置き、そのアドレスを Counter.Dispose に直接渡していました。JIT の実装 / 生成コード Dispose を呼ぶだけでよければ、この書き方で割り当てを省けます。元記事のように Dispose による変更を元の変数に書き戻す形では、ボックス化による割り当てが残ってしまいます。比較コード ボックスをスタック上に置く 最初から構造体の値をボックスの中に置いておけば、Dispose が書き換えた値をそのまま使えます。そこで、呼び出しの間だけ使うボックスをスタック上に組み立て、object やインターフェイス型の参照として渡してみます。3 ヘッダーと値本体の位置 ボックスを組み立てるには、ランタイムごとの配置を再現します。Windows x64 で object 参照の位置を 0 B とすると、ヘッダーと値本体は次の位置にあります。 ランタイム -8 B +0 B(object 参照の位置) +8 B +16 B CoreCLR / Native AOT 同期用ヘッダー MethodTable* 値本体 … Mono — MonoVTable* 同期用ポインター 値本体 IL2CPP — Il2CppClass* monitor* 値本体 MethodTable* などは型情報へのポインターです。IL2CPP の配置は、Editor に付属する Editor/Data/il2cpp/libil2cpp/il2cpp-object-internals.h の Il2CppObject で確認しました。CoreCLR の object.h / Native AOT の ObjectLayout.h / Mono の object.h StackBox を作る どの環境も、値の手前に型情報と同期用の領域が 1 ワードずつあります。そこで Value の前に 2 ワード分の領域を取ります。 [StructLayout(LayoutKind.Sequential)] public struct StackBox<T> where T : struct { public IntPtr Prefix0; public IntPtr Prefix1; public T Value; } CoreCLR はジェネリック型の Explicit layout を受け付けないため、Sequential にしています。ヘッダーの位置は Value の実際のアドレスから計算します。関連 issue object 参照を作る 型情報と同期用の 0 を書き込んだら、型情報の位置を指すアドレスを object 参照として扱います。これを行うのが StackBox.AsObject です。 StackBox<Counter> storage = default; storage.Value.Count = 41; object receiver = StackBox.AsObject(ref storage, StackBoxRuntime.CoreClr); ((IDisposable)receiver).Dispose(); Console.WriteLine(storage.Value.Count); // 42 Dispose は storage.Value を直接書き換えます。作った参照は、storage を置いたスタックフレームの中で使います。 インターフェイスの既定実装がボックスへの参照を保存する場合 インターフェイスの既定実装をボックス経由で呼ぶと、this はボックス自身を指します。既定実装が this をフィールドに保存すると、呼び出し後もその参照が残ります。 元記事のようにボックスを使い回す場合、保存した参照から再利用中の値も読めてしまいます。手動でスタックに置いたボックスでは、スタックフレームを抜けた時点で参照が無効になります。こうした実装は通常ほとんどありませんが、ボックスをこのように使う場合は this を外部に保存するべきではありません。 既定実装での this の使い方を解析し、条件を満たす呼び出しでボックス化を省く案も出ています。dotnet/runtime #126501 StackBox.AsObject の実装 MonoHeader<T> と Il2CppHeader<T> による型情報の取得は、次の節で説明します。 public static unsafe object AsObject<T>(ref StackBox<T> storage, StackBoxRuntime runtime = StackBoxRuntime.CoreClr) where T : struct { IntPtr typeWord = runtime switch { StackBoxRuntime.CoreClr => typeof(T).TypeHandle.Value, StackBoxRuntime.Mono => MonoHeader<T>.TypeWord, StackBoxRuntime.Il2Cpp => Il2CppHeader<T>.TypeWord, _ => throw new ArgumentOutOfRangeException(nameof(runtime)) }; byte* start = (byte*)Unsafe.AsPointer(ref storage.Prefix0); byte* payload = (byte*)Unsafe.AsPointer(ref storage.Value); if (payload - start < 2 * IntPtr.Size) throw new NotSupportedException("Insufficient prefix before the payload."); IntPtr* header = (IntPtr*)(payload - 2 * IntPtr.Size); bool coreClr = runtime == StackBoxRuntime.CoreClr; header[0] = coreClr ? IntPtr.Zero : typeWord; header[1] = coreClr ? typeWord : IntPtr.Zero; IntPtr raw = (IntPtr)(coreClr ? header + 1 : header); return Unsafe.As<IntPtr, object>(ref raw); } CoreCLR はヘッダーの 2 ワード目、Mono / IL2CPP は 1 ワード目を指す参照を返しています。StackBox の全体 ボックスを作らずに型情報を得る ヘッダーに書き込む型情報は、default(T) をボックス化して先頭ワードを読めば得られます。ここでは、そのためのボックスを作らずに TypeHandle から取得します。 CoreCLR / Native AOT では、値型の typeof(T).TypeHandle.Value をそのままヘッダーに書けます。Mono / IL2CPP では、ハンドルが指す型情報をランタイムの関数で変換します。Native AOT の RuntimeTypeHandle の実装 ランタイム TypeHandle の参照先 ボックス先頭に必要なもの CoreCLR / Native AOT MethodTable MethodTable* Mono MonoType MonoVTable* IL2CPP Il2CppType Il2CppClass* Mono の型情報を取得する Mono では MonoType* からクラスを取得し、そのクラスの vtable を引きます。vtable は domain ごとに作られるので、mono_domain_get() で現在の domain を渡します。 static IntPtr MonoVTable(RuntimeTypeHandle type) { IntPtr klass = mono_class_from_mono_type(type.Value); return mono_class_vtable(mono_domain_get(), klass); } 今回の Windows Player では、これらの関数を mono-2.0-bdwgc.dll から P/Invoke で呼べました。Mono の関数宣言 / vtable の取得・作成処理 IL2CPP の型情報を取得する IL2CPP は il2cpp_class_from_type で Il2CppType* から Il2CppClass* に変換できます。 [DllImport("GameAssembly", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] static extern IntPtr il2cpp_class_from_type(IntPtr type); この関数は GameAssembly.dll に公開されていました。Editor に付属する libil2cpp/il2cpp-api.cpp を見ると、Class::FromIl2CppType を呼んでいます。通常の値型と、型引数を指定したジェネリック型の変換もここで処理されます。 P/Invoke を含む実装 ldtoken から直接ハンドルを返す 変換に渡すハンドルを typeof(T).TypeHandle で取得すると、IL は ldtoken で得たハンドルをいったん Type.GetTypeFromHandle に渡し、返ってきた Type からハンドルを取り出します。Unity 6.3 IL2CPP ではこの処理が生成コードに残り、比較した 2 つの構造体で初回に割り当てが発生しました。 InlineIL.Fody を使えば、ldtoken で得たハンドルをそのまま返せます。 [MethodImpl(MethodImplOptions.AggressiveInlining)] public static RuntimeTypeHandle TypeHandle<T>() { Ldtoken(typeof(T)); return IL.Return<RuntimeTypeHandle>(); } ここに書いた typeof(T) は、InlineIL.Fody に型を指定するためのものです。ビルド後は次の 2 命令になります。 ldtoken !!T ret これで Unity 6.3 IL2CPP の初回の割り当てはなくなりました。C++ の生成コードでも Type.GetTypeFromHandle を経由せず、型のハンドルを返しています。4 Mono / IL2CPP では、こうして取得した型情報を T ごとにキャッシュします。型情報のキャッシュ 実験結果 型制約なしの TryDispose も手動で組み立てた StackBox<T> も、ウォームアップ後は今回のすべての環境で割り当てなしに Dispose を呼べました。 表は 1,000 回のウォームアップ後に 100,000 回実行し、1 回あたりの割り当て量を求めた結果です。StackBox<T> はボックスの組み立てから Dispose 後の値の読み出しまでを測りました。Counter と、参照型フィールドを持つ Payload で結果は同じでした。 方法 CoreCLR / Native AOT Unity 6.3 Mono Unity 6.3 / 7 IL2CPP 毎回ボックス化して外部に保持 24 B 20 B 20 B C# の制約付き呼び出し 0 B 0 B 0 B ボックスのキャッシュを再利用 0 B 0 B 0 B 型制約なしの IL 呼び出し 0 B 0 B 0 B TryDispose(型判定をキャッシュ) 0 B 0 B 0 B StackBox<T> の組み立てと呼び出し 0 B 0 B 0 B CoreCLR / Native AOT 列には、.NET 10 / 11 RC と Unity 7 CoreCLR の結果をまとめています。割り当て量はこの列では GC.GetAllocatedBytesForCurrentThread() で、Mono / IL2CPP では ProfilerRecorder の GC Allocated In Frame を同じフレーム内で読んだ差分で求めました。計測条件 / 実測ログ まとめ 今回試した .NET と Unity では、ボックスをプールせずに Dispose の変更を残せました。どちらの方法でも、ウォームアップ後の呼び出しでは割り当てがありませんでした。 手動のスタック配置では、ヒープにボックスを用意せずにインターフェイス型の参照を渡せました。Dispose による変更は StackBox<T>.Value に残ります。そのためには、ランタイムごとのヘッダー配置に合わせてボックスを組み立て、参照の寿命も自分で管理する必要があります。 元記事のように、すでにある変数を ref T で受け取るなら、値をその場で更新できる IL のほうが用途に合います。対象のランタイムで動作を確認できるなら、ボックスのプールも書き戻しも省けるので、実用の候補になりそうです。 脚注 生成コードは .NET 10 の JIT 出力 と Unity 6.3 の C++ にあります。 ↩︎ 制約なしの Dispose<int> を直接呼んだ CoreCLR のログと Native AOT のログ。0xC0000602 は STATUS_FAIL_FAST_EXCEPTIONです。この呼び出しは Unity では計測していません。 ↩︎ .NET 9 では、参照が外に出ないと判断できる一部のケースで、ボックスをスタックに置く最適化も入りました。JIT の対応 PR / Native AOT の対応 PR ↩︎ 比較用の typeof 経由の測定、直接取得した IL、Unity 6.3 の生成コード。 ↩︎

  2. 【.NET 11】SafeFileHandleの新規APIでハマった点について はじめに .NET 11 RC2やRC3などがリリースされて、もうすぐ.NET 11の正式リリースが行われる時期になりました。 そしてふと 改めて.NET 11 の新規 API を眺めていると、低レベル I/Oの機能が色々入っています。その中でハンドル関連のAPIについて、色々はまった点も含めて調べた内容に記載しておきます。 learn.microsoft.com 確認環境 .NET 11 RC3 ランタイムで確認しています。 バージョン > dotnet --version 11.0.100-rc.3.26522.148 匿名パイプの作成 SafeFileHandle.CreateAnonymousPipe 書き込みと読み込みが接続された匿名パイプとしてSafeFileHandleを作成することができるSafeFileHandle.CreateAnonymousPipeが追加されます。 // 書き込みと読み込みが接続された匿名パイプを作成する。 SafeFileHandle.CreateAnonymousPipe(out SafeFileHandle readHandle, out SafeFileHandle writeHandle); ! SafeFileHandle.CreateAnonymousPipeで作成されるパイプについて、引数であるasyncReadとasyncWriteの値によって内部で作成されるパイプが変わります。1 両方falseつまり同期だと、WindowsではCreatePipe関数による匿名パイプ、それ以外つまり非同期だと、CreateNamedPipeA関数による名前付きパイプが内部では作成されます。 これは、CreatePipe関数の匿名パイプでは非同期IOがサポートされていないためです。23 そして、.NET 11からProcessStartInfoクラスに標準出力/出力/エラーをSafeFileHandleで設定できるStandardOutputHandle/StandardErrorHandle/StandardInputHandleプロパティが追加されています。 これを組み合わせることで、ls | grep XXXのようなパイプラインをSafeFileHandleを用いて直接実装することができます。.NET 10やそれ以前でも実装自体はもちろんできますが、より低レベルなAPIを使うなどちょっと面倒でした。(もしかすると簡単な方法があったかもしれませんが...。) .NET 11からProcessクラスなどでも色々と(待望の!)非同期APIが入ったりなどしているので、どんなものが入るのかは、以下の公式ブログなどを読むことをお勧めします。 devblogs.microsoft.com 公式ブログでも記載されているls | grep XXXの出力結果をファイルに出力するサンプルコードがあるので、これをベースにWindows用にdir | findstr csの出力結果を文字列として取得するサンプルコードが以下になります。 SafeFileHandle.CreateAnonymousPipeのサンプルコード // ここは好きなフォルダ/ディレクトリで問題ない。 string projectDirectory = typeof(Program).Assembly .GetCustomAttributes<AssemblyMetadataAttribute>() .FirstOrDefault(a => a.Key == "ProjectDirectory")?.Value ?? Environment.CurrentDirectory; // 書き込みと読み込みが接続された匿名パイプを作成する。 SafeFileHandle.CreateAnonymousPipe(out SafeFileHandle readHandle, out SafeFileHandle writeHandle); // dir | findstr cs のパイプラインを再現する。 Process producer, consumer; // SafeFileHandle.CreateAnonymousPipeで作成したパイプはここで破棄する(重要) using (readHandle) using (writeHandle) { producer = Process.Start(new ProcessStartInfo("cmd", "/c dir") { WorkingDirectory = projectDirectory, StandardOutputHandle = writeHandle, StandardErrorHandle = writeHandle, })!; consumer = Process.Start(new ProcessStartInfo("findstr", "cs") { StandardInputHandle = readHandle, RedirectStandardOutput = true, RedirectStandardError = true, })!; } using (producer) using (consumer) { var (standardOutput, standardError) = await consumer.ReadAllTextAsync(); await Task.WhenAll(producer.WaitForExitAsync(), consumer.WaitForExitAsync()); Console.Write(standardOutput); // 標準出力結果 Console.Write(standardError); // 標準エラー出力結果 } サンプル結果例(実行環境で変わります) 2026/11/03 火 14:13 691 ConsoleApp.csproj 2026/11/04 水 21:02 6,360 Program.cs ここでは、実行プロジェクトのファイル名でcsが含まれているものを抽出する処理を実行しています。 サンプルコードを見てもらって分かる通り、SafeFileHandle.CreateAnonymousPipeで作成した匿名パイプであるSafeFileHandleを各ProcessStartInfoのStandardOutputHandle/StandardErrorHandle/StandardInputHandleプロパティに設定することで、C#/.NET上でパイプ的なことができるようになりました。 ただ、注意事項としてSafeFileHandle.CreateAnonymousPipeで作成したSafeFileHandleの破棄をちゃんとしないとデッドロックを引き起こすという点です。 最初、私は動作確認として以下のようなソースコードを実装していましたが、var (standardOutput, standardError) = await consumer.ReadAllTextAsync();の部分でデッドロックして、少し悩みました。 失敗例 SafeFileHandle.CreateAnonymousPipe(out SafeFileHandle readHandle, out SafeFileHandle writeHandle); using (readHandle) using (writeHandle) { producer = Process.Start(new ProcessStartInfo("cmd", "/c dir") { WorkingDirectory = projectDirectory, StandardOutputHandle = writeHandle, StandardErrorHandle = writeHandle, })!; consumer = Process.Start(new ProcessStartInfo("findstr", "cs") { StandardInputHandle = readHandle, RedirectStandardOutput = true, RedirectStandardError = true, })!; // ここで止まる...なぜ? var (standardOutput, standardError) = await consumer.ReadAllTextAsync(); await Task.WhenAll(producer.WaitForExitAsync(), consumer.WaitForExitAsync()); Console.Write(standardOutput); // 標準出力結果 Console.Write(standardError); // 標準エラー出力結果 } そして公式ブログのサンプルコードをちゃんと見ると以下のコメントがありました。 The producer process has its own copy of the write end of the pipe, we need to dispose the parent copy producer プロセスはパイプの書き込み側の自分用のコピーを持っているので、親側のコピーは破棄する必要があります (この部分はOpus 5.5による翻訳です) ということで、破棄する必要があるのかと思い、最初に提示したコードに変更したところ、無事実行できました。公式ブログのサンプルコードもちゃんと見るとusing内でDisposeしています(普通に見逃していました。) 実際の原因としては、今回のコードでいうと、SafeFileHandle.CreateAnonymousPipeで作成した匿名パイプのハンドルのコピーをProcess.StandardOutputHandle経由でProcess.Startで起動するproducerの子プロセスへ受け渡しています。この時点で親のwriteHandleのハンドルと子プロセスのハンドルが二つ開いた状態になっています。 そして、consumerのプロセスは、producerの子プロセスの出力が終了するまで待っていますが、匿名パイプの仕様として書き込み側のハンドルが全て閉じないとEOFと判断されないため、consumerは入力を待ち続ける動作(デッドロック)が発生していました。 ! ハンドルが破棄がされていないのが原因なので、例えば最初のサンプルコードからusing (writeHandle)をコメントアウトしても、同じように再現します。 ということでSafeFileHandle.CreateAnonymousPipeの動作や実際はまった点についての紹介でした。この辺りの匿名パイプの仕様は昔一度調べていたはずなので、すっかり忘れていたのでいい勉強になりました笑。 終わりに 今回は触れていませんが、SafeFileHandleにはこれ以外にも新規APIが増えたりして、やっとProcessクラス以外の選択肢も出来たと思います。互換性の問題だとは思いますが、引数にProcessStartInfoを用いるのはちょっと納得がいっていないですが では今日のところはこのへんで。 参考URL learn.microsoft.com 脚注 dotnet/runtime SafeFileHandle.Windows.cs - github.com/dotnet/runtime/blob/v11.0.0-rc.3.26522… ↩︎ CreatePipe 関数 learn.microsoft.com ↩︎ CreateNamedPipeA 関数 - learn.microsoft.com ↩︎

  3. Privacy Boundaries for AI Telemetry Turning off prompt capture is not enough to enforce a complete telemetry recording policy. Exceptions can still include customer text in spans. If an investigation is limited to ten samples, it can end up with forty if four replicas each track their own counters. Queued samples can exceed their intended retention if the destination resets the expiry upon arrival. The recording-policy tip establishes what to allow. This article focuses on enforcement: inspect the actual SDK output, authorize capture with a narrow scope, reserve quota across replicas, and carry an absolute expiry through every copy of evidence. The example is a support assistant that reads a private ticket, fetches a refund policy, calls an eligibility tool, and drafts a reply. An investigation requires the policy excerpt and the generated reply, but should not collect the ticket or account-tool payload. Verify what the SDK actually records Keep ordinary message capture explicitly disabled. This composition fragment assumes an existing provider client and uses Microsoft.Extensions.AI 10.10.0: using Microsoft.Extensions.AI; IChatClient client = new ChatClientBuilder(providerClient) .UseOpenTelemetry( sourceName: "SupportReply.AI", configure: telemetry => telemetry.EnableSensitiveData = false) .Build(); Setting this option explicitly overrides OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT for this wrapper. This is not a telemetry allowlist. Reviewing the 10.10.0 source shows there are additional paths: | Recorded value | Behavior with sensitive capture disabled | | --- | --- | | Error status description | The error helper passes the exception message to the activity status. | | Exception log | When a logger is configured and warnings are enabled, the helper passes the exception to ILogger, independently of span export. | | Conversation identifier | The wrapper records ChatOptions.ConversationId when supplied and the activity requests data. | | Tool definitions | It serializes tool definitions with optional properties excluded. Tool identities still need review. | For example, if a provider exception includes a synthetic customer marker, that marker remains in Activity.StatusDescription even if raw message capture is disabled. The conversation identifier can also link a trace to a private application record. For a strict routine destination, use application-owned instrumentation built from approved values. Do not connect its tracer provider to the wrapper’s source until the source’s output complies with policy. Review the configured logger and its providers separately. Excluding the activity source does not prevent exception logging. Catching exceptions outside the wrapper does not remove a completed span or an exception already passed to a logger. Treat any allowed sanitization as specific to the exact fields and signal paths it covers. Removing message attributes alone does not address status descriptions, events, resource attributes, log bodies, and other instrumentation. Collector redaction is an extra safeguard, but it cannot reverse data already sent to an exporter or persistent queue. These are observations specific to this version, not a guarantee for all SDK releases. Repeat inspections and marker checks with every upgrade. Keep routine evidence useful and bounded An operator investigating an ineligible refund often starts with operational evidence: | Investigation question | Approved routine evidence | | --- | --- | | Which behavior was deployed? | Build identity, prompt version, configured model alias, policy version | | Did retrieval return context? | Retrieval outcome, result count, context size | | Did the eligibility tool complete? | Registered tool name, duration, controlled outcome code | | What happened to the reply? | Completion outcome, token usage when available, application validation result | A result count alone does not establish relevance or provide sufficient evidence. To determine if the model saw a conflicting refund clause, you may need separately authorized access to content. Constrain not only field names but also the values. Use controlled failure codes. Exception messages, document titles, URLs, and log scopes can leak customer details or secrets. Keep record lookups behind authorized application access. Opaque IDs and hashes still allow correlation and should not be used in high-cardinality metric dimensions. Review OpenTelemetry Baggage separately, since propagation can expose it to downstream services via HTTP headers. Exclude customer content and credentials, treat incoming baggage as untrusted, and configure propagation at external boundaries. Make diagnostic capture require a versioned grant Keep ordinary capture disabled during an incident. Use a dedicated capture service to select permitted fields and write only to a restricted evidence store. A global content flag would gather unrelated conversations as well. An authorized service owner can issue a grant with the following limits. These values are illustrative, not SDK defaults or universal limits: | Property | Authorized decision | | --- | --- | | Purpose and scope | Refund-policy investigation for one tenant, the support-reply operation type, and an explicit set of operation IDs | | Fixed capture schema | Policy excerpt and generated reply. Exclude the private ticket and account-tool payload | | Collection window | Thirty minutes. Revoke earlier if necessary | | Shared quota | Ten reservations across all replicas, with each payload at most 16 KiB after redaction and UTF-8 serialization | | Destination and readers | Restricted evidence store for assigned investigators | | Evidence lifetime | Twenty-four hours from collection, independently of the collection window | An operation ID comes from trusted application context. It is not a model argument or an investigation header used for authorization. If the incident is linked to a conversation, resolve authorized operations on the server and list those IDs. A broad tenant-wide capture requires separate justification. This application supports a single fixed diagnostic schema: policy excerpt and generated reply. A grant authorizes only this schema and cannot select arbitrary fields. Adding or changing fields requires a separately reviewed schema and capture path with new grants. Assign each grant a version. Changes to scope, collection deadline, evidence lifetime, or byte limit invalidate old authorizations at reservation time. The capture writer cannot expand a grant or read evidence. Investigator access is a separate role. Put selection, reservation, and expiry on one path The following C# code shows the enforcement boundary inside a capture service. The application-specific collaborators described below are not features provided by OpenTelemetry: public sealed record CaptureGrant( Guid Id, int Version, string TenantId, string OperationType, IReadOnlySet<Guid> OperationIds, DateTimeOffset CollectUntilUtc, TimeSpan EvidenceTtl, int MaxUtf8Bytes); public async Task TryCaptureAsync( CaptureGrant grant, TrustedOperation operation, CancellationToken ct) { if (grant.TenantId != operation.TenantId || grant.OperationType != operation.Type || !grant.OperationIds.Contains(operation.Id)) return; using var budget = CancellationTokenSource.CreateLinkedTokenSource(ct); budget.CancelAfter(TimeSpan.FromSeconds(2)); var slot = await reservations.TryReserveAsync(grant, operation, budget.Token); if (slot is null) return; // This type exposes only the two fields authorized by this design. var selected = new DiagnosticFields(operation.PolicyExcerpt, operation.Reply); byte[]? safePayload = redactor.TrySerializeUtf8( selected, grant.MaxUtf8Bytes, budget.Token); if (safePayload is null || safePayload.Length > grant.MaxUtf8Bytes) return; var now = clock.GetUtcNow(); var expiresAt = slot.CollectedAtUtc + grant.EvidenceTtl; if (now >= slot.CollectUntilUtc || now >= expiresAt) return; var sample = new EvidenceSample(slot.SampleId, grant.Id, grant.Version, operation.Id, slot.CollectedAtUtc, expiresAt, safePayload); await evidence.WriteIfFreshAsync(sample, budget.Token); } grant is an immutable, server-authorized snapshot loaded after verifying the investigation purpose and the requesting identity. TrustedOperation is provided by the application. The first guard checks for context mismatches before selecting content. reservations revalidates the current grant, including its version, scope, revocation status, deadline, and shared quota. It returns a unique sample ID and database timestamp only after committing the reservation. CollectedAtUtc records the time the reservation was admitted, before payload selection and redaction, ensuring that processing or transport delays do not extend retention. redactor serializes only DiagnosticFields, respects the byte and cancellation budgets, removes required recognized secrets, and returns no payload if it cannot verify removal. This does not guarantee that arbitrary text is non-sensitive, so the destination must be authorized for any remaining content. Do not serialize the entire operation and then try to remove private fields afterward. evidence accepts the same sample ID idempotently and checks the absolute expiry again when admitting the sample. The fragment omits exception handling. Its calling capture boundary must catch operational exceptions from these collaborators, including capture timeouts, without failing the user request. On failed redaction or an operational failure, skip capture and record only a controlled outcome such as capture_skipped. Do not log the payload or exception text as a fallback. The example’s two-second budget requires all collaborators to honor cancellation and keep their work bounded. Reserve quota atomically across replicas A separate read of the remaining quota followed by an unconditional write races. In PostgreSQL, use a short transaction: acquire a lock on the grant row, check the deadline, and then perform a conditional increment: BEGIN; SELECT id FROM capture_grants WHERE id = @grant_id FOR UPDATE; WITH admission AS MATERIALIZED ( SELECT clock_timestamp() AS collected_at_utc ) UPDATE capture_grants SET samples_used = samples_used + 1 FROM admission WHERE id = @grant_id AND version = @version AND tenant_id = @tenant_id AND operation_type = @operation_type AND @operation_id = ANY(operation_ids) AND revoked_at IS NULL AND collect_until_utc > admission.collected_at_utc AND samples_used < max_samples RETURNING admission.collected_at_utc, collect_until_utc; COMMIT; Here, id is the primary key, operation_ids is a UUID array, and timestamps use timestamptz. The @ names are driver parameters. Each replica locks the same grant row. Evaluate database time after acquiring the lock so the deadline check uses the current time rather than a timestamp from before the wait. The materialized CTE provides a single collection timestamp for both the expiry check and the returned receipt. A successful committed increment uses up one reservation. If no row is returned, the capture is denied. Apply grant changes and revocations through the same authority, incrementing the version when the scope or lifetime changes. Include lock waits in the capture budget. Using database time prevents replica clocks from making independent decisions about when reservations expire. Reservations are not refunded after failed redaction or storage. This approach can result in fewer than ten samples but will never authorize more than ten captures. An ambiguous reservation response may mean capacity was consumed. Do not reset the counter when retrying. Reuse a sample ID for evidence write retries, keeping the original timestamps. The reservation is the authorization point. Revocation stops new reservations but does not retroactively cancel any that have already been issued. The producer must complete its work within the short budget and the collection deadline. If you need to immediately revoke in-flight capture, the evidence-admission service must check the current grant state before committing. Carry absolute expiry through queues and copies Every sample needs explicit lifecycle fields. The sample created above has this shape: public sealed record EvidenceSample( Guid SampleId, Guid GrantId, int GrantVersion, Guid OperationId, DateTimeOffset CollectedAtUtc, DateTimeOffset ExpiresAtUtc, byte[] Payload); GrantId and GrantVersion identify the authorization used, even if the live grant changes or is revoked later. Preserve that authorized version for audit purposes. Suppose reservation admission happens at 10:00 UTC and the evidence lifetime is twenty-four hours. ExpiresAtUtc will be 10:00 UTC the next day. If a queue delivers the redacted payload at 09:50 the next morning, the destination has ten minutes left, not another full day. Any arrival after 10:00 must be rejected. The destination should compute any storage TTL based on the original expiry minus the current time. Apply the absolute deadline on reads, since a scheduled deletion sweep may run later. Clock synchronization and destination-side time checks are still needed. Producer-side checks alone cannot prevent delayed arrivals. Stopping collection after thirty minutes does not delete accepted samples, and deleting from the active store does not remove all exported copies. Maintain a copy register with concrete enforcement and clear ownership: | Copy | Required control | Owner | | --- | --- | --- | | Exporter buffers, configured Collector queues, routine backend | Approved fields before enqueue, reviewed retention and access at each destination | Service and telemetry-platform owners | | Restricted sample and any sending queue | Original sample ID and absolute expiry, no expired admission or investigator reads, deletion mechanism | Evidence-service owner | | Download, alert attachment, incident document | Authorized destination, readers, expiry, and tracked deletion or approved preservation | Named investigation owner | | Production-derived evaluation case and outputs | Separate reuse authorization and lifecycle with traceable origin | Dataset owner | A dashboard role is insufficient if a routine operator can still access evidence through an API or export permission. Also verify service identities and administrator access. Placing data in a separate table only restricts access if the backend enforces it. If a dashboard forwards content to an AI diagnosis service, include that destination in the register. Backups, replicas, and preservation requirements need a defined lifecycle outside the active-store sweep. If an unexpected copy appears, contain the recording path and involve the responsible incident team in its resolution. Disabling the source flag does not dispose of existing evidence. Treat reuse for evaluation as a new authorization A production failure can become a regression case, but its investigation grant does not authorize keeping the dataset indefinitely. Prefer using a synthetic case that preserves the failure mechanism without including the original ticket. If real content is needed, separately approve its new purpose, readers, destination, and expiry. Maintain lineage to the source sample and record the new authorization. Copying into CI or a golden dataset must not silently remove the original deadline. A judge model is another possible destination. Its explanations and reviewer comments may quote the input and require content controls. A numeric score with approved labels can go into routine telemetry, but explanations must remain restricted. Versioning a dataset does not extend retention permission. Verify failure paths before enabling real capture Use distinct synthetic markers in messages, policy excerpts, tool payloads, conversation IDs, tool names, and provider exceptions. Include a fictional email address and a fake credential. Inspect spans, status descriptions, events, logs, queues, backend queries, and exports at their destinations. | Exercise | Required observation | | --- | --- | | Provider exception with content capture disabled | Raw wrapper status and a configured logger can receive the marker. Test and control each path independently before export. | | Environment flag accidentally enabled | Explicit wrapper setting still excludes message capture. Conversation IDs, tool identities, other instrumentation and logs are checked separately. | | Wrong tenant, operation ID or type, stale version, revoked or expired grant | No reservation or content write. | | Many replicas compete for ten remaining slots | At most ten committed reservations across the whole deployment. | | Redaction fails, payload is oversized, or evidence storage is unavailable | No evidence sample and no fallback containing raw text. | | Evidence-write retry after a lost acknowledgement | Same sample ID, grant version, and original expiry, with no new retention clock. | | Reservation commits but its acknowledgement is lost | Capacity stays consumed. No sample is written without a confirmed reservation receipt. | | Queue arrival or read after absolute expiry | Admission or read denied, even before the deletion sweep runs. | | Routine operator uses the query API or export route | Investigation content remains inaccessible. | Marker tests detect known paths, not every possible private sentence. Repeat these tests after any SDK, exporter, Collector, or evaluation changes. For a real incident, also test the copy register and deletion process. A successful source-side unit test does not confirm backend access or erasure. When to use this approach Use this approach when production debugging requires selected user or business content across multiple service instances. Start with a single feature, a versioned grant, a shared reservation authority, and a restricted store that enforces absolute expiry. The existing observability walkthrough covers instrumentation setup, and trust-boundary guidance provides the broader authorization model. When not to use this approach Experiments with only synthetic data may need simpler controls. Mandatory audit evidence and investigations that require unredacted records need a separately designed evidence process with the necessary availability, preservation, and retention guarantees. This best-effort diagnostic path is a single application control, not a complete privacy or governance program. Related reading - Define what AI telemetry may contain before recording content - Observability for Agents with OpenTelemetry, Aspire, and Application Insights - Trust Boundaries Around AI Features Sources - Microsoft Learn: EnableSensitiveData defaults and environment-variable precedence - .NET Extensions source: OpenTelemetryChatClient at the 10.10.0 package revision - .NET Extensions source: Error status and exception recording at the same revision - OpenTelemetry: Handling sensitive data - OpenTelemetry: Baggage security considerations - OpenTelemetry Collector Contrib: Redaction processor behavior - PostgreSQL 18: Read Committed isolation and concurrent updates - PostgreSQL 18: UPDATE and RETURNING - PostgreSQL 18: Current-time functions and clock_timestamp

  4. Ran an experiment that ran for a day that resulted in ~90 PRs to improve the performance of asp.net Core: aspnetcore#70219 Fun facts: - These were using Github's new cloud agents support that shipped this week at GHU. - Opus 5.5 is very good - ~$230 in tokens. #dotnet #aspnetcore

    111 likes · 10 reposts · 8,526 views
  5. 【CVE-2026-111497】Brotliの脆弱性(Broccoli2Shell) 概要 Googleが開発するデータ圧縮ライブラリ「Brotli」において、細工された圧縮ストリームを展開(デコード)する際に境界外書き込みが発生する脆弱性が公表された。未認証の遠隔の攻撃者から悪意あるBrotli圧縮ストリームを処理させられることで、隣接するデコーダー状態メモリの破壊、機密メモリ内容の漏洩、サービス拒否(DoS)、または任意のコード実行(RCE)に至る可能性がある1。 項目 内容 CVE ID CVE-2026-111497 対象ソフトウェア Google Brotli(C言語デコーダー実装 c/dec/decode.c) CVSSスコア ・9.2 Critical (CVSS:4.0/AV:N/AC:L/AT:P/PR:N/UI:N/VC:H/VI:H/VA:H/SC:N/SI:N/SA:N — Google CNA / GitHub) ・9.8 Critical (CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H — NVD) ・8.1 High / Important (CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:H — Microsoft / Red Hat / AWS) 脆弱性種別 CWE-252: 戻り値の未チェック(Unchecked Return Value)、CWE-787: 境界外書き込み(Out-of-bounds Write) 発見・報告者 Khai Tran 氏、Sylvie Mayer 氏(OpenAI Codex Security) 悪用・KEV登録状況 Imperva社がNode.js 24.22.0(Linux arm64公式イメージ)環境でのRCE実証を報告。2026年10月27日付で米国CISA KEVに追加(対策期限: 2026年11月10日) 本脆弱性は、発見から波及範囲の判明に至る経緯の中で以下の3つの通称で呼ばれている。 Node2Shell: 2026年10月21日のNode.jsセキュリティリリース公表後、Imperva Red TeamがNode.js組み込みBrotliデコーダーにおける解析およびRCE実証に命名した呼称である4。 HTTP2Shell: 当初はNode.jsに関連して「Node2Shell」と呼称されていたが、根本原因がHTTP通信(Content-Encoding: br)等で広く利用されるBrotliのC言語実装にあり、複数製品が影響を受けることから「HTTP2Shell」とも呼称されている(HTTP/2プロトコル自体の脆弱性ではない)5。 Broccoli / Broccoli2Shell: セキュリティ研究者のJuliano Rizzo氏が「Node2Shellという名称は影響範囲を狭く見せてしまう」として「Broccoli」と呼ぶことを提唱したことに端を発し、追跡サイト「broccoli2shell.com」等で用いられている呼称である6。 影響を受けるバージョンと修正バージョン 1. Brotli の影響範囲と修正状況 Brotliの公式C実装において、バージョン 1.0.3 から 1.2.0 までのデコーダーが影響を受ける。各系統の修正バージョンは以下の通りである1。 系統 / ブランチ 影響を受けるバージョン 修正版 補足 1.0 系 >= 1.0.3, < 1.0.10 1.0.10 コミット 50a9b70a(0ed4268a のバックポート) 1.1 系 >= 1.1.0, < 1.1.1 1.1.1 コミット da980636(0ed4268a のバックポート) 1.2 系 >= 1.2.0, < 1.2.1 1.2.1 コミット b418fa81 / 0ed4268a 2. 主要ランタイム・言語パッケージ・アプライアンスの対応状況 BrotliのC言語デコーダーをバンドルまたはバインディングとして組み込んでいる各ランタイム・パッケージおよびネットワーク製品の対応状況は以下の通りである。 ランタイム / 製品 影響を受けるバージョン 修正版 補足 Node.js (v24 LTS) 24.x(< 24.22.1) v24.22.1 / v24.22.2 3 v24.22.1 で修正(v24.22.2 はリグレッション修正版) Node.js (v22 LTS) 22.x(< 22.23.4) v22.23.4 組み込みBrotliの更新(2026年10月セキュリティリリース) Node.js (v26 Current) 26.x(< 26.12.1) v26.12.1 組み込みBrotliの更新(EOL系統の <= 20.x, 23.x, 25.x も影響対象) Bun >= 1.0.21, < 1.4.4 1.4.4 8 バンドルするBrotliを 1.2.0 から 1.2.1 へ更新(GHSA-663w-cv8g-jj2p) Python (brotli / PyPI) >= 1.0.3, < 1.2.1 1.2.1 7 PyPI上の公式C拡張パッケージ(GHSA-9v8h-qqgm-4m35) Python (brotlicffi / PyPI) >= 1.0.3, < 1.2.1.0 1.2.1.0 Brotli C実装のCFFIバインディング(GHSA-9v8h-qqgm-4m35) .NET 8 / 9 / 10 8.0.0〜8.0.32 9.0.0〜9.0.21 10.0.0〜10.0.13 Runtime 8.0.33 Runtime 9.0.22 Runtime 10.0.14 9 SDKは 8.0.133/8.0.427、9.0.123/9.0.320、10.0.114/10.0.403 で修正 Palo Alto Networks PAN-OS PAN-OS 12.2 / 12.1 / 11.2 の未修正版 12.2.4-h1 12.1.7-h11 等 6 データプレーンでTLS復号とBrotli検査を有効化している場合のみ影響(デフォルト無効) 3. 主要Linuxディストリビューションの対応状況 OSの共有ライブラリ(libbrotli)およびディストリビューション提供パッケージにおける対応状況は以下の通りである。ディストリビューションによってはNode.jsや.NETパッケージがOS標準の共有ライブラリ(libbrotli)を動的リンクしているため、影響有無がパッケージ単位で異なる点に注意が必要である。 ディストリビューション 対象パッケージ 修正版 補足 Amazon Linux 2023 brotli, libbrotli, python3-brotli nodejs24, dotnet8.0 1.1.0-1.amzn2023.0.2 アドバイザリ: ALAS2023-2026-3334(brotli)、ALAS2023-2026-3330(nodejs24)、ALAS2023-2026-3367(dotnet8.0)。nodejs22・dotnet9.0・dotnet10.0 は影響なし Ubuntu brotli, libbrotli1, python3-brotli dotnet8, dotnet10, dotnet11 24.04 LTS: 1.1.0-2ubuntu0.1 22.04 LTS: 1.0.9-2ubuntu0.1 アドバイザリ: USN-8956-1(Brotli)、USN-8958-1(.NET 8.0.33 / 10.0.14) Debian brotli, libbrotli1 Trixie (13): 1.1.0-2+deb13u1 Bookworm (12): 1.0.9-2+deb12u1 DSA-6582-1 / DLA-4855-1。Debianの nodejs と python-brotlicffi はシステムの brotli を利用 RHEL (Red Hat) brotli, nodejs:24, dotnet8.0, firefox, thunderbird 提供済(各RHSA) RHSB-2026-015(Important)。RHEL 8/9の nodejs:22、RHEL 10の nodejs22/nodejs24、および dotnet9.0/10.0/11.0 はシステムの brotli を利用 4. AWS マネージドサービスの対応状況 AWSの 2026-155-AWS で公表されている主要マネージドサービスの対応状況は以下の通りである10。 対象サービス / リソース 修正版 補足 AWS Lambda(Node.js 22/24/26、.NET 8/9/10) 更新済 マネージドランタイム・ベースイメージ更新済。コンテナイメージ利用時は最新ベースイメージでの再ビルドが必要(Python/Ruby/provided.al2023 はBrotli非同梱) AWS Elastic Beanstalk (Node.js) 6.11.12 Node.js 24.22.1 および 22.23.4 を含むプラットフォームバージョン 6.11.12 を提供済 AWS App Runner / Amplify / CodeBuild / Synthetics 更新済 App Runnerは2026年10月26日に全リージョン更新済(未デプロイのサービスは1回再デプロイが必要) 影響を受ける条件 本脆弱性は、ライブラリがシステム上に存在するだけでは発火せず、「信頼できないBrotli圧縮ストリームを、対象アプリケーションやプロキシがデコード(展開)する」 処理が行われた際に成立する6。 1. 前提となるシステム・ネットワーク構成 攻撃が成立する代表的な経路は以下の4パターンである。 パターン1:クライアントからの圧縮HTTPリクエストボディを展開するサーバー / API クライアントから送信された Content-Encoding: br ヘッダー付きのリクエストボディを、ミドルウェア(Node.jsのExpress 5.0.0〜5.3.0やbody-parser 2.0.0〜2.3.0における raw() / json() 等)やバックエンドアプリケーションが自動または明示的にBrotli展開している構成(なお、素の node:http サーバー自体は単体ではリクエストボディを自動展開しない)。 パターン2:外部サーバーからの圧縮HTTPレスポンスを展開するHTTPクライアント / クローラー 接続先サーバーから返却された Content-Encoding: br レスポンスを自動展開する構成(Node.jsやBunの組み込み fetch()、Brotliパッケージがインストールされた環境でのPython urllib3 / requests / aiohttp / httpx[brotli] 等)。 多くのHTTPクライアントはリクエスト時に Accept-Encoding: br を送信したかどうかにかかわらず、レスポンスヘッダーに Content-Encoding: br が含まれていれば自動的にBrotliデコーダーを呼び出す。そのため、任意のURLを取得するWebhook、リンクプレビュー、クローラー、または非暗号化通信経路上の中間者攻撃において、悪意あるサーバーから一方的に Content-Encoding: br のレスポンスを返されるだけでトリガーされ得る。 パターン3:ファイアウォール等のデータプレーンにおけるトラフィック検査 Palo Alto NetworksのPAN-OS(PA-Series / VM-Series)において、データプレーン上でTLS/SSL復号(Decryption)とBrotliインスペクションの両方が有効化されている場合、通過するBrotliトラフィックをファイアウォール自身が展開するため影響を受ける。 パターン4:デコーダーAPIの直接呼び出し・ファイル処理 アーカイブ、キャッシュ、メッセージキュー等の外部データを、node:zlib(brotliDecompress*、createBrotliDecompress)、Web標準の DecompressionStream、Pythonの brotli.decompress()、.NETの BrotliStream / BrotliDecoder 等で直接展開している場合。 ! 影響を受けないケース: 圧縮(エンコード)のみ・静的 .br ファイル配信のみの構成: 脆弱性はデコーダー(c/dec/decode.c)に存在するため、信頼できるデータをサーバー側でBrotli圧縮して送信するのみの構成や、事前圧縮された静的な .br ファイルをそのまま配信するだけの構成ではデコード処理が行われず、影響を受けない。 .NET のデフォルト挙動: Microsoftのアドバイザリによれば、ASP.NET Coreおよび HttpClient はデフォルトではBrotli圧縮データを自動展開しない。自動展開でBrotliを有効化するよう明示的に設定変更している場合、または System.IO.Compression(BrotliStream / BrotliDecoder)で信頼できないデータを直接展開している場合のみ影響を受ける9。 2. スタンドアロン実行ファイル・コンテナイメージに関する注意点 自己完結型バイナリ(Self-Contained / 単一実行ファイル): .NETのSelf-ContainedデプロイやNativeAOTでビルドされたアプリケーション、およびBunの bun build --compile で生成された実行ファイルは、ビルド時点のランタイム(および脆弱なBrotliデコーダー)をバイナリ内部に埋め込んでいるため、ランタイム更新後にアプリケーションの再ビルドと再デプロイが必要である。 動的リンクとバンドルの違い: Red Hat Enterprise LinuxやDebian、Amazon Linux 2023では、パッケージによってBrotliを内部バンドルしているもの(例:AL2023およびRHEL 8/9の nodejs24、dotnet8.0)と、OSの共有ライブラリ libbrotli を動的リンクしているもの(例:Debianの nodejs、RHEL 8/9およびAL2023の nodejs22、dotnet9.0、dotnet10.0)が混在している。したがって、OSパッケージとランタイムの双方を確認・更新し、実行中のプロセスを再起動する必要がある。 3. 自環境の確認方法 1. Node.js のバージョンおよびバンドルされている Brotli バージョンの確認 node --version node -p "process.versions.brotli" 2. Bun のバージョン確認(1.4.4 以上であるか) bun --version 3. .NET ランタイムのバージョン確認(8.0.33 / 9.0.22 / 10.0.14 以上であるか) dotnet --list-runtimes 4. Python パッケージの確認(Brotli 1.2.1 以上、brotlicffi 1.2.1.0 以上であるか) python3 -m pip show brotli brotlicffi 5. Linux OSパッケージおよびロード済み共有ライブラリの確認 dpkg -l | grep -i brotli # Debian / Ubuntu rpm -qa | grep -i brotli # RHEL / Amazon Linux sudo grep -l brotlidec /proc/[0-9]*/maps 2>/dev/null 脆弱性の技術的な仕組み 前提:Brotliのブロック切り替えと高速パス(Fast Path)/ 安全パス(Safe Path) Brotliの圧縮ストリームは複数のブロックに分割されており、デコーダーはストリームを読み進めながらリテラル(tree_type = 0)、挿入・コピーコマンド(tree_type = 1)、距離コード(tree_type = 2)のブロックタイプを切り替える。 ブロックタイプを切り替える際、デコーダーは c/dec/decode.c 内の DecodeBlockTypeAndLength() を呼び出して次のブロックタイプ番号とブロック長をビットストリームから読み取る。このときBrotliデコーダーは、入力バッファに十分なデータがある場合に境界チェックを省略して高速に処理する Fast Path(safe = 0) と、入力不足を検査しながら処理する Safe Path(safe = 1) の2つの経路を持っている。 原因:戻り値の未チェック(CWE-252)と境界外書き込み(CWE-787) 脆弱性の根本原因は、c/dec/decode.c 内の DecodeBlockTypeAndLength() が返す真偽値 BROTLI_BOOL の仕様と、呼び出し元における戻り値の未チェックにある1。 戻り値 BROTLI_FALSE における2種類の意味の混在: 修正前の DecodeBlockTypeAndLength() は戻り値として単なる真偽値(BROTLI_BOOL)を返していた。この関数は、Safe Pathで入力ビットが一時的に不足している場合(!SafeReadSymbol や !SafeReadBlockLength)に BROTLI_FALSE を返すだけでなく、ストリームのブロックタイプ最大数 max_block_type が 1 以下であるにもかかわらずブロック切り替えが要求されたというストリーム構文エラー(フォーマット不正) の場合(if (max_block_type <= 1) return BROTLI_FALSE;)にも同じ BROTLI_FALSE を返していた。 Fast Path(safe = 0)での戻り値の無視: 修正前のコードでは、Fast Path用のラッパー関数 DecodeLiteralBlockSwitch()、DecodeCommandBlockSwitch()、DecodeDistanceBlockSwitch() の戻り値型が static void と定義されており、内部で呼び出す Decode*BlockSwitchInternal(0, s) の戻り値を完全に無視していた。さらに呼び出し元のマクロ BROTLI_SAFE(METHOD) も、safe == 0 の分岐では戻り値を検証せずにそのまま処理を継続する実装となっていた。 不正な内部状態でのデコード継続と境界外書き込み: 細工された圧縮ストリームによって本来起こり得ないブロック切り替えが要求された際、脆弱なデコーダーは構文エラーで停止せず、「追加入力が必要である」状態と誤認する、あるいはFast Path上でエラーを無視して不整合な内部状態のまま処理を継続してしまう。その結果、内部バッファの終端を越えて攻撃者のバイト列が書き込まれ、隣接するデコーダー状態メモリ(BrotliDecoderState)の破壊や機密メモリの漏洩、さらには任意のコード実行(RCE)へとつながる。 攻撃フロー 修正パッチ Googleの公式リポジトリにおける修正コミット 0ed4268a(および各ブランチへの反映コミット 50a9b70a、da980636、b418fa81)では、以下の変更が行われている2。 c/include/brotli/decode.h に、ブロック切り替えのフォーマット異常を表す新しいエラーコード BROTLI_DECODER_ERROR_FORMAT_BLOCK_SWITCH = -17 を追加。 DecodeBlockTypeAndLength() および各 Decode*BlockSwitch*() 関数の戻り値型を BROTLI_BOOL / void から BrotliDecoderErrorCode に変更し、max_block_type <= 1 のフォーマット異常時は BROTLI_DECODER_ERROR_FORMAT_BLOCK_SWITCH、入力不足時は BROTLI_DECODER_NEEDS_MORE_INPUT、成功時は BROTLI_DECODER_SUCCESS を明示的に返すように修正。 Safe PathとFast Pathの両方でステータスコードを検証し、BROTLI_DECODER_SUCCESS 以外の場合は即座に saveStateAndReturn へジャンプして処理を中断するマクロ BROTLI_SAFE_WITH_STATUS(METHOD) を導入。 c/dec/decode.c // google/brotli commit 0ed4268a より抜粋 -static BROTLI_INLINE BROTLI_BOOL DecodeBlockTypeAndLength( +static BROTLI_INLINE BrotliDecoderErrorCode DecodeBlockTypeAndLength( int safe, BrotliDecoderState* s, int tree_type) { brotli_reg_t max_block_type = s->num_block_types[tree_type]; const HuffmanCode* type_tree = &s->block_type_trees[ @@ -1187,7 +1187,7 @@ brotli_reg_t* ringbuffer = &s->block_type_rb[tree_type * 2]; brotli_reg_t block_type; if (max_block_type <= 1) { - return BROTLI_FALSE; - return BROTLI_DECODER_ERROR_FORMAT_BLOCK_SWITCH; } /* Read 0..15 + 3..39 bits. */ @@ -1197,11 +1197,13 @@ } else { BrotliBitReaderState memento; BrotliBitReaderSaveState(br, &memento); - if (!SafeReadSymbol(type_tree, br, &block_type)) return BROTLI_FALSE; - if (!SafeReadSymbol(type_tree, br, &block_type)) { - return BROTLI_DECODER_NEEDS_MORE_INPUT; - } if (!SafeReadBlockLength(s, &s->block_length[tree_type], len_tree, br)) { s->substate_read_block_length = BROTLI_STATE_READ_BLOCK_LENGTH_NONE; BrotliBitReaderRestoreState(br, &memento); - return BROTLI_FALSE; - return BROTLI_DECODER_NEEDS_MORE_INPUT; } } ... ringbuffer[0] = ringbuffer[1]; ringbuffer[1] = block_type; - return BROTLI_TRUE; - return BROTLI_DECODER_SUCCESS; } -static BROTLI_INLINE BROTLI_BOOL DecodeLiteralBlockSwitchInternal( +static BROTLI_INLINE BrotliDecoderErrorCode DecodeLiteralBlockSwitchInternal( int safe, BrotliDecoderState* s) { - if (!DecodeBlockTypeAndLength(safe, s, 0)) { - return BROTLI_FALSE; - BrotliDecoderErrorCode result = DecodeBlockTypeAndLength(safe, s, 0); - if (result != BROTLI_DECODER_SUCCESS) { - return result; } PrepareLiteralDecoding(s); - return BROTLI_TRUE; - return BROTLI_DECODER_SUCCESS; } -static void BROTLI_NOINLINE DecodeLiteralBlockSwitch(BrotliDecoderState* s) { - DecodeLiteralBlockSwitchInternal(0, s); +static BROTLI_NOINLINE BrotliDecoderErrorCode +DecodeLiteralBlockSwitch(BrotliDecoderState* s) { - return DecodeLiteralBlockSwitchInternal(0, s); } 呼び出し元のマクロも以下のように BROTLI_SAFE_WITH_STATUS へ置き換えられ、safe フラグの有無にかかわらずエラー発生時にデコードが即座に停止するようになった。 c/dec/decode.c +/* NB: METHOD should return BROTLI_DECODER_SUCCESS, BROTLI_DECODER_ERROR_*, or - BROTLI_DECODER_NEEDS_MORE_INPUT; the later two break the processing. */ +#define BROTLI_SAFE_WITH_STATUS(METHOD) \ - { \ - BrotliDecoderErrorCode status; \ - if (safe) { \ - status = Safe##METHOD; \ - } else { \ - status = METHOD; \ - } \ - if (status != BROTLI_DECODER_SUCCESS) { \ - result = status; \ - goto saveStateAndReturn; \ - } \ - } 対策と緩和策 1. 恒久対策:修正バージョンへのアップデートと再ビルド 利用しているOSパッケージ、言語ランタイム、および依存ライブラリを修正バージョンへ更新し、稼働中のプロセスを再起動する。また、単一実行ファイル化(Bunの bun build --compile、.NETのSelf-Contained / NativeAOT)やDockerコンテナイメージを利用している場合は、更新後のランタイム・ベースイメージを用いて再ビルドおよび再デプロイを実施する必要がある。 各環境におけるアップデートコマンド例# 1. Amazon Linux 2023 の場合(ALAS2023-2026-3334 / 3330 / 3367) sudo dnf update brotli nodejs24 dotnet8.0 -y 2. Ubuntu / Debian の場合(USN-8956-1 / USN-8958-1 / DSA-6582-1) sudo apt update && sudo apt install --only-upgrade brotli libbrotli1 python3-brotli -y 3. Node.js の更新(nvm を利用している場合) nvm install 24.22.1 # または 22.23.4 / 26.12.1 4. Python パッケージ(PyPI)の更新(1.2.1 / 1.2.1.0 以上) pip install -U "Brotli>=1.2.1" "brotlicffi>=1.2.1.0" 5. Bun の更新(1.4.4 以上) bun upgrade 2. 一時的な緩和策(アップデートが直ちに実施できない場合) 各ベンダーのアドバイザリおよび技術資料では、即座にパッチを適用できない環境向けに以下の緩和策が案内されている。 WAFやリバースプロキシにおける受信 Content-Encoding: br リクエストの遮断: AWSのセキュリティブレティン 2026-155-AWS 等では、クライアントから送信される Content-Encoding: br ヘッダー付きHTTPリクエストをWAFやプロキシで拒否する緩和策が提示されている。AWSが公開しているAWS WAFカスタムルールの設定例(Countモードで検証後に "Count": {} を "Block": {} に変更する手順)は以下の通りである。なお、AWS WAFは受信リクエストのみを検査するため、アプリケーションが外部サーバーから取得するレスポンスには効かない点に留意が必要である10。 { "Name": "Node2Shell_BROTLI", "Priority": 2, "Statement": { "RegexMatchStatement": { "RegexString": "^\s*br\s*$", "FieldToMatch": { "SingleHeader": { "Name": "content-encoding" } }, "TextTransformations": [ { "Priority": 0, "Type": "LOWERCASE" } ] } }, "Action": { "Count": {} }, "RuleLabels": [ { "Name": "Node2Shell_Brotli" } ], "VisibilityConfig": { "SampledRequestsEnabled": true, "CloudWatchMetricsEnabled": true, "MetricName": "Node2Shell_BROTLI" } } HTTPクライアントおよびPython環境におけるBrotliデコードの抑止: 注意点(Accept-Encoding ヘッダー変更の限界): リクエストヘッダーの Accept-Encoding から br を除外したとしても、悪意あるサーバーはそれを無視して Content-Encoding: br のレスポンスを送りつけることができるため、クライアント側で自動解凍自体が有効なままでは対策として不十分である。 Bun: セキュリティアドバイザリ(GHSA-663w-cv8g-jj2p)では、fetch 呼び出し時に decompress: false を渡すことで、Accept-Encoding: br の送信を抑止するとともにレスポンス自体の自動デコードを無効化する回避策が案内されている8。 Python: 直ちにアップグレードできない場合の緩和策として、Brotli および brotlicffi パッケージ(ならびに httpx[brotli] 等のextra指定)自体をアンインストールし、urllib3、requests、aiohttp 等でBrotliデコーダーが呼び出されない状態(gzip / deflate へのフォールバック)にする方法が挙げられている6。 PAN-OS: ホットフィックス適用までの回避策としてCLIから set system setting brotli-encoding disable を実行(またはWeb UIの Device > Setup > Content-ID から Allow Brotli Encoding のチェックを解除)してBrotliインスペクションを無効化する手順が案内されている。 参考資料・出典一覧 CVE.org - CVE-2026-111497 Google Brotli GitHub Repository - Commit 0ed4268a Node.js - Wednesday, October 21, 2026 Security Releases Imperva Blog - Node2Shell: From Brotli Data to Remote Code Execution (CVE-2026-111497) JPCERT/CC - BrotliのC言語実装における境界外書き込みの脆弱性(CVE-2026-111497)に関する注意喚起 (JPCERT-AT-2026-0034) Broccoli2Shell: Brotli decoder RCE (CVE-2026-111497) in Node.js, Python and more GitHub Advisory Database - GHSA-9v8h-qqgm-4m35 (PyPI brotli / brotlicffi) Bun Security Advisory - GHSA-663w-cv8g-jj2p Microsoft .NET Security Advisory - CVE-2026-111497: Out of bounds write in Brotli leads to RCE AWS Security Bulletin - 2026-155-AWS: CVE-2026-111497: Native Brotli Decoder Remote Code Execution and Memory Disclosure

  6. Microsoft Just Shipped Visual Studio's October Update. Here's What It Actually Changes. Visual Studio used to ship big releases every couple of years. Now it ships something every month, and the October update (18.11) is the one where the monthly cadence stops looking like a gimmick and starts looking like a strategy. Version 18.11.0 landed on October 13, followed by 18.11.1 on October 20 and 18.11.2 on October 25. The headline items are a rebuilt GitHub Copilot chat, bulk merge conflict resolution, commit-to-working-tree comparison, and Hot Exit. The smaller changes matter too, because they fix things developers have been voting on for years. The Problem It's Solving Most of this release is about friction. Developers lose unsaved work when the IDE closes. They resolve forty merge conflicts one file at a time when they already know which side wins. They wait for a 20-project solution to finish building after project three has already failed. They reach for the mouse to reorder tabs. None of that is glamorous, and that's the point. Microsoft's own framing is "deep platform integration of AI, stronger fundamentals, and improved performance." The October update is the "stronger fundamentals" half of that sentence, plus a structural change to how the AI half is built. How It Actually Works The new Copilot chat. This is the one with architectural consequences. Visual Studio now has a Copilot chat experience built on the Copilot SDK, the same foundation behind the Copilot CLI and the GitHub Copilot app. It started in July with Agent (Preview), and 18.11 extends the SDK to the whole chat stack, including Ask mode, Agent mode, and the underlying infrastructure. Microsoft lists three gains: agent workflows that need less back-and-forth, longer sessions with better prompt caching, and consistent behavior across Copilot surfaces. To turn it on, go to Tools → Options → GitHub → Copilot → Copilot Chat, check "Use new Copilot chat experience," and restart. It's opt-in, so you choose when to switch. Git, three ways. Right-click a commit in the Git Repository window and choose Compare with Working Tree to diff that commit against your current code, committed and uncommitted changes together, without checking anything out. This one is useful in refactors. For merges, Keep All Current and Take All Incoming now work on any file, folder, or the repo root under Unmerged Changes in Git Changes, so one action resolves everything beneath it. This builds on the worktree support and Git submodule integration that arrived in 18.9. Hot Exit. When enabled, Visual Studio saves session state on close and restores it on reopen. That covers unsaved files, saved files with unsaved changes, undo history including Ctrl+Z, and your tab and window layout. It's off by default. Enable it under Tools → Options → Projects and Solutions → General by selecting "Reopen unsaved documents on solution load or empty environment." The new Markdown template in the New File dialog (Ctrl+N) works with it, so scratch notes survive a restart. Stop build on first failure. Another opt-in setting, under Tools → Options → Projects and Solutions → Build and Run, called "Stop solution build after first failure." Visual Studio stops queuing projects and safely halts the ones in flight, and the build still reports as failed rather than canceled. Microsoft's test showed four regular builds averaging 26 seconds with it off and 1.5 seconds with it on, a 94% cut in post-failure time. That's one solution, and the release notes say results vary. Long dependency chains and heavily parallel builds should benefit most. Fonts and Colors. The legacy dialog has been rebuilt on Unified Settings, with category navigation, search, inline previews, and a grid view. It now participates in the settings.json model, so changes made in the UI show up in the file and vice versa. That makes IDE customization something you can version and share. The small ones. Ctrl+Numpad 1 through 9 jumps to numbered document tabs. Ctrl+Shift+PgUp and PgDn move tabs, with pinned and unpinned tabs staying separated, and the shortcuts take over the old Text Editor bindings for Edit.ViewTopExtend and Edit.ViewBottomExtend. Ctrl+NumPad0 resets editor zoom to 100%. A Quick Info hover delay setting (50 to 10000 ms, default 150) stops tooltips popping up over the line you're aiming for. Open Folder now respects the same restore settings that solutions do. What Developers Are Actually Using It For The October features are fresh, so I'd rather not invent adoption stories. What the release notes do make clear is what each feature is built for. Compare with Working Tree is for refactoring and for answering "what have I changed since that commit?" without a stash or a checkout. Bulk conflict resolution is for large merges where you've already decided the policy, such as taking incoming on generated files or keeping current on a vendored folder. Fail-fast builds are for large solutions where a broken base project makes the rest of the build pointless. Hot Exit is for anyone who has closed the IDE with a dozen dirty buffers. The Copilot work from earlier in the cycle is where the agentic usage shows up. Version 18.9 added a Git agent that reviews uncommitted changes and commits, with inline comments, on both GitHub and Azure DevOps repos. Version 18.10 let it explore pull requests in chat, with navigable links to comments and files. It also added Bring Your Own Key in preview, supporting Microsoft Foundry, OpenAI, Anthropic, and Ollama, though only for the new Agent (Preview). The earlier BYOK path in the old Ask and Agent modes is no longer supported. Version 18.8 added built-in .NET and Azure skills, off by default. Two smaller details are worth knowing for agent work. Version 18.9 added low, medium, and high thinking-effort controls for supported models, plus a usage view, because Copilot moved to usage-based billing on token consumption. And since 18.7, Visual Studio has applied trust validation for MCP servers. It compares each server's configuration and its tools, prompts, resources, and instructions against the last approved fingerprint, and prompts you to re-approve if anything changed. That one is on by default. For anyone wiring MCP servers into an IDE agent, it's the most important security feature in the cycle. Why This Is a Bigger Deal Than It Looks There are two reasons. First, the SDK consolidation. When Visual Studio's chat runs on the same Copilot SDK as the CLI and the Copilot app, the IDE stops being a special case that gets agent features months late. Microsoft says it plainly: Visual Studio gets better as Copilot gets better. For a product with this much legacy surface area, that's a significant change in how AI features reach users. The cost is churn. The BYOK breaking changes in 18.10 show that migrating onto the new harness will break things for some users along the way. Second, the cadence. Since 18.6 in May, there has been a numbered update every month, with weekly point releases between them. The September rollout shipped alongside a long list of .NET and ASP.NET Core security fixes, and 18.11.2 includes three more advisories. A faster release train means more frequent patches, and also more frequent regressions. 18.11.2 lists a known issue where F5 fails to launch the Blazor WebAssembly debug adapter, and earlier point releases fixed Copilot chat failing to open. The monthly rhythm works if you treat the first two weeks of each cycle as a soft launch. Also keep in mind that three of the most useful October features, Hot Exit, fail-fast builds, and the new chat, ship disabled. If you update and see no difference, that's why. Availability and Access Visual Studio 2026 is available from the Visual Studio download page, and 18.11.2 is the current build. Existing installs update through the Visual Studio Installer. The Copilot features require a GitHub Copilot plan, and Copilot usage now counts against token-based limits. Org-level custom agents and custom instructions require a GitHub organization. Pull request browsing and review in the IDE sits under Tools → Options → Preview Features. The full list of changes and bug fixes is in the official release notes, and feature feedback goes through Developer Community. --- Visual Studio is becoming a client for a shared agent runtime, and the rest of the release is Microsoft paying down small debts to keep people from leaving. Both changes are real. Only the second one is finished. Follow for more coverage on MCP, agentic AI, and AI infrastructure.

    5 reactions · 4 comments
  7. Reddit · · r/RimWorld · u/GTurkistane · dotnet/runtime

    If your game does not launch or keep crashing when trying the new Waylith Engine 1.7 Unstable version, Hardware-enforced Stack Protection (CET) might be the cause. Here's what fixed it for me. NOTE: As per the developer's note below, the solution described here is unnecessary if you have installed the latest Windows cumulative updates. However, if you're stuck on an older version of Windows 10 and cannot upgrade for any reason, the solution below should work for you. Otherwise, simply install the latest Windows cumulative update, and there should be no need to apply this workaround. For anyone having trouble launching RimWorld 1.7 with the new Waylith Engine, I recently ran into an issue where the game simply refused to start. After quite a bit of troubleshooting, I managed to find the cause and fix it. The problem turned out to be Hardware-enforced Stack Protection (CET), a Windows security feature that was interfering with the game's startup. What is Hardware-enforced Stack Protection (CET)? Hardware-enforced Stack Protection is a Windows security feature that uses CPU hardware to protect applications against certain memory-based attacks. CET stands for Control-flow Enforcement Technology. One of its features uses something called a shadow stack, which keeps a protected copy of function return addresses. This helps prevent malicious software from hijacking a program's execution flow. It's an important security feature, but certain applications and runtimes can have compatibility issues with it, particularly on older Windows builds. In my case, RimWorld 1.7 uses the new Waylith Engine and its customized .NET 10 runtime, and the game was crashing during startup whenever this protection was enabled. How to fix it (only for RimWorld) Do NOT disable this protection for your entire system. Windows allows you to configure it for individual applications. 1. Open the Start menu, search for Windows Security, and open it. 2. Go to App & browser control. 3. Scroll down to Exploit protection and click Exploit protection settings. 4. Select the Program settings tab. 5. Click Add program to customize. 6. Select Choose exact file path. 7. Navigate to your RimWorld installation folder and select RimWorld.exe. For example: C:\Program Files (x86)\Steam\steamapps\common\RimWorld\RimWorld.exe Your installation path may be different. 8. A window will appear with several security settings. Scroll down until you find Hardware-enforced Stack Protection. 9. Check Override system settings. 10. Change the setting from On to Off. Your setting should look like this. 1. Leave the other options, including Enforce for all modules and Audit only, unchecked. 2. Click Apply, close the settings, and try launching RimWorld again. That's it! In my case, the game immediately started working after making this change. My system specifications (relevant to the issue) - Operating System: Windows 10 IoT Enterprise, 64-bit - Windows Version: 20H2 - OS Build: 19042 - CPU: AMD Ryzen 9 7950X3D - RimWorld Version: 1.7.4990.1069 (Waylith Engine, unstable branch) - Game Runtime: .NET 10.0.13-waylithclr.dev (WaylithCLR) - Installed .NET Runtime: microsoft.netcore.app 10.0.3 - Faulting Module: KERNELBASE.dll (10.0.19041.1466) - Exception Code: 0x80131506 These details might be particularly relevant if you're running an older Windows 10 build. I suspect there's a compatibility issue between the newer .NET runtime, CET, and certain older Windows components. Important security note This is a workaround, not necessarily a permanent fix. Disabling Hardware-enforced Stack Protection reduces exploit protection for the application you've selected. That's why I strongly recommend disabling it only for RimWorld.exe, rather than changing system-wide security settings. Microsoft has documented similar CET-related .NET runtime compatibility problems on older Windows builds. Updating Windows to a properly serviced version may resolve the underlying issue without requiring this exception. Relevant Microsoft .NET issue: runtime#108589 If you're experiencing the same problem, give this a try and let me know whether it works for you. Hopefully this saves someone else the hours of troubleshooting I went through!

    2 points · 7 comments
  8. Node2ShellはNode.jsだけの話ではない。Brotliの欠陥でPythonも対象、CVE-2026-111497の該当判定 「Node2Shell? うちは Node.js を使っていないから関係ないな」。10月下旬にこのニュースを見て、そう判断しかけた方もいると思います。ところが、Node.js を使っていなくても確認が必要な場合があります。Node2Shell(HTTP2Shell とも呼ばれる、CVE-2026-111497)の正体は、Node.js ではなく Google の圧縮ライブラリ Brotli の欠陥でした。Python の Brotli パッケージ(入っていると requests などが自動で使う)、Bun、Envoy、Git for Windows にも同じ欠陥がありました。10月27日には、米国の CISA(サイバーセキュリティ・社会基盤安全保障庁)が、実際に悪用が確認された脆弱性の一覧に追加しています。 この記事は、Node.js・Python・Bun でアプリを作っている人や、Windows で Git for Windows を使っている人向けです。読み終えると、次の3つが分かります。 なぜ「Node.js の脆弱性」として広まったのか(原因) 自分の環境が該当するか、どこまで上げればよいか(言語・ツール別の判定) 呼び名・深刻度・RCE の範囲など、記事ごとに食い違う情報のどれを信じればよいか 情報は2026年10月29日時点のものです。技術的な解析は Imperva と OpenSourceMalware の記事が詳しく、Python・curl・Git for Windows の動作検証は技術ブログの lilting が詳しいです。どれも末尾のリンクから読めます。 ! 先に答え(2026年10月29日時点) 出回った言い方 一次情報での答え Node.js の脆弱性 欠陥があるのは Google の Brotli。Node.js は同梱していた Brotli を直した側 Node.js を使っていなければ無関係 Python(Brotli パッケージがあると requests 等が使う)・Bun・Envoy・Git for Windows も対象 Node2Shell と HTTP2Shell は別の脆弱性 どちらも CVE-2026-111497 の1件 深刻度は High? Critical? どちらも正しい。評価した組織が違う 素の Node.js ならどこでも RCE される 実証は Node.js 24.22.0(arm64 と x86-64)で報告された。24.22.0 用の攻撃データは 22.23.3・26.12.0 では通らず、ビルドごとに調整が要る 実際の攻撃はまだ? CISA が10月27日に、悪用が確認された脆弱性の一覧(KEV)に追加した。攻撃通信を観測したという報告もある 自分は該当する? 使っている言語ではなく、「信頼できない相手から来た Brotli データを展開するか」と「同梱している Brotli が直った版か」で決まります。判定だけ知りたい方は「自分は関係ある? 言語・ツール別の判定」へ進んでください。 Brotli とは何で、何が壊れていたのか Brotli は Google が作った圧縮形式で、Web では HTTP の Content-Encoding: br として使われます。欠陥があったのは、圧縮データを元に戻す部分(デコーダ)の C 言語実装です。壊れた圧縮データを渡すと、デコーダが内部バッファの外へ書き込んでしまいます(境界外書き込み)。本来触ってはいけないメモリを書き換えるため、条件がそろうと遠隔からのコード実行(RCE)につながります。 影響を受ける版: 1.0 系は 1.0.3〜1.0.9、1.1 系は 1.1.0、1.2 系は 1.2.0 修正版: 1.0.10 / 1.1.1 / 1.2.1(日本時間の10月22日 04:27 公開) CVE の採番: Google 発見: OpenAI Codex Security なぜ「Node.js の脆弱性」として広まったのか 広く知られるきっかけが、10月21日の Node.js 定例セキュリティリリースだったからです。流れを表にまとめます(日付は日本時間)。 日付 出来事 2月27日 Brotli のリポジトリに修正コミット。メッセージは「Detect and report invalid stream earlier.」(壊れたストリームを早めに検出して報告する)の1行で、脆弱性とは書かれていない。修正版の公開まで約237日 10月21日 Node.js が定例セキュリティリリースで10件を修正。その1件がこの欠陥 10月22日 Brotli の修正版を公開。Imperva が「Node2Shell」の題で RCE の実証記事を公開 10月23日 OpenSourceMalware が Python への波及を公表 10月24日 Envoy がアドバイザリを公開。Git for Windows に Issue #6514 が立つ Imperva はセキュリティ企業です。実証の対象が Node.js で、記事の題にも Node.js が入りました。そのため、「Node.js の脆弱性」として広まりました。 ところが、Node.js が直した中身は Brotli のコードです。Node.js は Brotli を自分のリポジトリに同梱していて、今回は Brotli の修正コミットを cherry-pick で取り込みました。cherry-pick は、別のリポジトリの修正部分だけを持ってくる操作です。 つまり Node.js は、同梱していた Brotli を直した側であって、欠陥の持ち主ではありません。 Python への波及を最初にまとめた OpenSourceMalware(OSM)も、この点を「この脆弱性は、そもそも Node.js のものではない」と書いています。 OSM の原文(英語) The vulnerability isn't really in Node.js at all Node.js 以外に対象になるもの(Python・Bun・Envoy・Git for Windows) 原因が Brotli のデコーダにあるので、同じデコーダを積んでいるものは確認が必要です。10月29日までに、次のものが分かっています。 Python: Brotli / brotlicffi パッケージ。どちらかが入っていると、urllib3 / requests、httpx(brotli extra)、aiohttp、Scrapy が br の応答を自動で展開します Bun: 1.4.4 で同梱の Brotli を更新しました Envoy / Istio: Brotli の展開(decompressor)を設定した構成が対象です curl: リンクしている Brotli のライブラリ次第です Git for Windows: 2.56.0.windows.3 までは、同梱の brotli が 1.2.0 のままでした。10月26日に公開された 2.56.0.windows.4 で 1.2.1 に更新されています このほかにも、対象として次のものが見つかっています。 .NET: 修正版は 8.0.33 / 9.0.22 / 10.0.14 です(.NET Framework は対象外)。BrotliStream などで自分で展開する処理が対象です。HttpClient の応答の自動展開と ASP.NET Core のリクエスト展開は、既定では無効です Go の andybalholm/brotli: Brotli を Go に移植したライブラリです。v1.2.7 に同じ修正が移植されました Linux の git の HTTPS 通信: git が使う libcurl が Brotli 付きでビルドされていれば対象です。oss-security への投稿によれば、多くのディストリの libcurl は Brotli 付きです Git for Windows のメンテナは、10月24日の Issue で次のように書いています。 昨日の時点では、これは「ただの」node2shell だと言われていた。だから Git for Windows は無関係に見えた。 メンテナの原文(英語、Git for Windows Issue #6514) Yesterday, the word on the street was still that this was "only" node2shell, and therefore Git for Windows looked unaffected. メンテナ自身も、名前を見て「自分は関係ない」と判断しかけていたわけです。「Node2Shell」という名前は、最初に話題になった場所を示しているだけで、影響範囲を示してはいません。 該当するかは「言語」ではなく2つの条件で決まる 対象の部品が入っていても、すぐに攻撃されるとは限りません。どの環境から先に上げるかは、次の2つの条件で決まります。 条件1: 信頼できない相手から来た Brotli データを展開するか。 典型的な経路は2つあります。 外部サーバーの応答を、Node.js の fetch() や Python の requests などで受け取る。応答の中身は相手のサーバーが決めます。攻撃者が用意したサーバーなら、壊れたデータを返せます 受け取ったリクエスト本文を、ミドルウェアが展開する。Imperva は、Express 5 / body-parser 2 の raw()・json() がリクエスト本文を展開する経路で実証しています 一方、素の Node.js の HTTP サーバーは、リクエストを受け取っただけでは本文を展開しません(Imperva の記事による)。 条件2: 同梱している Brotli が直った版か。 これは言語やツール本体の版で決まります。Brotli を自分で入れていなくても、ランタイムやツールの中に入っていることがあります。 特に先に上げるべきなのは、外部の URL を取ってくる機能を持つアプリです。Webhook、URL のプレビュー表示、クローラ、「この URL の内容を取ってきて」と頼める機能などがこれに当たります。 下の図の「Brotli デコーダを積んだもの」は、前の節で挙げた Node.js・Python の Brotli / brotlicffi・Bun・Envoy・Git for Windows などです。 自分は関係ある? 言語・ツール別の判定 ! 判定の前に、落とし穴が2つあります。 版番号: Node.js は修正を cherry-pick で取り込みました。そのため、修正版でも process.versions.brotli の表示は修正前と変わりません(lilting の検証では、22.23.4 でも 1.1.0 のまま)。Node.js では、Brotli の版ではなく node -v で Node.js 本体の版を見ます。ただし、ディストリが提供する nodejs はシステムの libbrotli を使うことがあります(Ubuntu・Debian の告知)。その場合は、ディストリの告知で判断します スキャナ: Node.js はランタイムそのものなので、npm audit には出ません。Python では、10月23日に OSM が「Brotli・brotlicffi の PyPI パッケージは、どの脆弱性データベースにも載っていない」と書きました。翌24日には、pip-audit が検出するようになっています(lilting の検証)。スキャナが何も言わないことを、安全の根拠にしないでください 対象 何を見るか OK の基準・対処 優先して上げるべき使い方 Node.js node -v 22・24・26 系は、それぞれ 22.23.4 / 24.22.1 / 26.12.1 以上。それ以外の系列(20 以下など)は修正されないので、サポート中の系列へ移る fetch() で外部 URL を取る。Express 5 / body-parser 2 で br の本文を受ける Python Brotli / brotlicffi が入っているか Brotli 1.2.1 / brotlicffi 1.2.1.0 以上。Linux ディストリビューション(以下ディストリ)が提供するパッケージは、ディストリの更新で上げる requests・httpx・aiohttp 等で外部 URL を取る Bun bun --version 1.4.4 以上。コンパイル済みの実行ファイルは作り直しが必要(Aikido の記述) 外部 URL を取る Envoy Brotli decompressor を設定しているか 1.40.0 / 1.39.3 / 1.38.6 / 1.37.8 / 1.36.12 以上 設定している構成すべて Istio Brotli の展開を有効にしているか(EnvoyFilter など) 1.31.2 / 1.30.6 / 1.29.9 以上(Istio の告知 ISTIO-SECURITY-2026-007) 有効にしている構成すべて Git for Windows git version 2.56.0.windows.4 以上(10月26日公開。同梱の brotli は 1.2.1) 信頼できない HTTPS サーバーへの clone / fetch curl どの Brotli にリンクしているかは、ビルドした配布元次第 配布元(OS・ディストリ・同梱しているツール)の更新情報を確認して上げる。Windows 標準の curl は Brotli 非対応(lilting の検証) --compressed などで、信頼できない URL の応答を展開させる Linux の libbrotli ディストリのセキュリティ情報 ディストリの更新を当てる。版番号を変えずに修正だけ移植する(バックポート)ことがあるので、版番号ではなくディストリの告知で判断する libbrotli を使うアプリが外部データを展開する Bun 1.4.4 のリリースノート(公式ブログ)は、Security の節で Brotli に触れていますが、CVE 番号は書かれていません。CVE 番号で探すと見落としやすいので注意してください。 主なディストリの対応状況は、10月29日時点で次のとおりです。 Ubuntu: brotli は jammy(22.04)・noble(24.04)などで修正済みです(USN-8956-1 / USN-8956-2)。jammy 以降の nodejs はシステムの brotli を使うため「影響なし」とされています。focal(20.04)は、brotli が「評価中(Needs evaluation)」、Brotli を同梱する nodejs が「vulnerable」のままです Debian: brotli は bookworm(DLA-4855-1)と trixie(DSA-6582-1)で修正済みです。ディストリ版の nodejs と trixie の python-brotlicffi は、トラッカー上はまだ「vulnerable」の表示です。ただし、トラッカーの注記では、どちらもシステムの brotli を使うとされています 同じディストリの中でも、パッケージごとに状況が違います。使っているパッケージごとに告知を確かめてください。 Node.js・Python・Git for Windows については、表に書ききれない確かめ方を補足します。 Node.js: node -v で本体の版を見る node -v Docker イメージの中の Node.js を見る例(<image> は自分のイメージ名に置き換える) docker run --rm <image> node -v 出力を表の基準と比べます。手元の Node.js だけでなく、Docker イメージや、Node.js を同梱して配っているバイナリの中の版も見てください(Imperva の推奨)。 Python: Brotli / brotlicffi が入っているかを見る 確かめたい venv を有効にしてから実行する python -m pip show Brotli brotlicffi 出力の Version: の行を、表の基準と比べます。 どちらも見つからなければ、requests などは br の応答を展開しません(OSM の解説と lilting の検証)。ただし、Brotli を別の形で同梱したライブラリや外部ツールを使っている場合は、そちらも確かめてください。また、venv ごとに入っているパッケージは違います。アプリごと・コンテナごとに見てください。 Git for Windows: git version で版を見る git version git version 2.56.0.windows.4 以上と表示されれば、修正版です。10月26日に公開され、Issue #6514 もクローズされました。 同梱の brotli の版は、Git for Windows のインストール先にある etc/package-versions.txt で確かめられます。Git Bash からは次のように見られます。 grep brotli /etc/package-versions.txt 64ビット版の修正版なら mingw-w64-ucrt-x86_64-brotli 1.2.1-1 と出る 上げられるまでの間は、信頼できない HTTPS サーバーへの clone / fetch を避けるのが安全です(lilting の見解)。 呼び名・深刻度・RCE の範囲・悪用、食い違いの答え合わせ ここまでで判定はできます。この節では、調べると記事ごとに食い違って見える4つの点を、一次情報で確かめます。 Node2Shell と HTTP2Shell は同じ1件 Node2Shell は Imperva の記事タイトルが元です。HTTP2Shell は、セキュリティ企業 Aikido の記事の見出し「Node2Shell evolves to HTTP2Shell」(Node2Shell が HTTP2Shell に進化した)に出てくる呼び方です。見出しは、HTTP を使うライブラリやアプリにも影響が広がったことを伝えるものです。ただし、Aikido は呼び名を変えた理由を本文で説明していません。検索すると Broccoli2Shell という呼び方も出てきます。どれも指しているのは CVE-2026-111497 の1件です。 もう1つ紛らわしいのが、2024年の別の脆弱性です。「Node.js Brotli 脆弱性」で検索すると、検索エンジンの AI 要約が CVE-2024-22025 を返すことがあります。これは2024年に見つかった、fetch() が Brotli データを展開するときにサービス停止(DoS)を起こす問題で、今回とは別件です。 High と Critical 9.2 は、評価した組織が違う 深刻度の点数は CVSS という共通の尺度で付けます。CVE を採番した Google は、CVSS 4.0 で 9.2(Critical)と評価しました。GitHub のアドバイザリと NVD(米国の脆弱性データベース)もこの値を載せています。 一方、Node.js は自分のリリース表でこの件を High と書きました。評価したのは Node.js 自身で、対象も Node.js という1つの製品です。評価した組織も、評価の対象にした製品も違うので、どちらも間違いではありません。 RCE の実証は広がったが、成否はビルドで違う 「素の Node.js で RCE できる」という言い方も出回っています。最初に Imperva が実証に使ったのは、公式の Node.js 24.22.0 Linux arm64 イメージです(経路は fetch() と Express 5 / body-parser 2)。成功までには何回か試す必要がありました。 その後、セキュリティ企業の ELLIO が、Node.js 24.22.0 の x86-64 でも RCE に成功したと報告しました。成功した構成は4つで、node:http のサーバーから node:zlib を呼んで本文を展開するもの、公式の slim イメージ、Express 5.3.0(express.json())、Koa 3(@koa/bodyparser)です。node:http の例は、フレームワークを挟まず、アプリのコードが自分で本文を展開する構成です。ELLIO も、素の Node.js の HTTP サーバーはリクエストを受け取っただけでは Brotli の本文を展開しないと書いています(条件1 と同じ)。「node:http を使っているだけで RCE される」という意味ではありません。 一方、24.22.0 用に調整した攻撃データは、22.23.3 と 26.12.0 では1万回試しても成功しませんでした。ELLIO は、これらの版には別の調整が要るとしています。攻撃を成功させるには、ビルドごとに作り込む必要があるということです。「どの環境でもすぐ RCE される」わけではありません。 Python については、RCE を試した報告はまだありません。OSM は、壊れたデータを渡すとデコーダが誤った結果を返すところまでを確かめています。 ただし、24.22.0 用の攻撃データが通らなかったからといって、ほかの版が安全だという証明にはなりません。判定で該当した環境は、実証の有無にかかわらず上げてください。 実際の攻撃は、すでに確認されている 10月25日の時点では、悪用の報告は見当たりませんでした。その後、状況が変わっています。CISA は10月27日に、この CVE を KEV(Known Exploited Vulnerabilities、実際に悪用が確認された脆弱性の一覧)に追加しました。米国の政府機関には、11月10日までの対応が求められています。 ELLIO も10月26日の追記で、自社のセンサーで実際の攻撃通信を観測したと報告しています。狙いは Node.js 24.22.0 の x86-64 で、ELLIO の検証では、観測した攻撃データそのものは成功しませんでした。攻撃を再現するコード(PoC)も公開されて出回っています。 ただし、CISA は KEV に追加した根拠となった具体的な事例を公表していません。どの製品のどの使い方が狙われたのかは、まだ分かりません。 まだ分かっていないこと 10月29日時点で、次の点はまだ決着していません。 Ubuntu の focal(20.04)は、修正版が出ていません。Debian のディストリ版 nodejs と trixie の python-brotlicffi は、トラッカー上「vulnerable」の表示が残っています Python で RCE が可能かどうかを検証した報告は、見当たりません CISA が KEV に追加した根拠の事例は公表されておらず、どこが狙われたのかは分かっていません ブラウザや、Node.js を内蔵するアプリ(Electron など)については、この記事では扱いません。影響の有無を示す一次情報が見当たらないためです。 おわりに Node2Shell の原因は Node.js ではなく Brotli の欠陥で、Node.js は直した側でした。呼び名が違っても指しているのは1件で、深刻度の違いは評価した組織の違いです。RCE の成否はビルドによって違いますが、実際の攻撃はすでに確認されています。 「Node.js は使っていないから関係ない」と思った方も、Python の venv と git version を一度確かめてみてください。判断の材料は「Node2Shell」という名前ではなく、手元にあるパッケージとツールの版です。 参考リンク Node.js: Security Releases: October 21, 2026 GitHub Advisory: GHSA-9v8h-qqgm-4m35(PyPI Brotli / brotlicffi) NVD: CVE-2026-111497 google/brotli: Releases Git for Windows Issue #6514: Bundled brotli is still 1.2.0 in 2.56.0.windows.3 Imperva: Node2Shell: From Brotli Data to Remote Code Execution OpenSourceMalware: Node2Shell affects Python Envoy: GHSA-3wvj-j6qm-62mr Istio: ISTIO-SECURITY-2026-007 Bun: Bun v1.4.4 Aikido: Node2Shell evolves to HTTP2Shell lilting: Brotli CVE-2026-111497 beyond Node.js, tested with Python, curl, and Git for Windows CISA: Known Exploited Vulnerabilities Catalog(CVE-2026-111497) ELLIO: Node2Shell(CVE-2026-111497)の検証記事 Git for Windows: v2.56.0.windows.4 Ubuntu: CVE-2026-111497 Debian Security Tracker: CVE-2026-111497 .NET: dotnet/announcements #494 oss-security: Brotli CVE-2026-111497 and git-remote-https andybalholm/brotli: v1.2.7 の更新履歴

  9. Где Runtime Async в .NET 11 экономит память, а где нет Всем привет, меня зовут Сергей Прощаев, я Tech Lead и руководитель направления Java | Kotlin разработки в FinTech & E-commerce и преподаю на курсах разработки и архитектуры в ОТУС. В этой статье расскажу про Runtime Async в .NET 11: что меняется, когда приостановкой async-методов занимается рантайм, а не код от компилятора, и где выигрыш пропадает. На собеседованиях по C# годами звучит один и тот же вопрос: «во что компилятор превращает async-метод?». Правильный ответ знают все: в структуру с конечным автоматом, методом MoveNext и билдером задачи. В .NET 11 у этого ответа появляется оговорка «если вы не включили runtime-async». Я прогнал бенчмарки команды .NET на RC2 сам: аллокации совпали с опубликованными до байта, но только при одном условии, о котором в анонсах не пишут. Сразу оговорюсь, почему про C# пишет человек с JVM. Мой основной стек — Java и Kotlin, и именно поэтому за асинхронностью в .NET интересно следить со стороны: в 2023 году Java выпустила виртуальные потоки, а команда .NET свой эксперимент с зелёными потоками отложила и пошла улучшать async/await. В ноябре ожидается релиз .NET 11, где Runtime Async уже есть, но как preview-функция: для своего кода её включают явно, а поведение проверяют на своей нагрузке. С изменениями в .NET 11 разобраться проще, если понимать, как async/await работает в классической реализации. Эту тему подробнее разберём 29 октября в 20:00 на бесплатном открытом уроке «Магические слова async/await». Три платформы, три ответа на один вопрос Задача у всех одна: поток не должен простаивать, пока сервис ждёт ответа от базы или соседнего API. Решают её по-разному. Компиляторы Kotlin и классического C# преобразуют асинхронные функции в конечные автоматы. Kotlin превращает каждую suspend-функцию в автомат с полем label и скрытым параметром Continuation. Компилятор C# делает почти то же: генерирует структуру, хранит в её полях локальные переменные, которые живут через await, и раскладывает тело метода по состояниям внутри MoveNext. JVM и CLR исполняют уже сгенерированный код и дают инфраструктуру продолжений, но сам автомат не строят. // (Kotlin) suspend-функция: компилятор добавит скрытый параметр Continuation // и превратит тело в конечный автомат с полем label suspend fun price(id: Int): Int { val base = loadBase(id) // точка приостановки, пример схематичный return base + tax(base) }sourcecraft.dev Java пошла другим путём. В JDK 21, вышедшем 19 сентября 2023 года, появились виртуальные потоки (JEP 444): код остаётся синхронным и блокирующим, а приостановкой и возобновлением занимается JVM, снимая стек виртуального потока с несущего. // (Java 21, схематично) код блокирующий, приостанавливает и возобновляет его JVM try (var executor = Executors.newVirtualThreadPerTaskExecutor()) { executor.submit(() -> callBlockingApi(id)); }sourcecraft.dev Команда .NET попробовала тот же путь. За одиннадцать дней до выхода JDK 21, 8 сентября 2023 года, она опубликовала итоги эксперимента с зелёными потоками. Вывод был сдержанным: зелёные потоки возможны, но это вторая модель программирования рядом с async/await, и они плохо уживаются друг с другом. Самая наглядная цифра из отчёта: 100 миллионов минимальных вызовов P/Invoke на зелёном потоке заняли около 1800 мс вместо 300 мс. Э ксперимент поставили на паузу и решили улучшать существующую модель. Следующим шагом стал эксперимент runtime-async в .NET 9, а итогом — Runtime Async в .NET 11. На схеме ниже три подхода сведены рядом, чтобы было видно, где живёт конечный автомат в каждом из них. Главная мысль схемы: .NET 11 занял середину. Программная модель остаётся прежней, с Task, await и окраской методов, но приостановку и возобновление организуют VM и JIT, у которых больше информации. Виртуальные потоки позволяют во многих сценариях писать блокирующий код без async-сигнатур по всему стеку вызовов. .NET от окраски методов не отказывается и выигрывает за счёт того, что ничего не надо переписывать. Что именно меняется под капотом В классической схеме компилятор C# для каждого async-метода выпускает метод-обёртку, тип конечного автомата, поля для захваченного состояния и тело MoveNext. Упрощённо это выглядит так: // (C#, упрощённо) что компилятор делает из async-метода без runtime-async public Task<int> GetPriceAsync(int id) { var sm = new GetPriceAsyncStateMachine { Id = id, State = -1 }; sm.Builder = AsyncTaskMethodBuilder<int>.Create(); sm.Builder.Start(ref sm); return sm.Builder.Task; } struct GetPriceAsyncStateMachine : IAsyncStateMachine { public int State; public int Id; public AsyncTaskMethodBuilder<int> Builder; private TaskAwaiter<int> _awaiter; public void MoveNext() { // switch по State, получение awaiter, // AwaitUnsafeOnCompleted при незавершённой задаче, SetResult в конце } public void SetStateMachine(IAsyncStateMachine stateMachine) { } }sourcecraft.dev С runtime-async компилятор не генерирует пару IAsyncStateMachine и MoveNext. Он оставляет компактный IL, ставит методу флаг async (MethodImplOptions.Async, значение 0x2000 по черновику изменений ECMA-335), а точки приостановки кодирует вызовами методов AsyncHelpers: для Task и ValueTask это перегрузки Await, для прочих awaiter-ов AwaitAwaiter и UnsafeAwaitAwaiter. Для оптимизации важнее всего пара «вызов async-метода и сразу подходящий Await». Приостановку, сохранение нужного состояния и возобновление на другом потоке организуют VM и JIT. Так что меняется не столько место, где строится прежний автомат, сколько распределение ответственности, и поэтому JIT может оптимизировать границы между async-методами, а не только код внутри каждого. Включается фича свойством в файле проекта, выключается для проекта другим свойством. В .NET 11 это по-прежнему preview: <!-- (XML, csproj) включение Runtime Async для своего кода --> <PropertyGroup> <TargetFramework>net11.0</TargetFramework> <Features>$(Features);runtime-async=on</Features> </PropertyGroup> <!-- (XML, csproj) отключение для проекта --> <PropertyGroup> <UseRuntimeAsync>false</UseRuntimeAsync> </PropertyGroup>sourcecraft.dev Есть деталь, которую легко пропустить: сами библиотеки .NET 11 уже собраны с runtime-async=on и конечных автоматов от компилятора больше не содержат. Ваш код при этом может оставаться в классическом режиме, обе модели работают вместе. Получается, что, обновившись на .NET 11, вы уже исполняете runtime-async внутри BCL, даже если флаг не трогали. Что это даёт: пять наблюдений Я бы предпочёл не принимать «быстрее и чище» на веру, поэтому собрал репозиторий dotnet-runtime-async-demo и прогнал его на .NET 11 RC2 (SDK 11.0.100-rc.2) в GitHub Actions на ubuntu-24.04. Классическую схему для отдельного метода оставляет экспериментальный атрибут [RuntimeAsyncMethodGeneration(false)], как у Тауба, поэтому оба варианта работают на одном рантайме и отличаются только тем, как их собрал компилятор. Для сверки рядом цифры из документации .NET 11 и разбора производительности Стивена Тауба. Аллокации между прогонами стабильны, время скачет, поэтому опираюсь на байты. Наблюдение 1. Живые стек-трейсы стали короче в разы. Три вложенных async-метода, внутренний создаёт new StackTrace(). В классическом режиме у меня 13 кадров, из них 3 — AsyncMethodBuilderCore.Start. С runtime-async кадров 4, и это ровно цепочка вызовов. В документации 13 и 5: там Main тоже асинхронный, а мой пробник входит в цепочку из синхронного Main через GetAwaiter().GetResult(), так что точное число кадров зависит от точки входа: (text, вывод с runtime-async=on, results/stack_runtime.txt) at Runtime.InnerAsync() at Runtime.MiddleAsync() at Runtime.OuterAsync() at Program.<Main>$(String[] args)sourcecraft.dev Важная оговорка из той же документации: стек-трейсы исключений из catch выглядят одинаково в обоих режимах, их давно чистит ExceptionDispatchInfo. Выигрывают профилировщики, отладчик и всё, что смотрит на стек во время работы. С виртуальными потоками Java сходство здесь концептуальное: при реальной приостановке await физический стек в .NET по-прежнему разматывается, Runtime Async оптимизирует существующую модель, а не заменяет её стековыми потоками. Наблюдение 2. Когда Runtime Async устраняет промежуточные задачи. Самый интересный механизм — слияние вызовов. Если A ждёт B, а B ждёт C, в классической схеме у каждого метода свой сгенерированный автомат и свой результат-задача. Автомат при этом не обязательно попадает в кучу: на синхронном пути структура живёт на стеке, а платим мы в основном за задачи-результаты. JIT с runtime-async распознаёт пару «вызов метода, возвращающего Task, и сразу await» и передаёт результат дальше обычным значением. Задача материализуется только на внешней границе, где её действительно кто-то ждёт как объект. На схеме ниже эта разница показана на цепочке из трёх методов. Главная мысль схемы: контракт Task никуда не делся, он переехал туда, где задача действительно нужна. На рисунке благоприятный случай: вся цепочка собрана в новом режиме и вызовы идут подряд. В смешанной цепочке с классическими методами или когда задачу кладут в коллекцию и вешают продолжение руками, объект по-прежнему создаётся. Бенчмарк Тауба с двумя уровнями вложенности я повторил в BenchmarkDotNet 0.16.0-preview.2 с MemoryDiagnoser, в обычном режиме, когда каждый бенчмарк идёт в отдельном процессе: Сценарий Аллокации у меня Аллокации у Тауба Цепочка завершилась синхронно, классический async 144 B 144 B Цепочка завершилась синхронно, runtime-async 0 B 0 B Цепочка с Task.Yield, классический async 248 B 248 B Цепочка с Task.Yield, runtime-async 168 B 168 B Байты совпали до единицы. По времени у меня синхронная цепочка ускорилась с 26,95 до 10,72 нс, у Тауба с 21,221 до 6,151 нс: порядок тот же, машины разные. Наблюдение 3. Глубокие цепочки с исключением дешевеют на порядок. Рекурсивная цепочка бросает исключение на дне. На глубине 30 без приостановки классический вариант аллоцирует 84,2 KB, runtime-async — 5,71 KB, у меня и у Тауба одинаково. По времени у меня 56,6 мкс против 13,0 мкс. Пример синтетический: в реальном сервисе картину изменят локальная обработка исключений, логирование, повторы и настоящие точки приостановки. Наблюдение 4. Сборка становится вдвое меньше. Приложение из десяти вложенных async-методов в классическом режиме дало SizeProbe.dll размером 10 752 байта, а с runtime-async — 5632 байта, байт в байт как в разборе. Для одного сервиса это мелочь, в крупном приложении с множеством своих async-методов эффект может накапливаться. Размер итогового NativeAOT-образа и готовых сторонних сборок я не мерил, так что количественного вывода для них не делаю. Наблюдение 5. Продолжения перестали таскать пустой контекст. Это изменение не про флаг, по документации оно работает в .NET 11 для всех. Раньше каждое продолжение снимало и восстанавливало ExecutionContext со значениями AsyncLocal, даже когда восстанавливать было нечего. Теперь рантайм этот цикл пропускает. Пункт из документации, я его не мерил: в сервисе с трассировкой и областями логирования контекст часто непустой, и эффект там, по моей гипотезе, скромный. Где выигрыш пропадает Первое, и для меня главное: результат зависит от того, как запущен замер. Сначала я гонял те же бенчмарки с флагом --inProcess, когда BenchmarkDotNet исполняет их в своём процессе, и написал свой пробник на GC.GetTotalAllocatedBytes, который тоже работает в одном процессе. Оба показали другую картину: Сценарий, в одном процессе Классический async Runtime-async Цепочка завершилась синхронно 144 B 72 B Цепочка с Task.Yield 192 B 240 B Причину расхождения я не установил, поэтому не считаю эти цифры свойством самой реализации. Методики тоже не одинаковые: мой пробник считает аллокации всего процесса, включая пул потоков, а не одного вызова. Оба режима шли на одной модели процессора, AMD EPYC 9V74, так что разница в железе маловероятна. Для выводов об аллокациях беру стандартный запуск BenchmarkDotNet в отдельном процессе: эти значения совпали с опубликованными. Абсолютное время отличается между окружениями, и я считаю его результатом конкретного прогона, а не свойством .NET 11. Внутрипроцессные цифры оставляю отдельным наблюдением, которое ещё надо проверить. Практический вывод: сравнивайте замеры, запущенные одинаково, на одной версии SDK и рантайма, иначе вы меряете методику, а не механизм. Время на коротких цепочках вообще ненадёжно. У Тауба исключение на глубине 1 с приостановкой в runtime-async медленнее в 1,3 раза, а у меня быстрее: 7,709 мкс против 6,438 мкс. Байты в обоих прогонах те же: 1,68 KB против 1,4 KB. Второе: это микробенчмарки. Типовой запрос ждёт базу миллисекунды, а экономия измеряется наносекундами. Могу себе представить нагрузочный тест I/O-bound API, где с флагом латентность не изменится, и это будет честный результат. Заметнее эффект там, где много синхронно завершающихся await (кэши, парсеры, глубокие конвейеры), и в метриках GC, а не во времени ответа. Третье: фича в preview. Документация прямо просит присылать отчёты о пропускной способности и размере библиотек в обе стороны, положительные и отрицательные. До стабилизации производительность и детали поведения могут меняться от сборки к сборке. Переменные окружения DOTNET_RuntimeAsync и UNSUPPORTED_RuntimeAsync больше не поддерживаются, включение теперь через Features, отключение через UseRuntimeAsync=false. Четвёртое: всё, что опирается на внутренности старой схемы, стоит проверить. Кадров билдера в живом стеке у меня стало 0 вместо 3, и если в коде или в инструментах кто-то разбирает стек по именам вида <Method>d__0 или по кадрам MoveNext, их больше не найдут. Я бы проверил: самописные логгеры, которые склеивают стек в строку; правила группировки ошибок в системе мониторинга; диагностику, которая ищет конечный автомат через reflection. Пятое: поддержка не сплошная. Runtime-async работает с четырьмя типами возврата: Task, ValueTask, Task<T> и ValueTask<T>. По разбору Тауба, async void, async-итераторы и собственные task-like типы со своими билдерами пока остаются на классическом преобразовании. Для пограничных случаев, вроде by-ref локальных переменных и await в обработчиках исключений, сверяться стоит с поведением текущего компилятора, а не только с черновиком ECMA-335. Что проверить у себя Мой вариант порядка, если сервис уже на .NET 10 и переезд на .NET 11 запланирован: Обновить проект на .NET 11 без флага и снять базовые метрики: аллокации в секунду, паузы GC, p99 латентности. Часть выигрыша вы получите уже здесь, потому что BCL собрана в новом режиме. В отдельной ветке включить <Features>$(Features);runtime-async=on</Features> и прогнать те же замеры на той же версии SDK и рантайма, тем же способом. Так эффект обновления BCL отделяется от эффекта флага. Сравнивать аллокации, а не только время. Найти горячий путь с глубокой цепочкой await и написать на него бенчмарк BenchmarkDotNet с MemoryDiagnoser. Там будет видно, сливаются ли вызовы. Проверить всё, что читает стек: логирование, APM-агент, группировку ошибок. Если что-то ломается, лучше узнать до GA, а не после. Если встали ограничения, отключить фичу для проекта через <UseRuntimeAsync>false</UseRuntimeAsync> и оставить отчёт команде .NET. Сроки поджимают: по официальной политике поддержки .NET 8 и .NET 9 перестают поддерживаться 10 ноября 2026 года, а .NET 11 RC2 вышел 13 октября с go-live лицензией. Окно для экспериментов как раз сейчас. Вывод Самое интересное здесь, на мой взгляд, не наносекунды, а выбор архитектуры. Java с виртуальными потоками позволила во многих сценариях писать блокирующий код без async-сигнатур, переложив приостановку на JVM, хотя асинхронные API в экосистеме никуда не делись. Kotlin до сих пор строит конечный автомат в компиляторе, ровно как C# последние двенадцать лет. .NET в 2023 году отказался от зелёных потоков и пошёл третьим путём: модель программирования оставили прежней, а реализацию перенесли туда, где у неё больше информации. Для разработчика на C# это означает простую вещь: ответ «async — это синтаксический сахар над конечным автоматом» теперь неполный. Компилятор больше не выпускает привычный MoveNext, приостановку и возобновление организуют VM и JIT, и JIT может оптимизировать границы между вложенными async-методами, потому что видит цепочку вызовов. Для разработчика на JVM это хороший повод посмотреть на корутины Kotlin иначе: компиляторная схема не единственная, и платформа с похожей моделью только что показала, сколько она стоит. Ссылка на мой репозиторий с экспериментом Начать стоит с README.md: там все таблицы этой статьи, включая разницу между запуском в одном процессе и в отдельных. shared/Chains.cs — измеряемые цепочки. src/AsyncProbe/Program.cs — стек-трейсы и аллокации. bench/AsyncBench/Program.cs — бенчмарки. ./run.sh — собирает всё с нуля и сверяет результат с инвариантами из expected.txt. ИСТОЧНИКИРепозиторий с экспериментом What's new in .NET 11 runtime, Microsoft Learn, обновлено для RC2 Toub S., Performance Improvements in .NET 11, .NET Blog, 15.09.2026 Runtime-async, черновик изменений ECMA-335, dotnet/runtime Green Thread Experiment Results, dotnet/runtimelab #2398, 08.09.2023 .NET 9 Runtime Async Experiment, dotnet/runtime #94620 .NET and .NET Core Support Policy Announcing .NET 11 Release Candidate 2, .NET Blog, 13.10.2026 JDK 21, OpenJDK Kotlin Coroutines, KEEP Чтобы расти до уровня Senior C#-разработчика и браться за более сложные задачи, важно не только уверенно писать код, но и понимать, как работает .NET под капотом. На курсе «C#-разработчик. Продвинутый уровень» можно углубить эти знания, научиться находить узкие места в приложениях и принимать более обоснованные архитектурные решения. Пройдите вступительный тест, чтобы оценить свой текущий уровень, выявить пробелы и понять, какие навыки стоит подтянуть для следующего карьерного шага.

  10. Reddit · · r/dotnet · u/shanewas726 · dotnet/core

    CVE-2026-111497: a Brotli decoder bug forced an out-of-band .NET release (10.0.14) on Oct 25 Microsoft released .NET 10.0.14 on 25 October as an out-of-band security update, and the stated reason is CVE-2026-111497, an out-of-bounds write (CWE-787) in the Brotli decoder bundled with .NET. CVSS 8.1, publicly disclosed, exploitation marked more likely. Affected and fixed: - .NET 8 before 8.0.33 → 8.0.33 - .NET 9 before 9.0.22 → 9.0.22 - .NET 10 before 10.0.14 → 10.0.14 (SDK 10.0.114 or 10.0.403) - .NET Framework is not affected The same release completes the October 13 fixes for CVE-2026-70197 (SocketAsyncEventArgs) and CVE-2026-102870 (Blazor WebAssembly debugger), which had shipped incomplete, and adds CVE-2026-114148 for Linux source-build tarballs. Exposure needs a call path that decompresses attacker-controlled Brotli: request decompression with br enabled, HttpClient AutomaticDecompression including Brotli, or BrotliStream/BrotliDecoder on untrusted input. None of that is on by default, and response compression alone does not hit it. Until you update, dropping Brotli from those paths is a workable stopgap. Framework-dependent apps need the host runtime updated and restarted. Self-contained apps need a rebuild and republish, and the Blazor WebAssembly Server/DevServer packages have to be bumped separately because the shared runtime update does not cover them. Advisory: announcements#494 Release notes: github.com/dotnet/core/blob/main/release-notes/10…

    51 points · 11 comments

Achievements

Awards earned by the repositories dotnet owns. Select an award to embed its badge.

Gold
111st place finishes
Silver
132nd place finishes
Bronze
43rd place finishes
Awarded repositories
9Owned by dotnet now

Repositories

Ordered by gold, then silver, then bronze.

  1. coreclrBest: 1st by stars, 3,160 stars510
  2. aspnetcoreBest: 1st by stars, 2,140 stars300
  3. corefxBest: 1st by stars, 1,455 stars260
  4. wpfBest: 1st by stars, 880 stars120
  5. roslynBest: 2nd by PR authors, 19 PR authors020
  6. mauiBest: 2nd by stars, 1,021 stars011
  7. machinelearningBest: 2nd by stars, 678 stars010
  8. runtimeBest: 3rd by forks, 83 forks002
  9. winformsBest: 3rd by stars, 568 stars001

Every award

Top-three finishes of these repositories in completed trending days and weeks, newest first.

  1. roslyn
  2. aspnetcore
  3. aspnetcore
  4. aspnetcore
  5. maui
  6. maui
  7. runtime
  8. runtime

A repository earns an award for finishing first, second or third in a completed trending day or week, ranked by new stars, new forks or pull request authors. Awards follow a repository through renames and transfers and count toward its current owner. Weeks run Monday to Sunday, UTC.