[インデックス 13676] ファイルの概要
このコミットは、Go言語の標準ライブラリであるnetパッケージにおける、RemoteAddr().String()メソッドチェーン呼び出し時のnilポインタデリファレンスを防ぐための修正です。特に、ソケットのアドレス情報(ローカルアドレスやリモートアドレス)が取得できない場合に発生する可能性のあるクラッシュを回避します。
コミット
commit 6cf77f2af483b562e76556aec1f51ece00756d34
Author: Mikio Hara <mikioh.mikioh@gmail.com>
Date: Thu Aug 23 20:54:00 2012 +0900
net: avoid nil pointer dereference when RemoteAddr.String method chain is called
Fixes #3721.
R=dave, rsc
CC=golang-dev
https://golang.org/cl/6395055
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/6cf77f2af483b562e76556aec1f51ece00756d34
元コミット内容
net: avoid nil pointer dereference when RemoteAddr.String method chain is called
このコミットは、RemoteAddr().String()のようなメソッドチェーンが呼び出された際に発生する可能性のあるnilポインタデリファレンスを回避することを目的としています。これはIssue #3721を修正するものです。
変更の背景
Go言語のnetパッケージでは、ネットワーク接続のローカルアドレス(LocalAddr())やリモートアドレス(RemoteAddr())を取得する機能が提供されています。これらのメソッドはnet.Addrインターフェースを返します。通常、このインターフェースは具体的なアドレス型(例: *net.TCPAddr, *net.UDPAddr, *net.IPAddrなど)の実体へのポインタを保持しており、そのString()メソッドを呼び出すことでアドレスの文字列表現を得ることができます。
しかし、特定の条件下(例えば、ソケットがまだ完全に接続されていない、またはピアアドレスが存在しない場合など)において、syscall.Getsocknameやsyscall.Getpeernameといったシステムコールがnilのsyscall.Sockaddrを返すことがありました。これまでの実装では、このnilのsyscall.SockaddrがそのままtoAddr関数(syscall.Sockaddrをnet.Addrに変換する関数)に渡され、結果としてnet.Addrインターフェースがnilのポインタをラップした状態になることがありました。
このような状態でRemoteAddr().String()のようにString()メソッドが呼び出されると、インターフェースがラップしているポインタがnilであるため、nilポインタデリファレンスが発生し、プログラムがクラッシュする原因となっていました。Issue #3721はこの問題が報告されたもので、このコミットはその修正を目的としています。
前提知識の解説
- Go言語の
netパッケージ: ネットワークI/O機能を提供するGoの標準ライブラリです。TCP/UDP接続、IPアドレスの解決、ソケット操作など、ネットワークプログラミングの基本的な機能を提供します。 net.Connインターフェース: ネットワーク接続を表す基本的なインターフェースで、LocalAddr()とRemoteAddr()メソッドを持ち、それぞれローカルとリモートのエンドポイントアドレスをnet.Addrインターフェースとして返します。net.Addrインターフェース: ネットワークアドレスの抽象化を表すインターフェースで、Network()とString()メソッドを持ちます。String()メソッドはアドレスの文字列表現を返します。netFD構造体:netパッケージ内部で、ファイルディスクリプタ(ソケット)を抽象化するために使用される構造体です。ソケットのファイルディスクリプタ(sysfd)、ネットワークタイプ、ソケットタイプなどの情報を含みます。syscall.Sockaddrインターフェース: Goのsyscallパッケージで定義されている、OSのソケットアドレス構造体(例:syscall.SockaddrInet4,syscall.SockaddrInet6,syscall.SockaddrUnixなど)を抽象化するインターフェースです。syscall.Getsockname(fd int): 指定されたファイルディスクリプタfdに関連付けられたソケットのローカルアドレスを取得するシステムコールです。成功するとsyscall.Sockaddrを返します。syscall.Getpeername(fd int): 指定されたファイルディスクリプタfdに関連付けられたソケットのピア(リモート)アドレスを取得するシステムコールです。成功するとsyscall.Sockaddrを返します。- nilポインタデリファレンス: プログラムがnil(何も指していない)ポインタを逆参照しようとしたときに発生するランタイムエラーです。Goではパニック(panic)を引き起こし、プログラムが異常終了します。
- インターフェースとnil: Goのインターフェースは、型と値のペアで構成されます。インターフェース変数がnilであるのは、その型と値の両方がnilの場合のみです。しかし、インターフェースが非nilの型(例:
*net.TCPAddr)を保持していても、その型が指す値(ポインタ)がnilである場合があります。この場合、インターフェース自体は非nilですが、その内部のポインタはnilであり、そのポインタをデリファレンスしようとするとパニックが発生します。
技術的詳細
このコミットの技術的詳細は、syscall.Getsocknameやsyscall.Getpeernameがnilのsyscall.Sockaddrを返す可能性を考慮し、net.Addrインターフェースの生成ロジックを堅牢にすることにあります。
以前の実装では、syscall.Getsocknameやsyscall.Getpeernameがエラーなしでnilのsyscall.Sockaddrを返した場合、そのnil値が直接toAddr関数に渡されていました。toAddr関数はsyscall.Sockaddrをnet.Addrに変換する役割を担いますが、nilが渡された場合のハンドリングが不十分でした。結果として、net.Addrインターフェースが、例えば(*net.TCPAddr)(nil)のような、型は存在するが値がnilであるポインタをラップした状態になることがありました。
このコミットでは、以下の新しいヘルパー関数を導入することでこの問題を解決しています。
-
localSockname(fd *netFD, toAddr func(syscall.Sockaddr) Addr) Addr:syscall.Getsockname(fd.sysfd)を呼び出してローカルソケットアドレスを取得します。- 取得した
syscall.Sockaddrがnilの場合、nullProtocolAddr関数を呼び出して、ソケットのファミリーとタイプに応じたnilのnet.Addrインターフェース(例:(*TCPAddr)(nil))を返します。 - nilでない場合は、通常の
toAddr関数でnet.Addrに変換して返します。
-
remoteSockname(fd *netFD, toAddr func(syscall.Sockaddr) Addr) Addr:syscall.Getpeername(fd.sysfd)を呼び出してリモートソケットアドレスを取得します。- 取得した
syscall.Sockaddrがnilの場合、nullProtocolAddr関数を呼び出して、ソケットのファミリーとタイプに応じたnilのnet.Addrインターフェースを返します。 - nilでない場合は、通常の
toAddr関数でnet.Addrに変換して返します。
-
nullProtocolAddr(f, t int) Addr:- ソケットのファミリー(
f、例:syscall.AF_INET)とタイプ(t、例:syscall.SOCK_STREAM)に基づいて、適切な型のnilポインタをnet.Addrインターフェースとして返します。 - 例えば、TCPストリームソケットであれば
(*TCPAddr)(nil)を、UDPデータグラムソケットであれば(*UDPAddr)(nil)を返します。これにより、net.Addrインターフェースが常に有効な型情報を持つようにしつつ、アドレスが不明な場合はその値がnilであることを明示します。
- ソケットのファミリー(
これらの変更により、netFD.setAddrに渡されるladdr(ローカルアドレス)やraddr(リモートアドレス)が、syscall.Sockaddrがnilの場合でも、適切な型のnilのnet.Addrインターフェースとして設定されるようになります。これにより、後続のAddr.String()呼び出し時にnilポインタデリファレンスが発生するのを防ぎます。(*SomeAddr)(nil).String()は、Goのインターフェースの特性により、パニックを起こさずに空文字列を返すか、適切なデフォルト値を返すように実装されています。
また、この修正には、ipraw_test.goとudp_test.goに新しいテストケースが追加されており、LocalAddr().String()とRemoteAddr().String()がnilポインタデリファレンスを引き起こさないことを確認しています。
コアとなるコードの変更箇所
このコミットで変更された主要なファイルとコードの変更点は以下の通りです。
-
src/pkg/net/fd.go:netFD.accept関数内で、netfd.setAddrを呼び出す際に、ローカルアドレスの取得にsyscall.Getsocknameの代わりに新しく導入されたlocalSocknameヘルパー関数を使用するように変更されました。--- a/src/pkg/net/fd.go +++ b/src/pkg/net/fd.go @@ -612,7 +612,7 @@ func (fd *netFD) accept(toAddr func(syscall.Sockaddr) Addr) (netfd *netFD, err e if netfd, err = newFD(s, fd.family, fd.sotype, fd.net); err != nil { closesocket(s) return nil, err } - lsa, _ := syscall.Getsockname(netfd.sysfd) - netfd.setAddr(toAddr(lsa), toAddr(rsa)) + netfd.setAddr(localSockname(fd, toAddr), toAddr(rsa)) return netfd, nil }
-
src/pkg/net/file.go:newFileFD関数内で、ローカルアドレスとリモートアドレスの取得にそれぞれlocalSocknameとremoteSocknameヘルパー関数を使用するように変更されました。--- a/src/pkg/net/file.go +++ b/src/pkg/net/file.go @@ -53,16 +53,14 @@ func newFileFD(f *os.File) (*netFD, error) { toAddr = sockaddrToUnixpacket } } - laddr := toAddr(sa) - sa, _ = syscall.Getpeername(fd) - raddr := toAddr(sa) + laddr := toAddr(lsa) netfd, err := newFD(fd, family, sotype, laddr.Network()) if err != nil { closesocket(fd) return nil, err } - netfd.setAddr(laddr, raddr) + netfd.setAddr(laddr, remoteSockname(netfd, toAddr)) return netfd, nil }FileConn関数内のswitch文で、UnixAddrのケースの順序が変更されました。これは機能的な変更ではなく、コードの整理です。
-
src/pkg/net/sock.go:socket関数の引数名がla,raからulsa,ursaに変更され、より明確になりました。- ローカルアドレスとリモートアドレスの取得と設定に、新しく導入された
localSocknameとremoteSocknameヘルパー関数が使用されるようになりました。--- a/src/pkg/net/sock.go +++ b/src/pkg/net/sock.go @@ -16,7 +16,7 @@ import ( var listenerBacklog = maxListenerBacklog() // Generic socket creation. -func socket(net string, f, t, p int, ipv6only bool, la, ra syscall.Sockaddr, toAddr func(syscall.Sockaddr) Addr) (fd *netFD, err error) { +func socket(net string, f, t, p int, ipv6only bool, ulsa, ursa syscall.Sockaddr, toAddr func(syscall.Sockaddr) Addr) (fd *netFD, err = nil) { // See ../syscall/exec.go for description of ForkLock. syscall.ForkLock.RLock() s, err := syscall.Socket(f, t, p) @@ -27,21 +27,18 @@ func socket(net string, f, t, p int, ipv6only bool, la, ra syscall.Sockaddr, toA syscall.CloseOnExec(s) syscall.ForkLock.RUnlock() - err = setDefaultSockopts(s, f, t, ipv6only) - if err != nil { + if err = setDefaultSockopts(s, f, t, ipv6only); err != nil { closesocket(s) return nil, err } - var bla syscall.Sockaddr - if la != nil { - bla, err = listenerSockaddr(s, f, la, toAddr) - if err != nil { + var blsa syscall.Sockaddr + if ulsa != nil { + if blsa, err = listenerSockaddr(s, f, ulsa, toAddr); err != nil { closesocket(s) return nil, err } - err = syscall.Bind(s, bla) - if err != nil { + if err = syscall.Bind(s, blsa); err != nil { closesocket(s) return nil, err } @@ -52,8 +49,8 @@ func socket(net string, f, t, p int, ipv6only bool, la, ra syscall.Sockaddr, toA return nil, err } - if ra != nil { - if err = fd.connect(ra); err != nil { + if ursa != nil { + if err = fd.connect(ursa); err != nil { closesocket(s) fd.Close() return nil, err @@ -61,17 +58,13 @@ func socket(net string, f, t, p int, ipv6only bool, la, ra syscall.Sockaddr, toA fd.isConnected = true } - sa, _ := syscall.Getsockname(s) var laddr Addr - if la != nil && bla != la { - laddr = toAddr(la) + if ulsa != nil && blsa != ulsa { + laddr = toAddr(ulsa) } else { - laddr = toAddr(sa) + laddr = localSockname(fd, toAddr) } - sa, _ = syscall.Getpeername(s) - raddr := toAddr(sa) - - fd.setAddr(laddr, raddr) + fd.setAddr(laddr, remoteSockname(fd, toAddr)) return fd, nil } @@ -85,3 +78,39 @@ func genericReadFrom(w io.Writer, r io.Reader) (n int64, err error) { // Use wrapper to hide existing r.ReadFrom from io.Copy.\n \treturn io.Copy(writerOnly{w}, r)\n }\n+\n+func localSockname(fd *netFD, toAddr func(syscall.Sockaddr) Addr) Addr {\n+\tsa, _ := syscall.Getsockname(fd.sysfd)\n+\tif sa == nil {\n+\t\treturn nullProtocolAddr(fd.family, fd.sotype)\n+\t}\n+\treturn toAddr(sa)\n+}\n+\n+func remoteSockname(fd *netFD, toAddr func(syscall.Sockaddr) Addr) Addr {\n+\tsa, _ := syscall.Getpeername(fd.sysfd)\n+\tif sa == nil {\n+\t\treturn nullProtocolAddr(fd.family, fd.sotype)\n+\t}\n+\treturn toAddr(sa)\n+}\n+\n+func nullProtocolAddr(f, t int) Addr {\n+\tswitch f {\n+\tcase syscall.AF_INET, syscall.AF_INET6:\n+\t\tswitch t {\n+\t\tcase syscall.SOCK_STREAM:\n+\t\t\treturn (*TCPAddr)(nil)\n+\t\tcase syscall.SOCK_DGRAM:\n+\t\t\treturn (*UDPAddr)(nil)\n+\t\tcase syscall.SOCK_RAW:\n+\t\t\treturn (*IPAddr)(nil)\n+\t\t}\n+\tcase syscall.AF_UNIX:\n+\t\tswitch t {\n+\t\tcase syscall.SOCK_STREAM, syscall.SOCK_DGRAM, syscall.SOCK_SEQPACKET:\n+\t\t\treturn (*UnixAddr)(nil)\n+\t\t}\n+\t}\n+\tpanic(\"unreachable\")\n+}\n ```
-
src/pkg/net/ipraw_test.go:TestIPConnAddrStringという新しいテスト関数が追加されました。このテストは、IPコネクションのLocalAddr().String()とRemoteAddr().String()がnilポインタデリファレンスを引き起こさないことを確認します。- 既存の
TestICMPテストのログメッセージが"test disabled; must be root"から"skipping test; must be root"に変更されました。
-
src/pkg/net/udp_test.go:TestUDPConnAddrStringという新しいテスト関数が追加されました。このテストは、UDPコネクションのLocalAddr().String()とRemoteAddr().String()がnilポインタデリファレンスを引き起こさないことを確認します。
コアとなるコードの解説
このコミットの核心は、netパッケージがソケットアドレスを取得する際に、システムコールがnilのsyscall.Sockaddrを返す可能性を適切に処理することです。
以前は、syscall.Getsocknameやsyscall.Getpeernameがnilのsyscall.Sockaddrを返した場合、そのnil値がそのままtoAddr関数に渡され、結果としてnet.Addrインターフェースが、例えば(*net.TCPAddr)(nil)のような、型は存在するが値がnilであるポインタをラップした状態になることがありました。Goのインターフェースは、型と値のペアで構成されるため、この場合インターフェース自体はnilではありませんが、その内部のポインタがnilであるため、そのポインタに対してメソッド(例: String())を呼び出すとnilポインタデリファレンスが発生し、パニックを引き起こしていました。
この修正では、localSocknameとremoteSocknameという2つのヘルパー関数が導入されました。これらの関数は、それぞれsyscall.Getsocknameとsyscall.Getpeernameのラッパーとして機能します。これらのラッパー関数は、システムコールがnilのsyscall.Sockaddrを返した場合に、直接toAddr関数に渡すのではなく、nullProtocolAddrという別のヘルパー関数を呼び出します。
nullProtocolAddr関数は、ソケットのファミリー(例: IPv4, IPv6, Unixドメイン)とタイプ(例: ストリーム, データグラム, RAW)に基づいて、適切な型のnilポインタをnet.Addrインターフェースとして生成します。例えば、TCPソケットであれば(*net.TCPAddr)(nil)を、UDPソケットであれば(*net.UDPAddr)(nil)を返します。
これにより、netFD.setAddrに渡されるローカルアドレスとリモートアドレスは、たとえシステムコールがアドレス情報を取得できなかった場合でも、常に適切な型情報を持つnet.Addrインターフェース(ただし、その内部のポインタはnil)として設定されるようになります。Goのインターフェースの設計上、(*net.TCPAddr)(nil).String()のような呼び出しはパニックを引き起こさず、通常は空文字列を返すか、その型に定義されたデフォルトの動作をします。このため、RemoteAddr().String()のようなメソッドチェーンが安全に実行できるようになり、nilポインタデリファレンスによるクラッシュが回避されます。
この変更は、Goのnetパッケージの堅牢性を高め、ネットワークプログラミングにおける一般的なエラーパターンの一つを解消する重要な修正です。
関連リンク
- Go Issue #3721: https://github.com/golang/go/issues/3721
- Go CL 6395055: https://golang.org/cl/6395055
参考にした情報源リンク
- Go言語の公式ドキュメント:
netパッケージ,syscallパッケージ - Go言語のソースコード:
src/pkg/net/ディレクトリ内の関連ファイル - Go言語のIssueトラッカー: Issue #3721の詳細な議論
- Go言語のコードレビューシステム (Gerrit): CL 6395055の変更内容とレビューコメント
[インデックス 13676] ファイルの概要
このコミットは、Go言語の標準ライブラリであるnetパッケージにおける、RemoteAddr().String()メソッドチェーン呼び出し時のnilポインタデリファレンスを防ぐための修正です。特に、ソケットのアドレス情報(ローカルアドレスやリモートアドレス)が取得できない場合に発生する可能性のあるクラッシュを回避します。
コミット
commit 6cf77f2af483b562e76556aec1f51ece00756d34
Author: Mikio Hara <mikioh.mikioh@gmail.com>
Date: Thu Aug 23 20:54:00 2012 +0900
net: avoid nil pointer dereference when RemoteAddr.String method chain is called
Fixes #3721.
R=dave, rsc
CC=golang-dev
https://golang.org/cl/6395055
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/6cf77f2af483b562e76556aec1f51ece00756d34
元コミット内容
net: avoid nil pointer dereference when RemoteAddr.String method chain is called
このコミットは、RemoteAddr().String()のようなメソッドチェーンが呼び出された際に発生する可能性のあるnilポインタデリファレンスを回避することを目的としています。これはIssue #3721を修正するものです。
変更の背景
Go言語のnetパッケージでは、ネットワーク接続のローカルアドレス(LocalAddr())やリモートアドレス(RemoteAddr())を取得する機能が提供されています。これらのメソッドはnet.Addrインターフェースを返します。通常、このインターフェースは具体的なアドレス型(例: *net.TCPAddr, *net.UDPAddr, *net.IPAddrなど)の実体へのポインタを保持しており、そのString()メソッドを呼び出すことでアドレスの文字列表現を得ることができます。
しかし、特定の条件下(例えば、ソケットがまだ完全に接続されていない、またはピアアドレスが存在しない場合など)において、syscall.Getsocknameやsyscall.Getpeernameといったシステムコールがnilのsyscall.Sockaddrを返すことがありました。これまでの実装では、このnilのsyscall.SockaddrがそのままtoAddr関数(syscall.Sockaddrをnet.Addrに変換する関数)に渡され、結果としてnet.Addrインターフェースがnilのポインタをラップした状態になることがありました。
このような状態でRemoteAddr().String()のようにString()メソッドが呼び出されると、インターフェースがラップしているポインタがnilであるため、nilポインタデリファレンスが発生し、プログラムがクラッシュする原因となっていました。Issue #3721はこの問題が報告されたもので、このコミットはその修正を目的としています。
前提知識の解説
- Go言語の
netパッケージ: ネットワークI/O機能を提供するGoの標準ライブラリです。TCP/UDP接続、IPアドレスの解決、ソケット操作など、ネットワークプログラミングの基本的な機能を提供します。 net.Connインターフェース: ネットワーク接続を表す基本的なインターフェースで、LocalAddr()とRemoteAddr()メソッドを持ち、それぞれローカルとリモートのエンドポイントアドレスをnet.Addrインターフェースとして返します。net.Addrインターフェース: ネットワークアドレスの抽象化を表すインターフェースで、Network()とString()メソッドを持ちます。String()メソッドはアドレスの文字列表現を返します。netFD構造体:netパッケージ内部で、ファイルディスクリプタ(ソケット)を抽象化するために使用される構造体です。ソケットのファイルディスクリプタ(sysfd)、ネットワークタイプ、ソケットタイプなどの情報を含みます。syscall.Sockaddrインターフェース: Goのsyscallパッケージで定義されている、OSのソケットアドレス構造体(例:syscall.SockaddrInet4,syscall.SockaddrInet6,syscall.SockaddrUnixなど)を抽象化するインターフェースです。syscall.Getsockname(fd int): 指定されたファイルディスクリプタfdに関連付けられたソケットのローカルアドレスを取得するシステムコールです。成功するとsyscall.Sockaddrを返します。syscall.Getpeername(fd int): 指定されたファイルディスクリプタfdに関連付けられたソケットのピア(リモート)アドレスを取得するシステムコールです。成功するとsyscall.Sockaddrを返します。- nilポインタデリファレンス: プログラムがnil(何も指していない)ポインタを逆参照しようとしたときに発生するランタイムエラーです。Goではパニック(panic)を引き起こし、プログラムが異常終了します。
- インターフェースとnil: Goのインターフェースは、型と値のペアで構成されます。インターフェース変数がnilであるのは、その型と値の両方がnilの場合のみです。しかし、インターフェースが非nilの型(例:
*net.TCPAddr)を保持していても、その型が指す値(ポインタ)がnilである場合があります。この場合、インターフェース自体は非nilですが、その内部のポインタはnilであり、そのポインタをデリファレンスしようとするとパニックが発生します。
技術的詳細
このコミットの技術的詳細は、syscall.Getsocknameやsyscall.Getpeernameがnilのsyscall.Sockaddrを返す可能性を考慮し、net.Addrインターフェースの生成ロジックを堅牢にすることにあります。
以前の実装では、syscall.Getsocknameやsyscall.Getpeernameがエラーなしでnilのsyscall.Sockaddrを返した場合、そのnil値が直接toAddr関数に渡されていました。toAddr関数はsyscall.Sockaddrをnet.Addrに変換する役割を担いますが、nilが渡された場合のハンドリングが不十分でした。結果として、net.Addrインターフェースが、例えば(*net.TCPAddr)(nil)のような、型は存在するが値がnilであるポインタをラップした状態になることがありました。
このコミットでは、以下の新しいヘルパー関数を導入することでこの問題を解決しています。
-
localSockname(fd *netFD, toAddr func(syscall.Sockaddr) Addr) Addr:syscall.Getsockname(fd.sysfd)を呼び出してローカルソケットアドレスを取得します。- 取得した
syscall.Sockaddrがnilの場合、nullProtocolAddr関数を呼び出して、ソケットのファミリーとタイプに応じたnilのnet.Addrインターフェース(例:(*TCPAddr)(nil))を返します。 - nilでない場合は、通常の
toAddr関数でnet.Addrに変換して返します。
-
remoteSockname(fd *netFD, toAddr func(syscall.Sockaddr) Addr) Addr:syscall.Getpeername(fd.sysfd)を呼び出してリモートソケットアドレスを取得します。- 取得した
syscall.Sockaddrがnilの場合、nullProtocolAddr関数を呼び出して、ソケットのファミリーとタイプに応じたnilのnet.Addrインターフェースを返します。 - nilでない場合は、通常の
toAddr関数でnet.Addrに変換して返します。
-
nullProtocolAddr(f, t int) Addr:- ソケットのファミリー(
f、例:syscall.AF_INET)とタイプ(t、例:syscall.SOCK_STREAM)に基づいて、適切な型のnilポインタをnet.Addrインターフェースとして返します。 - 例えば、TCPストリームソケットであれば
(*TCPAddr)(nil)を、UDPデータグラムソケットであれば(*UDPAddr)(nil)を返します。これにより、net.Addrインターフェースが常に有効な型情報を持つようにしつつ、アドレスが不明な場合はその値がnilであることを明示します。
- ソケットのファミリー(
これらの変更により、netFD.setAddrに渡されるladdr(ローカルアドレス)やraddr(リモートアドレス)が、syscall.Sockaddrがnilの場合でも、適切な型のnilのnet.Addrインターフェースとして設定されるようになります。これにより、後続のAddr.String()呼び出し時にnilポインタデリファレンスが発生するのを防ぎます。(*SomeAddr)(nil).String()は、Goのインターフェースの特性により、パニックを起こさずに空文字列を返すか、適切なデフォルト値を返すように実装されています。
また、この修正には、ipraw_test.goとudp_test.goに新しいテストケースが追加されており、LocalAddr().String()とRemoteAddr().String()がnilポインタデリファレンスを引き起こさないことを確認しています。
コアとなるコードの変更箇所
このコミットで変更された主要なファイルとコードの変更点は以下の通りです。
-
src/pkg/net/fd.go:netFD.accept関数内で、netfd.setAddrを呼び出す際に、ローカルアドレスの取得にsyscall.Getsocknameの代わりに新しく導入されたlocalSocknameヘルパー関数を使用するように変更されました。--- a/src/pkg/net/fd.go +++ b/src/pkg/net/fd.go @@ -612,7 +612,7 @@ func (fd *netFD) accept(toAddr func(syscall.Sockaddr) Addr) (netfd *netFD, err e if netfd, err = newFD(s, fd.family, fd.sotype, fd.net); err != nil { closesocket(s) return nil, err } - lsa, _ := syscall.Getsockname(netfd.sysfd) - netfd.setAddr(toAddr(lsa), toAddr(rsa)) + netfd.setAddr(localSockname(fd, toAddr), toAddr(rsa)) return netfd, nil }
-
src/pkg/net/file.go:newFileFD関数内で、ローカルアドレスとリモートアドレスの取得にそれぞれlocalSocknameとremoteSocknameヘルパー関数を使用するように変更されました。--- a/src/pkg/net/file.go +++ b/src/pkg/net/file.go @@ -53,16 +53,14 @@ func newFileFD(f *os.File) (*netFD, error) { toAddr = sockaddrToUnixpacket } } - laddr := toAddr(sa) - sa, _ = syscall.Getpeername(fd) - raddr := toAddr(sa) + laddr := toAddr(lsa) netfd, err := newFD(fd, family, sotype, laddr.Network()) if err != nil { closesocket(fd) return nil, err } - netfd.setAddr(laddr, raddr) + netfd.setAddr(laddr, remoteSockname(netfd, toAddr)) return netfd, nil }FileConn関数内のswitch文で、UnixAddrのケースの順序が変更されました。これは機能的な変更ではなく、コードの整理です。
-
src/pkg/net/sock.go:socket関数の引数名がla,raからulsa,ursaに変更され、より明確になりました。- ローカルアドレスとリモートアドレスの取得と設定に、新しく導入された
localSocknameとremoteSocknameヘルパー関数が使用されるようになりました。--- a/src/pkg/net/sock.go +++ b/src/pkg/net/sock.go @@ -16,7 +16,7 @@ import ( var listenerBacklog = maxListenerBacklog() // Generic socket creation. -func socket(net string, f, t, p int, ipv6only bool, la, ra syscall.Sockaddr, toAddr func(syscall.Sockaddr) Addr) (fd *netFD, err error) { +func socket(net string, f, t, p int, ipv6only bool, ulsa, ursa syscall.Sockaddr, toAddr func(syscall.Sockaddr) Addr) (fd *netFD, err = nil) { // See ../syscall/exec.go for description of ForkLock. syscall.ForkLock.RLock() s, err := syscall.Socket(f, t, p) @@ -27,21 +27,18 @@ func socket(net string, f, t, p int, ipv6only bool, la, ra syscall.Sockaddr, toA syscall.CloseOnExec(s) syscall.ForkLock.RUnlock() - err = setDefaultSockopts(s, f, t, ipv6only) - if err != nil { + if err = setDefaultSockopts(s, f, t, ipv6only); err != nil { closesocket(s) return nil, err } - var bla syscall.Sockaddr - if la != nil { - bla, err = listenerSockaddr(s, f, la, toAddr) - if err != nil { + var blsa syscall.Sockaddr + if ulsa != nil { + if blsa, err = listenerSockaddr(s, f, ulsa, toAddr); err != nil { closesocket(s) return nil, err } - err = syscall.Bind(s, bla) - if err != nil { + if err = syscall.Bind(s, blsa); err != nil { closesocket(s) return nil, err } @@ -52,8 +49,8 @@ func socket(net string, f, t, p int, ipv6only bool, la, ra syscall.Sockaddr, toA return nil, err } - if ra != nil { - if err = fd.connect(ra); err != nil { + if ursa != nil { + if err = fd.connect(ursa); err != nil { closesocket(s) fd.Close() return nil, err @@ -61,17 +58,13 @@ func socket(net string, f, t, p int, ipv6only bool, la, ra syscall.Sockaddr, toA fd.isConnected = true } - sa, _ := syscall.Getsockname(s) var laddr Addr - if la != nil && bla != la { - laddr = toAddr(la) + if ulsa != nil && blsa != ulsa { + laddr = toAddr(ulsa) } else { - laddr = toAddr(sa) + laddr = localSockname(fd, toAddr) } - sa, _ = syscall.Getpeername(s) - raddr := toAddr(sa) - - fd.setAddr(laddr, raddr) + fd.setAddr(laddr, remoteSockname(fd, toAddr)) return fd, nil } @@ -85,3 +78,39 @@ func genericReadFrom(w io.Writer, r io.Reader) (n int64, err error) { // Use wrapper to hide existing r.ReadFrom from io.Copy.\n \treturn io.Copy(writerOnly{w}, r)\n }\n+\n+func localSockname(fd *netFD, toAddr func(syscall.Sockaddr) Addr) Addr {\n+\tsa, _ := syscall.Getsockname(fd.sysfd)\n+\tif sa == nil {\n+\t\treturn nullProtocolAddr(fd.family, fd.sotype)\n+\t}\n+\treturn toAddr(sa)\n+}\n+\n+func remoteSockname(fd *netFD, toAddr func(syscall.Sockaddr) Addr) Addr {\n+\tsa, _ := syscall.Getpeername(fd.sysfd)\n+\tif sa == nil {\n+\t\treturn nullProtocolAddr(fd.family, fd.sotype)\n+\t}\n+\treturn toAddr(sa)\n+}\n+\n+func nullProtocolAddr(f, t int) Addr {\n+\tswitch f {\n+\tcase syscall.AF_INET, syscall.AF_INET6:\n+\t\tswitch t {\n+\t\tcase syscall.SOCK_STREAM:\n+\t\t\treturn (*TCPAddr)(nil)\n+\t\tcase syscall.SOCK_DGRAM:\n+\t\t\treturn (*UDPAddr)(nil)\n+\t\tcase syscall.SOCK_RAW:\n+\t\t\treturn (*IPAddr)(nil)\n+\t\t}\n+\tcase syscall.AF_UNIX:\n+\t\tswitch t {\n+\t\tcase syscall.SOCK_STREAM, syscall.SOCK_DGRAM, syscall.SOCK_SEQPACKET:\n+\t\t\treturn (*UnixAddr)(nil)\n+\t\t}\n+\t}\n+\tpanic(\"unreachable\")\n+}\n ```
-
src/pkg/net/ipraw_test.go:TestIPConnAddrStringという新しいテスト関数が追加されました。このテストは、IPコネクションのLocalAddr().String()とRemoteAddr().String()がnilポインタデリファレンスを引き起こさないことを確認します。- 既存の
TestICMPテストのログメッセージが"test disabled; must be root"から"skipping test; must be root"に変更されました。
-
src/pkg/net/udp_test.go:TestUDPConnAddrStringという新しいテスト関数が追加されました。このテストは、UDPコネクションのLocalAddr().String()とRemoteAddr().String()がnilポインタデリファレンスを引き起こさないことを確認します。
コアとなるコードの解説
このコミットの核心は、netパッケージがソケットアドレスを取得する際に、システムコールがnilのsyscall.Sockaddrを返す可能性を適切に処理することです。
以前は、syscall.Getsocknameやsyscall.Getpeernameがnilのsyscall.Sockaddrを返した場合、そのnil値がそのままtoAddr関数に渡され、結果としてnet.Addrインターフェースが、例えば(*net.TCPAddr)(nil)のような、型は存在するが値がnilであるポインタをラップした状態になることがありました。Goのインターフェースは、型と値のペアで構成されるため、この場合インターフェース自体はnilではありませんが、その内部のポインタがnilであるため、そのポインタに対してメソッド(例: String())を呼び出すとnilポインタデリファレンスが発生し、パニックを引き起こしていました。
この修正では、localSocknameとremoteSocknameという2つのヘルパー関数が導入されました。これらの関数は、それぞれsyscall.Getsocknameとsyscall.Getpeernameのラッパーとして機能します。これらのラッパー関数は、システムコールがnilのsyscall.Sockaddrを返した場合に、直接toAddr関数に渡すのではなく、nullProtocolAddrという別のヘルパー関数を呼び出します。
nullProtocolAddr関数は、ソケットのファミリー(例: IPv4, IPv6, Unixドメイン)とタイプ(例: ストリーム, データグラム, RAW)に基づいて、適切な型のnilポインタをnet.Addrインターフェースとして生成します。例えば、TCPソケットであれば(*net.TCPAddr)(nil)を、UDPソケットであれば(*net.UDPAddr)(nil)を返します。
これにより、netFD.setAddrに渡されるローカルアドレスとリモートアドレスは、たとえシステムコールがアドレス情報を取得できなかった場合でも、常に適切な型情報を持つnet.Addrインターフェース(ただし、その内部のポインタはnil)として設定されるようになります。Goのインターフェースの設計上、(*net.TCPAddr)(nil).String()のような呼び出しはパニックを引き起こさず、通常は空文字列を返すか、その型に定義されたデフォルトの動作をします。このため、RemoteAddr().String()のようなメソッドチェーンが安全に実行できるようになり、nilポインタデリファレンスによるクラッシュが回避されます。
この変更は、Goのnetパッケージの堅牢性を高め、ネットワークプログラミングにおける一般的なエラーパターンの一つを解消する重要な修正です。
関連リンク
- Go Issue #3721: https://github.com/golang/go/issues/3721
- Go CL 6395055: https://golang.org/cl/6395055
参考にした情報源リンク
- Go言語の公式ドキュメント:
netパッケージ,syscallパッケージ - Go言語のソースコード:
src/pkg/net/ディレクトリ内の関連ファイル - Go言語のIssueトラッカー: Issue #3721の詳細な議論
- Go言語のコードレビューシステム (Gerrit): CL 6395055の変更内容とレビューコメント