Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

[インデックス 13580] ファイルの概要

このコミットは、Go言語のsyscallパッケージにおける重要なセキュリティ強化を目的としています。具体的には、システムコールに渡される文字列引数にヌル文字(\x00)が含まれている場合に、EINVAL(無効な引数)エラーを返すように変更することで、ヌルバイトインジェクション攻撃のリスクを排除します。

コミット

commit a108369c830db0b9a9f519fd346b8f593a4d7e14
Author: Alexey Borzenkov <snaury@gmail.com>
Date:   Sun Aug 5 17:24:32 2012 -0400

    syscall: return EINVAL when string arguments have NUL characters
    
    Since NUL usually terminates strings in underlying syscalls, allowing
    it when converting string arguments is a security risk, especially
    when dealing with filenames. For example, a program might reason that
    filename like "/root/..\x00/" is a subdirectory or "/root/" and allow
    access to it, while underlying syscall will treat "\x00" as an end of
    that string and the actual filename will be "/root/..", which might
    be unexpected. Returning EINVAL when string arguments have NUL in
    them makes sure this attack vector is unusable.
    
    R=golang-dev, r, bradfitz, fullung, rsc, minux.ma
    CC=golang-dev
    https://golang.org/cl/6458050

GitHub上でのコミットページへのリンク

https://github.com/golang/go/commit/a108369c830db0b9a9f519fd346b8f593a4d7e14

元コミット内容

Go言語のsyscallパッケージにおいて、システムコールに渡される文字列引数にヌル文字(\x00)が含まれている場合、EINVALエラーを返すように修正。これは、基盤となるシステムコールがヌル文字を文字列の終端として扱うため、ヌル文字を含む文字列を許可することがセキュリティリスクとなるためである。特にファイル名の場合、プログラムが「/root/..\x00/」のようなパスをサブディレクトリまたは「/root/」として解釈しアクセスを許可する可能性があるが、実際のシステムコールは「\x00」を文字列の終端とみなし、ファイル名が「/root/..」として扱われることで予期せぬ動作を引き起こす可能性がある。ヌル文字を含む文字列引数に対してEINVALを返すことで、この攻撃ベクトルを無効にする。

変更の背景

この変更の背景には、ヌルバイトインジェクション(Null Byte Injection)と呼ばれるセキュリティ脆弱性への対策があります。多くのC言語ベースのシステムコールやライブラリは、文字列をヌル終端(null-terminated)形式で扱います。これは、文字列の終端をヌル文字(\x00)で示すという慣習です。

Go言語のプログラムがシステムコールを呼び出す際、Goの文字列(Goの文字列は長さ情報を持つためヌル終端ではない)を、OSが期待するヌル終端文字列に変換する必要があります。この変換プロセスにおいて、Goの文字列内に意図しないヌル文字が含まれていた場合、OS側のシステムコールはそのヌル文字を文字列の終端と誤認識し、Goプログラムが意図したパスや引数とは異なる短い文字列として処理してしまう可能性があります。

コミットメッセージの例にあるように、"/root/..\x00/"というファイル名をGoプログラムがシステムコールに渡した場合、Goプログラムはこれを特定のサブディレクトリ内のファイルとして扱おうとします。しかし、OSのシステムコールは\x00で文字列が終端すると解釈し、結果的に"/root/.."というパスとして処理してしまう可能性があります。"/root/.."は親ディレクトリを指すため、攻撃者がこれを利用して、本来アクセスが許可されないはずのディレクトリやファイルにアクセスできてしまう、といったセキュリティ上の問題が発生する可能性があります。

このコミットは、このようなヌルバイトインジェクションによる意図しないパスの切り詰めや、それに伴う権限昇格、情報漏洩などのリスクを未然に防ぐために導入されました。

前提知識の解説

ヌル終端文字列 (Null-terminated string)

C言語や多くのUNIX系システムコールにおいて、文字列は一連の文字の後にヌル文字(ASCII値0、\x00)が続く形式で表現されます。ヌル文字は文字列の終端を示すマーカーとして機能し、文字列の長さはヌル文字が見つかるまで数えることで決定されます。

Go言語の文字列

