[インデックス 16305] ファイルの概要
このコミットは、Go言語の標準ライブラリ os/user
パッケージにおけるWindows環境でのユーザー情報ルックアップのパフォーマンス改善と、リソースリークの修正を目的としています。具体的には、ドメインに参加していないWindowsマシンでのユーザー表示名(display name)の取得が遅い問題に対処し、また user.Current()
関数におけるトークンのリークを防ぐための変更が加えられています。
コミット
commit fae362e97e852cf04c6c089e61e92c1ad559b29b
Author: Alexey Borzenkov <snaury@gmail.com>
Date: Wed May 15 13:24:54 2013 +1000
os/user: faster user lookup on Windows
Trying to lookup user's display name with directory services can
take several seconds when user's computer is not in a domain.
As a workaround, check if computer is joined in a domain first,
and don't use directory services if it is not.
Additionally, don't leak tokens in user.Current().
Fixes #5298.
R=golang-dev, bradfitz, alex.brainman, lucio.dere
CC=golang-dev
https://golang.org/cl/8541047
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/fae362e97e852cf04c6c089e61e92c1ad559b29b
元コミット内容
このコミットは、Windows環境におけるユーザー情報のルックアップ処理を高速化し、同時にリソースリークを修正します。具体的には、ユーザーのコンピューターがドメインに参加していない場合に、ディレクトリサービスを使用したユーザー表示名のルックアップに数秒かかることがある問題に対処します。この問題を回避するため、まずコンピューターがドメインに参加しているかどうかを確認し、参加していない場合はディレクトリサービスを使用しないようにします。さらに、user.Current()
関数で取得されるトークンがリークしないように修正が加えられています。この変更は、Go issue #5298 を修正するものです。
変更の背景
このコミットが行われた背景には、主に以下の2つの問題がありました。
-
Windows環境でのユーザー情報ルックアップのパフォーマンス問題: Goの
os/user
パッケージは、現在のユーザー情報を取得するためにWindows APIを呼び出します。以前の実装では、ユーザーの表示名(Full Name)を取得する際に、まずTranslateAccountName
関数を使ってドメインコントローラーへの問い合わせを試みていました。しかし、ユーザーのコンピューターがActive Directoryドメインに参加していない(例えば、スタンドアロンのワークグループ環境にある)場合、このドメインコントローラーへの問い合わせがタイムアウトするまで数秒間待機してしまうことがありました。これにより、os/user
パッケージの利用が非常に遅くなるというパフォーマンス上の問題が発生していました。特に、CLIツールなどでユーザー情報を頻繁に取得するようなシナリオでは、この遅延が顕著でした。 -
トークンリークの問題:
os/user
パッケージのcurrent()
関数は、内部的にWindowsのユーザーアクセストークンを取得していました。しかし、取得したトークンが適切にクローズ(解放)されていなかったため、リソースリークが発生する可能性がありました。これは、プログラムが長時間実行されたり、user.Current()
が頻繁に呼び出されたりする場合に、システムリソースを徐々に消費してしまう原因となります。
これらの問題に対処するため、本コミットでは、ドメイン参加状況の事前チェックと、トークンの適切な解放メカニズムが導入されました。
前提知識の解説
このコミットを理解するためには、以下の概念について知っておく必要があります。
1. Windowsのユーザーアカウントとドメイン/ワークグループ
- ワークグループ (Workgroup): 小規模なネットワーク環境で用いられるピアツーピアのネットワークモデルです。各コンピューターは独立しており、ユーザーアカウント情報はそれぞれのコンピューターのローカルに保存されます。ユーザーは各コンピューターで個別にアカウントを作成・管理する必要があります。
- ドメイン (Domain): Active Directory (AD) を中心としたクライアント/サーバー型のネットワークモデルです。ユーザーアカウント、コンピューター、その他のネットワークリソースに関する情報が一元的に管理されます。ドメインに参加しているコンピューターは、ドメインコントローラーを通じてユーザー認証やリソースアクセスを行います。大規模な企業ネットワークで一般的に使用されます。
- ユーザー表示名 (Display Name): ユーザーのフルネームや表示用の名前です。Windowsでは、ユーザーアカウント名(例:
johndoe
)とは別に、より人間が読みやすい表示名(例:John Doe
)を設定できます。
2. Windows API (WinAPI)
Windows APIは、Windowsオペレーティングシステムが提供する関数群で、アプリケーションがOSの機能を利用するためのインターフェースです。このコミットでは、特に以下のAPIが重要です。
TranslateAccountName
: アカウント名(ユーザー名、グループ名など)を異なる形式に変換する関数です。例えば、SAMアカウント名(DOMAIN\username
)を表示名に変換する際に使用されます。この関数は、ドメインコントローラーに問い合わせを行う場合があります。NetUserGetInfo
: ネットワーク上のユーザーアカウントに関する情報を取得する関数です。特定のサーバー(またはローカルコンピューター)上のユーザー情報を取得できます。NetGetJoinInformation
: コンピューターがドメインまたはワークグループのどちらに参加しているか、およびその名前を取得する関数です。これにより、ドメイン参加状況を効率的に判断できます。NetApiBufferFree
:NetUserGetInfo
やNetGetJoinInformation
などのNetAPI関数によって割り当てられたメモリバッファを解放するための関数です。これらのAPIを使用する際には、メモリリークを防ぐために必ず呼び出す必要があります。
3. Go言語の syscall
パッケージ
Go言語の syscall
パッケージは、オペレーティングシステムが提供する低レベルのプリミティブ(システムコール)へのアクセスを提供します。これにより、Goプログラムから直接Windows APIなどのOS固有の機能を呼び出すことが可能になります。
syscall.NewProc
: DLLから特定の関数のアドレスを取得します。syscall.Syscall
: 実際のシステムコールを実行します。syscall.UTF16PtrFromString
,syscall.UTF16ToString
: Goの文字列とWindows APIが使用するUTF-16エンコードされた文字列ポインタとの間で変換を行います。syscall.Token
: Windowsのアクセストークンを表す型です。ユーザーやプロセスのセキュリティコンテキストを定義します。
4. defer
ステートメント
Go言語の defer
ステートメントは、その関数がリターンする直前に実行される関数呼び出しをスケジュールします。これは、リソースの解放(ファイルクローズ、ロック解除、メモリ解放など)を確実に行うために非常に便利です。このコミットでは、取得したトークンを確実にクローズするために defer
が使用されています。
技術的詳細
このコミットの技術的な核心は、Windows環境でのユーザー情報ルックアップのロジックを改善し、リソースリークを防ぐ点にあります。
-
ドメイン参加状況の事前チェック: 以前は、ユーザーの表示名を取得する際に、まず
syscall.TranslateAccountName
を使用してドメインコントローラーへの問い合わせを試みていました。このコミットでは、新しい関数isDomainJoined()
を導入し、syscall.NetGetJoinInformation
APIを呼び出すことで、コンピューターがドメインに参加しているかどうかを事前にチェックします。NetGetJoinInformation
は、コンピューターがドメインに参加しているか(NetSetupDomainName
)、ワークグループに参加しているか(NetSetupWorkgroupName
)、または未参加か(NetSetupUnjoined
)などの情報を返します。- このチェックにより、ドメインに参加していないコンピューターでは、高コストなドメイン問い合わせをスキップできるようになります。
-
ユーザー表示名ルックアップのロジック変更:
lookupFullName
関数が再設計されました。- まず
isDomainJoined()
を呼び出し、ドメインに参加している場合はlookupFullNameDomain
(内部でTranslateAccountName
を使用) を試みます。 - ドメインに参加していない場合、または
lookupFullNameDomain
が失敗した場合は、lookupFullNameServer
(内部でNetUserGetInfo
を使用) を試みます。NetUserGetInfo
は、ローカルコンピューターまたは指定されたサーバー上のユーザー情報を取得するために使用されます。これにより、ドメインコントローラーへの不要な問い合わせを回避し、ローカルユーザーの情報を効率的に取得できます。 - どちらのルックアップも失敗した場合、最終手段としてユーザー名をそのまま表示名として返すフォールバックロジックが追加されました。これは、ドメインサーバーが利用できない場合など、特定のシナリオでユーザーエクスペリエンスを向上させるためのものです。
- まず
-
トークンの適切な解放:
os/user
パッケージのcurrent()
関数内で、syscall.OpenCurrentProcessToken
などによって取得されたアクセストークン(syscall.Token
型)が、以前は明示的にクローズされていませんでした。 このコミットでは、defer t.Close()
という行が追加されました。これにより、current()
関数が終了する際に、取得したトークンが確実にClose()
メソッドによって解放されるようになります。これは、Windows APIのCloseHandle
関数を呼び出すことで実現され、リソースリークを防ぎます。 -
新しいWindows APIの定義:
src/pkg/syscall/security_windows.go
に、NetGetJoinInformation
APIのGo言語バインディングが追加されました。また、関連する定数(NetSetupUnknownStatus
,NetSetupUnjoined
,NetSetupWorkgroupName
,NetSetupDomainName
)も定義されています。src/pkg/syscall/zsyscall_windows_386.go
とsrc/pkg/syscall/zsyscall_windows_amd64.go
は、Goのツールによって自動生成されるファイルで、新しいAPIのシステムコールエントリポイントが追加されています。
これらの変更により、Windows環境でのユーザー情報取得がより高速かつ堅牢になり、リソースリークの問題も解決されました。
コアとなるコードの変更箇所
主要な変更は以下のファイルに集中しています。
src/pkg/os/user/lookup_windows.go
src/pkg/syscall/security_windows.go
src/pkg/syscall/zsyscall_windows_386.go
src/pkg/syscall/zsyscall_windows_amd64.go
src/pkg/os/user/lookup_windows.go
の変更点
新規追加関数 isDomainJoined()
func isDomainJoined() (bool, error) {
var domain *uint16
var status uint32
err := syscall.NetGetJoinInformation(nil, &domain, &status)
if err != nil {
return false, err
}
syscall.NetApiBufferFree((*byte)(unsafe.Pointer(domain)))
return status == syscall.NetSetupDomainName, nil
}
この関数は、syscall.NetGetJoinInformation
を呼び出してコンピューターのドメイン参加状況を取得します。取得したドメイン名バッファは syscall.NetApiBufferFree
で解放されます。
新規追加関数 lookupFullNameDomain()
func lookupFullNameDomain(domainAndUser string) (string, error) {
return syscall.TranslateAccountName(domainAndUser,
syscall.NameSamCompatible, syscall.NameDisplay, 50)
}
この関数は、TranslateAccountName
を使用してドメイン経由でユーザーの表示名を取得します。
新規追加関数 lookupFullNameServer()
func lookupFullNameServer(servername, username string) (string, error) {
s, e := syscall.UTF16PtrFromString(servername)
if e != nil {
return "", e
}
u, e := syscall.UTF16PtrFromString(username)
if e != nil {
return "", e
}
var p *byte
e = syscall.NetUserGetInfo(s, u, 10, &p)
if e != nil {
return "", e
}
defer syscall.NetApiBufferFree(p)
i := (*syscall.UserInfo10)(unsafe.Pointer(p))
if i.FullName == nil {
return "", nil
}
name := syscall.UTF16ToString((*[1024]uint16)(unsafe.Pointer(i.FullName))[:])
return name, nil
}
この関数は、NetUserGetInfo
を使用して指定されたサーバー(またはローカル)からユーザーの表示名を取得します。取得したバッファは defer syscall.NetApiBufferFree(p)
で解放されます。
lookupFullName()
関数の変更
--- a/src/pkg/os/user/lookup_windows.go
+++ b/src/pkg/os/user/lookup_windows.go
@@ -10,37 +10,63 @@ import (
"unsafe"
)
-func lookupFullName(domain, username, domainAndUser string) (string, error) {
- // try domain controller first
- name, e := syscall.TranslateAccountName(domainAndUser,
-+func isDomainJoined() (bool, error) {
-+ var domain *uint16
-+ var status uint32
-+ err := syscall.NetGetJoinInformation(nil, &domain, &status)
-+ if err != nil {
-+ return false, err
-+ }
-+ syscall.NetApiBufferFree((*byte)(unsafe.Pointer(domain)))
-+ return status == syscall.NetSetupDomainName, nil
-+}
-+
-+func lookupFullNameDomain(domainAndUser string) (string, error) {
-+ return syscall.TranslateAccountName(domainAndUser,
syscall.NameSamCompatible, syscall.NameDisplay, 50)
- }
- if e != nil {
- // domain lookup failed, perhaps this pc is not part of domain
- d, e := syscall.UTF16PtrFromString(domain)
- if e != nil {
- return "", e
- }
- u, e := syscall.UTF16PtrFromString(username)
- if e != nil {
- return "", e
- }
- var p *byte
- e = syscall.NetUserGetInfo(d, u, 10, &p)
- if e != nil {
- // path executed when a domain user is disconnected from the domain
- // pretend username is fullname
- return username, nil
- }
- defer syscall.NetApiBufferFree(p)
- i := (*syscall.UserInfo10)(unsafe.Pointer(p))
- if i.FullName == nil {
- return "", nil
- }
- name = syscall.UTF16ToString((*[1024]uint16)(unsafe.Pointer(i.FullName))[:])
- }
-+func lookupFullNameServer(servername, username string) (string, error) {
-+ s, e := syscall.UTF16PtrFromString(servername)
-+ if e != nil {
-+ return "", e
-+ }
-+ u, e := syscall.UTF16PtrFromString(username)
-+ if e != nil {
-+ return "", e
-+ }
-+ var p *byte
-+ e = syscall.NetUserGetInfo(s, u, 10, &p)
-+ if e != nil {
-+ return "", e
-+ }
-+ defer syscall.NetApiBufferFree(p)
-+ i := (*syscall.UserInfo10)(unsafe.Pointer(p))
-+ if i.FullName == nil {
-+ return "", nil
-+ }
-+ name := syscall.UTF16ToString((*[1024]uint16)(unsafe.Pointer(i.FullName))[:])
-+ return name, nil
-+ }
-+
-+func lookupFullName(domain, username, domainAndUser string) (string, error) {
-+ joined, err := isDomainJoined()
-+ if err == nil && joined {
-+ name, err := lookupFullNameDomain(domainAndUser)
-+ if err == nil {
-+ return name, nil
-+ }
-+ }
-+ name, err := lookupFullNameServer(domain, username)
-+ if err == nil {
-+ return name, nil
-+ }
-+ // domain worked neigher as a domain nor as a server
-+ // could be domain server unavailable
-+ // pretend username is fullname
-+ return username, nil
-+ }
return name, nil
}
@@ -73,6 +99,7 @@ func current() (*User, error) {
if e != nil {
return nil, e
}
-+ defer t.Close()
u, e := t.GetTokenUser()
if e != nil {
return nil, e
lookupFullName
のロジックが大幅に変更され、isDomainJoined
の結果に基づいて lookupFullNameDomain
または lookupFullNameServer
を呼び出すようになりました。
current()
関数の変更
--- a/src/pkg/os/user/lookup_windows.go
+++ b/src/pkg/os/user/lookup_windows.go
@@ -73,6 +99,7 @@ func current() (*User, error) {
if e != nil {
return nil, e
}
-+ defer t.Close()
u, e := t.GetTokenUser()
if e != nil {
return nil, e
defer t.Close()
が追加され、トークンが確実に解放されるようになりました。
src/pkg/syscall/security_windows.go
の変更点
--- a/src/pkg/syscall/security_windows.go
+++ b/src/pkg/syscall/security_windows.go
@@ -58,6 +58,14 @@ func TranslateAccountName(username string, from, to uint32, initSize int) (strin
return UTF16ToString(b), nil
}
+const (
+ // do not reorder
+ NetSetupUnknownStatus = iota
+ NetSetupUnjoined
+ NetSetupWorkgroupName
+ NetSetupDomainName
+)
+
type UserInfo10 struct {
Name *uint16
Comment *uint16
@@ -66,6 +74,7 @@ type UserInfo10 struct {
}
//sys NetUserGetInfo(serverName *uint16, userName *uint16, level uint32, buf **byte) (neterr error) = netapi32.NetUserGetInfo
- //sys NetGetJoinInformation(server *uint16, name **uint16, bufType *uint32) (neterr error) = netapi32.NetGetJoinInformation
+ //sys NetGetJoinInformation(server *uint16, name **uint16, bufType *uint32) (neterr error) = netapi32.NetGetJoinInformation
//sys NetApiBufferFree(buf *byte) (neterr error) = netapi32.NetApiBufferFree
const (
NetSetupUnknownStatus
などの定数と、NetGetJoinInformation
のシステムコール定義が追加されています。
src/pkg/syscall/zsyscall_windows_386.go
および src/pkg/syscall/zsyscall_windows_amd64.go
の変更点
これらのファイルには、NetGetJoinInformation
のプロシージャアドレスの取得と、実際のシステムコール呼び出しを行うためのGoコードが自動生成によって追加されています。
--- a/src/pkg/syscall/zsyscall_windows_386.go
+++ b/src/pkg/syscall/zsyscall_windows_386.go
@@ -140,6 +140,7 @@ var (
procTranslateNameW = modsecur32.NewProc("TranslateNameW")
procGetUserNameExW = modsecur32.NewProc("GetUserNameExW")
procNetUserGetInfo = modnetapi32.NewProc("NetUserGetInfo")
+ procNetGetJoinInformation = modnetapi32.NewProc("NetGetJoinInformation")
procNetApiBufferFree = modnetapi32.NewProc("NetApiBufferFree")
procLookupAccountSidW = modadvapi32.NewProc("LookupAccountSidW")
procLookupAccountNameW = modadvapi32.NewProc("LookupAccountNameW")
@@ -1613,6 +1614,14 @@ func NetUserGetInfo(serverName *uint16, userName *uint16, level uint32, buf **by
return
}
+func NetGetJoinInformation(server *uint16, name **uint16, bufType *uint32) (neterr error) {
+ r0, _, _ := Syscall(procNetGetJoinInformation.Addr(), 3, uintptr(unsafe.Pointer(server)), uintptr(unsafe.Pointer(name)), uintptr(unsafe.Pointer(bufType)))
+ if r0 != 0 {
+ neterr = Errno(r0)
+ }
+ return
+}
+
func NetApiBufferFree(buf *byte) (neterr error) {
r0, _, _ := Syscall(procNetApiBufferFree.Addr(), 1, uintptr(unsafe.Pointer(buf)), 0, 0)
if r0 != 0 {
同様の変更が zsyscall_windows_amd64.go
にも適用されています。
コアとなるコードの解説
lookup_windows.go
の変更
-
isDomainJoined()
: この関数は、Windows APIのNetGetJoinInformation
を呼び出すことで、現在のコンピューターがドメインに参加しているか(NetSetupDomainName
)を効率的に判定します。これにより、高コストなドメインコントローラーへの問い合わせを、必要な場合にのみ実行するよう制御できます。NetApiBufferFree
をdefer
で呼び出すことで、APIが割り当てたメモリを確実に解放しています。 -
lookupFullName
の新しいロジック: 新しいlookupFullName
関数は、まずisDomainJoined()
を呼び出してドメイン参加状況を確認します。- もしドメインに参加していると判断された場合、
lookupFullNameDomain
を呼び出し、TranslateAccountName
を使ってドメイン経由でのユーザー表示名取得を試みます。 lookupFullNameDomain
が成功すれば、その結果を返します。- ドメインに参加していない場合、または
lookupFullNameDomain
が失敗した場合は、lookupFullNameServer
を呼び出します。これはNetUserGetInfo
を使用して、ローカルコンピューター(または指定されたサーバー)からユーザー情報を取得します。 lookupFullNameServer
が成功すれば、その結果を返します。- どちらのルックアップも失敗した場合、最終的にユーザー名をそのまま表示名として返すフォールバック処理を行います。これは、ネットワークの問題などでドメインサーバーが一時的に利用できない場合などにも対応するための堅牢な設計です。
- もしドメインに参加していると判断された場合、
-
current()
におけるトークン解放:current()
関数内で、syscall.OpenCurrentProcessToken
などによって取得されたアクセストークンt
に対してdefer t.Close()
が追加されました。これは、Goのdefer
機構を利用して、current()
関数が正常終了するかエラーで終了するかにかかわらず、必ずt.Close()
メソッドが呼び出されるようにします。syscall.Token
のClose()
メソッドは、内部的にWindows APIのCloseHandle
を呼び出し、アクセストークンに関連付けられたシステムリソースを解放します。これにより、トークンリークが防止されます。
syscall/security_windows.go
の変更
NetGetJoinInformation
の定義://sys NetGetJoinInformation(...)
というコメント行は、Goのgo generate
ツールがシステムコールバインディングを自動生成するための指示です。これにより、Goプログラムから直接NetGetJoinInformation
Windows APIを呼び出せるようになります。NetSetup*
定数:NetGetJoinInformation
が返すステータスコードに対応する定数が定義されています。これらは、コンピューターがドメイン、ワークグループ、または未参加のいずれであるかを示すために使用されます。
zsyscall_windows_386.go
および zsyscall_windows_amd64.go
の変更
これらのファイルは、go generate
コマンドによって自動生成されるもので、NetGetJoinInformation
APIを呼び出すための低レベルなGoコード(Syscall
関数へのラッパー)が含まれています。これにより、GoプログラムがWindowsのネイティブAPIと連携できるようになります。
これらの変更により、Windows環境でのユーザー情報取得が、ドメイン参加状況に応じて最適な方法を選択し、不要な遅延を回避できるようになりました。また、リソースリークも確実に防がれるようになりました。
関連リンク
- Go Issue #5298: https://code.google.com/p/go/issues/detail?id=5298 (古いGoのIssueトラッカーのリンクですが、コミットメッセージに記載されています)
- Go CL 8541047: https://golang.org/cl/8541047 (Goのコードレビューシステムへのリンク)
参考にした情報源リンク
- NetGetJoinInformation function (Windows): https://learn.microsoft.com/en-us/windows/win32/api/lmjoin/nf-lmjoin-netgetjoininformation
- NetApiBufferFree function (Windows): https://learn.microsoft.com/en-us/windows/win32/api/lmapibuf/nf-lmapibuf-netapibufferfree
- TranslateAccountName function (Windows): https://learn.microsoft.com/en-us/windows/win32/api/secext/nf-secext-translateaccountnamea
- NetUserGetInfo function (Windows): https://learn.microsoft.com/en-us/windows/win32/api/lmaccess/nf-lmaccess-netusergetinfo
- Access Tokens (Windows): https://learn.microsoft.com/en-us/windows/win32/secauthz/access-tokens
- Go
syscall
package documentation: https://pkg.go.dev/syscall - Go
defer
statement: https://go.dev/blog/defer-panic-and-recover - Windows Workgroup vs Domain: https://learn.microsoft.com/en-us/windows-server/identity/ad-ds/plan/understanding-active-directory-domain-services