[インデックス 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の変更内容とレビューコメント