Go言語の文字列は、バイトの読み取り専用スライスであり、長さ情報(len)と容量情報(cap)を内部に持っています。C言語のようにヌル文字で終端される必要はありません。この違いが、システムコールとの連携時に問題を引き起こす可能性があります。

システムコール (System Call)

オペレーティングシステム(OS)のカーネルが提供するサービスを、ユーザー空間のプログラムが利用するためのインターフェースです。ファイル操作(読み書き、作成、削除)、プロセス管理(起動、終了)、ネットワーク通信など、OSの機能にアクセスするために使用されます。Go言語のsyscallパッケージは、これらのOS固有のシステムコールをGoプログラムから呼び出すための機能を提供します。

EINVAL (Invalid argument)

システムコールが返すエラーコードの一つで、関数に渡された引数が無効であることを示します。このコミットでは、セキュリティ上の理由から、ヌル文字を含む文字列引数を「無効な引数」として扱うことで、システムコールが予期せぬ動作をするのを防ぎます。

ヌルバイトインジェクション (Null Byte Injection)

入力値の検証が不十分なアプリケーションにおいて、ヌル文字(\x00)を挿入することで、文字列処理の挙動を変化させる攻撃手法です。特にファイルパスやコマンド引数など、OSのシステムコールに渡される文字列に対して行われると、意図しないファイルへのアクセスやコマンド実行につながる可能性があります。

技術的詳細

このコミットの技術的な核心は、Goの文字列をシステムコールが期待する形式(バイトスライスまたはUTF-16エンコードされたユニットのポインタ)に変換する際に、ヌル文字の存在を厳密にチェックし、存在する場合はエラーを返すように変更した点です。

具体的には、以下の関数が変更または新規導入されました。

  • syscall.ByteSliceFromString(s string) ([]byte, error):
    • Goの文字列sをヌル終端のバイトスライスに変換します。
    • 文字列s内にヌル文字が含まれている場合、EINVALエラーを返します。
    • 以前のStringByteSlice関数は、ヌル文字が含まれている場合にパニックを起こす可能性がありましたが、この新しい関数はエラーを返すことで、より安全なエラーハンドリングを可能にします。
  • syscall.BytePtrFromString(s string) (*byte, error):
    • ByteSliceFromStringを利用して、ヌル終端バイトスライスの先頭へのポインタを返します。
    • 同様に、ヌル文字が含まれている場合はEINVALエラーを返します。
    • 以前のStringBytePtr関数を置き換えるものです。
  • syscall.UTF16FromString(s string) ([]uint16, error):
    • GoのUTF-8文字列sをUTF-16エンコードされたuint16スライスに変換し、終端にヌル文字を追加します。
    • 文字列s内にヌル文字が含まれている場合、EINVALエラーを返します。
    • Windows APIはUTF-16文字列を多用するため、この関数はWindows固有のシステムコールとの連携で重要です。
    • 以前のStringToUTF16関数を置き換えるものです。
  • syscall.UTF16PtrFromString(s string) (*uint16, error):
    • UTF16FromStringを利用して、UTF-16エンコードされたヌル終端文字列の先頭へのポインタを返します。
    • 同様に、ヌル文字が含まれている場合はEINVALエラーを返します。
    • 以前のStringToUTF16Ptr関数を置き換えるものです。
  • syscall.SlicePtrFromStrings(ss []string) ([]*byte, error):
    • 文字列スライスssを、ヌル終端バイトスライスのポインタのスライスに変換します。
    • スライス内のいずれかの文字列にヌル文字が含まれている場合、EINVALエラーを返します。
    • 以前のStringSlicePtr関数を置き換えるもので、exec関連のシステムコールで引数リストを渡す際に使用されます。

これらの新しい関数は、ヌル文字チェックとエラー返却のロジックを内部に持ち、Goのシステムコールラッパーがこれらの関数を使用するように変更されました。これにより、Goプログラムが意図せずヌル文字を含む文字列をシステムコールに渡そうとした場合、実行時パニックではなく、明確なエラー(EINVAL)として捕捉できるようになります。

