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

[インデックス 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つの問題がありました。

  1. Windows環境でのユーザー情報ルックアップのパフォーマンス問題: Goの os/user パッケージは、現在のユーザー情報を取得するためにWindows APIを呼び出します。以前の実装では、ユーザーの表示名(Full Name)を取得する際に、まず TranslateAccountName 関数を使ってドメインコントローラーへの問い合わせを試みていました。しかし、ユーザーのコンピューターがActive Directoryドメインに参加していない(例えば、スタンドアロンのワークグループ環境にある)場合、このドメインコントローラーへの問い合わせがタイムアウトするまで数秒間待機してしまうことがありました。これにより、os/user パッケージの利用が非常に遅くなるというパフォーマンス上の問題が発生していました。特に、CLIツールなどでユーザー情報を頻繁に取得するようなシナリオでは、この遅延が顕著でした。

  2. トークンリークの問題: 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: NetUserGetInfoNetGetJoinInformation などの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環境でのユーザー情報ルックアップのロジックを改善し、リソースリークを防ぐ点にあります。

  1. ドメイン参加状況の事前チェック: 以前は、ユーザーの表示名を取得する際に、まず syscall.TranslateAccountName を使用してドメインコントローラーへの問い合わせを試みていました。このコミットでは、新しい関数 isDomainJoined() を導入し、syscall.NetGetJoinInformation APIを呼び出すことで、コンピューターがドメインに参加しているかどうかを事前にチェックします。

    • NetGetJoinInformation は、コンピューターがドメインに参加しているか(NetSetupDomainName)、ワークグループに参加しているか(NetSetupWorkgroupName)、または未参加か(NetSetupUnjoined)などの情報を返します。
    • このチェックにより、ドメインに参加していないコンピューターでは、高コストなドメイン問い合わせをスキップできるようになります。
  2. ユーザー表示名ルックアップのロジック変更: lookupFullName 関数が再設計されました。

    • まず isDomainJoined() を呼び出し、ドメインに参加している場合は lookupFullNameDomain (内部で TranslateAccountName を使用) を試みます。
    • ドメインに参加していない場合、または lookupFullNameDomain が失敗した場合は、lookupFullNameServer (内部で NetUserGetInfo を使用) を試みます。NetUserGetInfo は、ローカルコンピューターまたは指定されたサーバー上のユーザー情報を取得するために使用されます。これにより、ドメインコントローラーへの不要な問い合わせを回避し、ローカルユーザーの情報を効率的に取得できます。
    • どちらのルックアップも失敗した場合、最終手段としてユーザー名をそのまま表示名として返すフォールバックロジックが追加されました。これは、ドメインサーバーが利用できない場合など、特定のシナリオでユーザーエクスペリエンスを向上させるためのものです。
  3. トークンの適切な解放: os/user パッケージの current() 関数内で、syscall.OpenCurrentProcessToken などによって取得されたアクセストークン(syscall.Token 型)が、以前は明示的にクローズされていませんでした。 このコミットでは、defer t.Close() という行が追加されました。これにより、current() 関数が終了する際に、取得したトークンが確実に Close() メソッドによって解放されるようになります。これは、Windows APIの CloseHandle 関数を呼び出すことで実現され、リソースリークを防ぎます。

  4. 新しいWindows APIの定義: src/pkg/syscall/security_windows.go に、NetGetJoinInformation APIのGo言語バインディングが追加されました。また、関連する定数(NetSetupUnknownStatus, NetSetupUnjoined, NetSetupWorkgroupName, NetSetupDomainName)も定義されています。 src/pkg/syscall/zsyscall_windows_386.gosrc/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)を効率的に判定します。これにより、高コストなドメインコントローラーへの問い合わせを、必要な場合にのみ実行するよう制御できます。NetApiBufferFreedefer で呼び出すことで、APIが割り当てたメモリを確実に解放しています。

  • lookupFullName の新しいロジック: 新しい lookupFullName 関数は、まず isDomainJoined() を呼び出してドメイン参加状況を確認します。

    1. もしドメインに参加していると判断された場合、lookupFullNameDomain を呼び出し、TranslateAccountName を使ってドメイン経由でのユーザー表示名取得を試みます。
    2. lookupFullNameDomain が成功すれば、その結果を返します。
    3. ドメインに参加していない場合、または lookupFullNameDomain が失敗した場合は、lookupFullNameServer を呼び出します。これは NetUserGetInfo を使用して、ローカルコンピューター(または指定されたサーバー)からユーザー情報を取得します。
    4. lookupFullNameServer が成功すれば、その結果を返します。
    5. どちらのルックアップも失敗した場合、最終的にユーザー名をそのまま表示名として返すフォールバック処理を行います。これは、ネットワークの問題などでドメインサーバーが一時的に利用できない場合などにも対応するための堅牢な設計です。
  • current() におけるトークン解放: current() 関数内で、syscall.OpenCurrentProcessToken などによって取得されたアクセストークン t に対して defer t.Close() が追加されました。これは、Goの defer 機構を利用して、current() 関数が正常終了するかエラーで終了するかにかかわらず、必ず t.Close() メソッドが呼び出されるようにします。syscall.TokenClose() メソッドは、内部的に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環境でのユーザー情報取得が、ドメイン参加状況に応じて最適な方法を選択し、不要な遅延を回避できるようになりました。また、リソースリークも確実に防がれるようになりました。

関連リンク

参考にした情報源リンク