また、src/pkg/syscall/mksyscall.plsrc/pkg/syscall/mksyscall_windows.plというPerlスクリプトも変更されています。これらのスクリプトは、Goのsyscallパッケージのシステムコールラッパーコードを自動生成するために使用されます。この変更により、自動生成されるコードが、文字列引数を扱う際に新しい安全な関数(BytePtrFromStringUTF16PtrFromStringなど)を使用するように更新され、Goのシステムコール全体にわたってヌル文字チェックが強制されるようになりました。

コアとなるコードの変更箇所

このコミットの主要な変更は、src/pkg/syscall/syscall.gosrc/pkg/syscall/syscall_windows.go、そしてシステムコールラッパーを生成するスクリプトであるsrc/pkg/syscall/mksyscall.plsrc/pkg/syscall/mksyscall_windows.plに集中しています。

src/pkg/syscall/syscall.go

このファイルでは、Unix系システムコールで一般的に使用されるバイトスライスおよびバイトポインタ変換関数が変更されています。

// StringByteSlice is deprecated. Use ByteSliceFromString instead.
// If s contains a NUL byte this function panics instead of
// returning an error.
func StringByteSlice(s string) []byte {
	a, err := ByteSliceFromString(s)
	if err != nil {
		panic("syscall: string with NUL passed to StringByteSlice")
	}
	return a
}

// ByteSliceFromString returns a NUL-terminated slice of bytes
// containing the text of s. If s contains a NUL byte at any
// location, it returns (nil, EINVAL).
func ByteSliceFromString(s string) ([]byte, error) {
	for i := 0; i < len(s); i++ {
		if s[i] == 0 {
			return nil, EINVAL
		}
	}
	a := make([]byte, len(s)+1)
	copy(a, s)
	return a, nil
}

// StringBytePtr is deprecated. Use BytePtrFromString instead.
// If s contains a NUL byte this function panics instead of
// returning an error.
func StringBytePtr(s string) *byte { return &StringByteSlice(s)[0] }

// BytePtrFromString returns a pointer to a NUL-terminated array of
// bytes containing the text of s. If s contains a NUL byte at any
// location, it returns (nil, EINVAL).
func BytePtrFromString(s string) (*byte, error) {
	a, err := ByteSliceFromString(s)
	if err != nil {
		return nil, err
	}
	return &a[0], nil
}

StringByteSliceStringBytePtrは非推奨となり、ヌル文字チェックとエラー返却を行うByteSliceFromStringBytePtrFromStringが導入されました。非推奨関数は、互換性のために残されていますが、内部で新しい関数を呼び出し、エラーが発生した場合はパニックを発生させるようになっています。

src/pkg/syscall/syscall_windows.go

Windows固有のUTF-16エンコーディングを扱う関数が変更されています。

// StringToUTF16 is deprecated. Use UTF16FromString instead.
// If s contains a NUL byte this function panics instead of
// returning an error.
func StringToUTF16(s string) []uint16 {
	a, err := UTF16FromString(s)
	if err != nil {
		panic("syscall: string with NUL passed to StringToUTF16")
	}
	return a
}

// UTF16FromString returns the UTF-16 encoding of the UTF-8 string
// s, with a terminating NUL added. If s contains a NUL byte at any
// location, it returns (nil, EINVAL).
func UTF16FromString(s string) ([]uint16, error) {
	for i := 0; i < len(s); i++ {
		if s[i] == 0 {
			return nil, EINVAL
		}
	}
	return utf16.Encode([]rune(s + "\x00")), nil
}

// StringToUTF16Ptr is deprecated. Use UTF16PtrFromString instead.
// If s contains a NUL byte this function panics instead of
// returning an error.
func StringToUTF16Ptr(s string) *uint16 { return &StringToUTF16(s)[0] }

// UTF16PtrFromString returns pointer to the UTF-16 encoding of
// the UTF-8 string s, with a terminating NUL added. If s
// contains a NUL byte at any location, it returns (nil, EINVAL).
func UTF16PtrFromString(s string) (*uint16, error) {
	a, err := UTF16FromString(s)
	if err != nil {
		return nil, err
	}
	return &a[0], nil
}

StringToUTF16StringToUTF16Ptrも非推奨となり、ヌル文字チェックとエラー返却を行うUTF16FromStringUTF16PtrFromStringが導入されました。

src/pkg/syscall/mksyscall.pl および src/pkg/syscall/mksyscall_windows.pl

これらのPerlスクリプトは、Goのシステムコールラッパーを生成する際に、文字列引数を扱う部分のコード生成ロジックを変更しています。以前はStringBytePtrStringToUTF16Ptrを直接呼び出していましたが、変更後は、システムコール関数がエラーを返す場合(err戻り値がある場合)は、新しく導入された安全な関数(BytePtrFromStringUTF16PtrFromString)を使用し、そのエラーを適切に処理するコードを生成するようになりました。これにより、Goのシステムコール呼び出し全体でヌル文字チェックが自動的に組み込まれるようになります。

例えば、mksyscall.plでは、以下のような変更が加えられています。

# Prepare arguments to Syscall.
my @args = ();
my $n = 0;
foreach my $p (@in) {
	my ($name, $type) = parseparam($p);
	if($type =~ /^\*/) {
		push @args, "uintptr(unsafe.Pointer($name))";
	} elsif($type eq "string" && $errvar ne "") { # <-- ここが変更点
		$text .= "\\tvar _p$n *byte\\n";
		$text .= "\\t_p$n, $errvar = BytePtrFromString($name)\\n";
		$text .= "\\tif $errvar != nil {\\n\\t\\treturn\\n\\t}\\n";
		push @args, "uintptr(unsafe.Pointer(_p$n))";
		$n++;
	} elsif($type eq "string") {
		print STDERR "$ARGV:$.: $func uses string arguments, but has no error return\\n";
		$text .= "\\tvar _p$n *byte\\n";
		$text .= "\\t_p$n, _ = BytePtrFromString($name)\\n";
		push @args, "uintptr(unsafe.Pointer(_p$n))";
		$n++;
	} elsif($type =~ /^\[\](.*)/) {
		# Convert slice into pointer, length.
		# Have to be careful not to take address of &a[0] if len == 0:

$errvar ne ""の条件が追加され、システムコールがエラーを返す場合に、BytePtrFromStringからのエラーを適切に処理するコードが生成されるようになっています。

コアとなるコードの解説

このコミットの核心は、Goの文字列とOSのシステムコールが期待するヌル終端文字列との間の安全な変換レイヤーを導入したことです。

  1. ヌル文字の検出とエラー返却: 新しいByteSliceFromStringBytePtrFromStringUTF16FromStringUTF16PtrFromString関数は、入力文字列をバイトスライスまたはUTF-16スライスに変換する前に、文字列内にヌル文字(\x00)が含まれていないかを厳密にチェックします。もしヌル文字が検出された場合、これらの関数はnilsyscall.EINVALエラーを返します。これにより、Goプログラムはシステムコールを呼び出す前に、不正な入力値を検知し、適切なエラーハンドリングを行うことができます。

  2. 自動生成コードへの組み込み: mksyscall.plmksyscall_windows.plスクリプトの変更により、Goのsyscallパッケージ内のすべてのシステムコールラッパーが、文字列引数を扱う際にこれらの新しい安全な変換関数を使用するように自動的に更新されます。これにより、開発者が個々のシステムコール呼び出しでヌル文字チェックを意識する必要がなくなり、Goのシステムコールインターフェース全体で一貫したセキュリティ対策が適用されます。

  3. セキュリティ強化: この変更は、ヌルバイトインジェクションという既知のセキュリティ脆弱性に対する直接的な対策となります。ファイルパスやその他の重要な文字列引数にヌル文字が挿入されることで、システムコールが意図しない動作をするリスクを排除し、Goアプリケーションの堅牢性とセキュリティを向上させます。

  4. 後方互換性と非推奨化: 既存のStringByteSliceStringBytePtrStringToUTF16StringToUTF16Ptr関数は、即座に削除されるのではなく、非推奨(deprecated)としてマークされています。これらの非推奨関数は、内部で新しい安全な関数を呼び出し、ヌル文字が検出された場合にはパニックを発生させるように変更されています。これにより、既存のコードベースがすぐに壊れることを防ぎつつ、開発者には新しい安全な関数への移行を促しています。

このコミットは、Go言語がシステムプログラミングにおいて安全性を重視している姿勢を示す良い例であり、低レベルなOSインターフェースとの連携における潜在的なセキュリティリスクを体系的に解決しています。

関連リンク

参考にした情報源リンク