Alex Brainman would like Jason A. Donenfeld and Brad Fitzpatrick to review this change.
windows/svc: reimplement IsAnInteractiveSession
CL 244958 includes isWindowsService function that determines if a
process is running as a service. The code of the function is based
on public .Net implementation.
Current IsAnInteractiveSession implementation is based on a
Stackoverflow post, which is not as authoritative as code written by
Microsoft for their official product.
This change replaces IsAnInteractiveSession with a code similar to
CL 244958.
Change-Id: I4a33b7f590ee8161d1134d8e83668e9da4e6b434
---
M windows/svc/security.go
M windows/svc/svc_test.go
M windows/syscall_windows.go
M windows/zsyscall_windows.go
4 files changed, 104 insertions(+), 31 deletions(-)
diff --git a/windows/svc/security.go b/windows/svc/security.go
index 6502599..d3888aa 100644
--- a/windows/svc/security.go
+++ b/windows/svc/security.go
@@ -7,53 +7,102 @@
package svc
import (
+ "errors"
+ "syscall"
+ "unsafe"
+
"golang.org/x/sys/windows"
)
-func allocSid(subAuth0 uint32) (*windows.SID, error) {
- var sid *windows.SID
- err := windows.AllocateAndInitializeSid(&windows.SECURITY_NT_AUTHORITY,
- 1, subAuth0, 0, 0, 0, 0, 0, 0, 0, &sid)
- if err != nil {
- return nil, err
- }
- return sid, nil
-}
+var (
+ ntdll = windows.NewLazySystemDLL("ntdll.dll")
+ _NtQueryInformationProcess = ntdll.NewProc("NtQueryInformationProcess")
+
+ kernel32 = windows.NewLazySystemDLL("kernel32.dll")
+ _QueryFullProcessImageNameA = kernel32.NewProc("QueryFullProcessImageNameA")
+)
// IsAnInteractiveSession determines if calling process is running interactively.
-// It queries the process token for membership in the Interactive group.
-// http://stackoverflow.com/questions/2668851/how-do-i-detect-that-my-application-is-running-as-service-or-in-an-interactive-s
func IsAnInteractiveSession() (bool, error) {
- interSid, err := allocSid(windows.SECURITY_INTERACTIVE_RID)
+ ret, err := isWindowsService()
+ return !ret, err
+}
+
+// isWindowsService returns whether the process is currently executing as a
+// Windows service. The below technique looks a bit hairy, but it's actually
+// exactly what the .NET framework does for the similarly named function:
+// https://github.com/dotnet/extensions/blob/f4066026ca06984b07e90e61a6390ac38152ba93/src/Hosting/WindowsServices/src/WindowsServiceHelpers.cs#L26-L31
+// Specifically, it looks up whether the parent process has session ID zero
+// and is called "services".
+func isWindowsService() (bool, error) {
+ const _CURRENT_PROCESS = ^uintptr(0)
+ // pbi is a PROCESS_BASIC_INFORMATION struct, where we just care about
+ // the 6th pointer inside of it, which contains the pid of the process
+ // parent:
+ // https://github.com/wine-mirror/wine/blob/42cb7d2ad1caba08de235e6319b9967296b5d554/include/winternl.h#L1294
+ var pbi [6]uintptr
+ var pbiLen uint32
+ r0, _, _ := syscall.Syscall6(_NtQueryInformationProcess.Addr(), 5, _CURRENT_PROCESS, 0, uintptr(unsafe.Pointer(&pbi[0])), uintptr(unsafe.Sizeof(pbi)), uintptr(unsafe.Pointer(&pbiLen)), 0)
+ if r0 != 0 {
+ return false, errors.New("NtQueryInformationProcess failed: error=" + itoa(int(r0)))
+ }
+ var psid uint32
+ err := windows.ProcessIdToSessionId(uint32(pbi[5]), &psid)
if err != nil {
return false, err
}
- defer windows.FreeSid(interSid)
+ if psid != 0 {
+ // parent session id should be 0 for service process
+ return false, nil
+ }
- serviceSid, err := allocSid(windows.SECURITY_SERVICE_RID)
+ pproc, err := windows.OpenProcess(windows.PROCESS_QUERY_LIMITED_INFORMATION, false, uint32(pbi[5]))
if err != nil {
return false, err
}
- defer windows.FreeSid(serviceSid)
+ defer windows.CloseHandle(pproc)
- t, err := windows.OpenCurrentProcessToken()
- if err != nil {
- return false, err
- }
- defer t.Close()
-
- gs, err := t.GetTokenGroups()
- if err != nil {
- return false, err
- }
-
- for _, g := range gs.AllGroups() {
- if windows.EqualSid(g.Sid, interSid) {
- return true, nil
+ // exeName gets the path to the executable image of the parent process
+ var exeName [261]byte
+ exeNameLen := uint32(len(exeName) - 1)
+ r0, _, e0 := syscall.Syscall6(_QueryFullProcessImageNameA.Addr(), 4, uintptr(pproc), 0, uintptr(unsafe.Pointer(&exeName[0])), uintptr(unsafe.Pointer(&exeNameLen)), 0, 0)
+ if r0 == 0 {
+ if e0 != 0 {
+ return false, e0
+ } else {
+ return false, syscall.EINVAL
}
- if windows.EqualSid(g.Sid, serviceSid) {
+ }
+ servicesLower := "services.exe"
+ servicesUpper := "SERVICES.EXE"
+ i := int(exeNameLen) - 1
+ j := len(servicesLower) - 1
+ if i < j {
+ return false, nil
+ }
+ for {
+ if j == -1 {
+ return i == -1 || exeName[i] == '\\', nil
+ }
+ if exeName[i] != servicesLower[j] && exeName[i] != servicesUpper[j] {
return false, nil
}
+ i--
+ j--
}
- return false, nil
+}
+
+func itoa(val int) string { // do it here rather than with fmt to avoid dependency
+ if val < 0 {
+ return "-" + itoa(-val)
+ }
+ var buf [32]byte // big enough for int64
+ i := len(buf) - 1
+ for val >= 10 {
+ buf[i] = byte(val%10 + '0')
+ i--
+ val /= 10
+ }
+ buf[i] = byte(val + '0')
+ return string(buf[i:])
}
diff --git a/windows/svc/svc_test.go b/windows/svc/svc_test.go
index 3a80e48..130f046 100644
--- a/windows/svc/svc_test.go
+++ b/windows/svc/svc_test.go
@@ -132,3 +132,13 @@
t.Errorf("%q string does not contain %q", string(out), want)
}
}
+
+func TestIsAnInteractiveSession(t *testing.T) {
+ isInteractive, err := svc.IsAnInteractiveSession()
+ if err != nil {
+ t.Fatal(err)
+ }
+ if !isInteractive {
+ t.Error("TestIsAnInteractiveSession retuns false when running interactively.")
+ }
+}
diff --git a/windows/syscall_windows.go b/windows/syscall_windows.go
index 2aa29e8..37dcc57 100644
--- a/windows/syscall_windows.go
+++ b/windows/syscall_windows.go
@@ -270,6 +270,7 @@
//sys RegEnumKeyEx(key Handle, index uint32, name *uint16, nameLen *uint32, reserved *uint32, class *uint16, classLen *uint32, lastWriteTime *Filetime) (regerrno error) = advapi32.RegEnumKeyExW
//sys RegQueryValueEx(key Handle, name *uint16, reserved *uint32, valtype *uint32, buf *byte, buflen *uint32) (regerrno error) = advapi32.RegQueryValueExW
//sys GetCurrentProcessId() (pid uint32) = kernel32.GetCurrentProcessId
+//sys ProcessIdToSessionId(pid uint32, sessionid *uint32) (err error) = kernel32.ProcessIdToSessionId
//sys GetConsoleMode(console Handle, mode *uint32) (err error) = kernel32.GetConsoleMode
//sys SetConsoleMode(console Handle, mode uint32) (err error) = kernel32.SetConsoleMode
//sys GetConsoleScreenBufferInfo(console Handle, info *ConsoleScreenBufferInfo) (err error) = kernel32.GetConsoleScreenBufferInfo
diff --git a/windows/zsyscall_windows.go b/windows/zsyscall_windows.go
index 347f13d..121c658 100644
--- a/windows/zsyscall_windows.go
+++ b/windows/zsyscall_windows.go
@@ -180,6 +180,7 @@
procRegEnumKeyExW = modadvapi32.NewProc("RegEnumKeyExW")
procRegQueryValueExW = modadvapi32.NewProc("RegQueryValueExW")
procGetCurrentProcessId = modkernel32.NewProc("GetCurrentProcessId")
+ procProcessIdToSessionId = modkernel32.NewProc("ProcessIdToSessionId")
procGetConsoleMode = modkernel32.NewProc("GetConsoleMode")
procSetConsoleMode = modkernel32.NewProc("SetConsoleMode")
procGetConsoleScreenBufferInfo = modkernel32.NewProc("GetConsoleScreenBufferInfo")
@@ -1942,6 +1943,18 @@
return
}
+func ProcessIdToSessionId(pid uint32, sessionid *uint32) (err error) {
+ r1, _, e1 := syscall.Syscall(procProcessIdToSessionId.Addr(), 2, uintptr(pid), uintptr(unsafe.Pointer(sessionid)), 0)
+ if r1 == 0 {
+ if e1 != 0 {
+ err = errnoErr(e1)
+ } else {
+ err = syscall.EINVAL
+ }
+ }
+ return
+}
+
func GetConsoleMode(console Handle, mode *uint32) (err error) {
r1, _, e1 := syscall.Syscall(procGetConsoleMode.Addr(), 2, uintptr(console), uintptr(unsafe.Pointer(mode)), 0)
if r1 == 0 {
To view, visit change 259397. To unsubscribe, or for help writing mail filters, visit settings.
Patch set 1:Run-TryBot +1Trust +1
1 comment:
Patchset:
Jason,
I hope you like this change. Brad prefers your copy of .Net code to what I found on the Stackoverflow long time ago.
Alex
To view, visit change 259397. To unsubscribe, or for help writing mail filters, visit settings.
1 comment:
File windows/svc/security.go:
// IsAnInteractiveSession determines if calling process is running interactively.
func IsAnInteractiveSession() (bool, error) {
ret, err := isWindowsService()
The docs and the implementation don't (naively) seem to agree with each other.
If I were looking for a function that reported whether I was being run as a service, it's not obvious to me (or from these docs) that "is an interactive session" was the inverse of "running as a service".
That is, are there are only two contexts that a Windows program can run in: service vs interactive?
It's clear to me that a service is not interactive, but I can think of other types of process (e.g. a non-GUI subprocess of a GUI process) that are neither interactive nor services. Did "IsAnInteractiveSession" previously return true for those, but won't now?
This CL deletes the docs with the stackoverflow link that even suggested this was about service-or-not. I guess this is in the "svc" package, but some more explicit docs would be nice.
To view, visit change 259397. To unsubscribe, or for help writing mail filters, visit settings.
2 comments:
Patchset:
Thank you for review.
Alex
File windows/svc/security.go:
// IsAnInteractiveSession determines if calling process is running interactively.
func IsAnInteractiveSession() (bool, error) {
ret, err := isWindowsService()
The docs and the implementation don't (naively) seem to agree with each other. […]
Before I start adding docs, here is another suggestion.
We can just add new IsWindowsService function instead.
And leave IsAnInteractiveSession function alone. We will also add DEPRETIATED in IsAnInteractiveSession docs, and make them refer to new IsWindowsService instead.
The downside of this approach, is that, obviously, none of existing people code will use IsWindowsService. I don't see this as a big deal, because IsAnInteractiveSession actually works properly. I have not anyone report issue with IsAnInteractiveSession.
I agree that IsAnInteractiveSession is not a good name for a package about Windows service. I take the blame for it. I just copied the code from Stackoverflow, and kept the name.
We can also just drop this CL altogether and continue to use IsAnInteractiveSession as before.
Let me know.
Thank you.
Alex
To view, visit change 259397. To unsubscribe, or for help writing mail filters, visit settings.
1 comment:
File windows/svc/security.go:
// IsAnInteractiveSession determines if calling process is running interactively.
func IsAnInteractiveSession() (bool, error) {
ret, err := isWindowsService()
Before I start adding docs, here is another suggestion. […]
Adding a new function like "RunningInService" sounds fine (the code in the Go runtime is what I want to use and trust, so copying it to a public place like this package is good), but the downside to having two functions (even if one is deprecated) is we'd then want to document a bit what the difference between them are.
Documenting the old one as "basically the opposite of the other one, but kinda a different implementation, I dunno" isn't great documentation, but might be sufficient if you can find some slightly better explanation.
Also, the main reason I want to use the Go runtime implementation is because I understand and trust it, since it's the same implementation in dotnet.
Maybe for the old documentation, you can just explain in English exactly how it works implementation-wise. But looking at the implementation, I feel like you should do two passes over the SID list first and first return false if it contains service, and then do a pass looking for interactive (to return true), and then finally return false as the fallback.
Otherwise for a process in both interactive and service (such a services with the "Interact with Desktop" bit: https://docs.microsoft.com/en-us/windows/win32/services/interactive-services), the order in the list seems to matter, which seems random-ish.
So I'd fix that implementation and say that it "reports true if the current process is both a member of the Interactive group and not a member of the service group", perhaps with polished wording.
Then adding RunningInService isn't redundant too.
To view, visit change 259397. To unsubscribe, or for help writing mail filters, visit settings.
1 comment:
File windows/svc/security.go:
// IsAnInteractiveSession determines if calling process is running interactively.
func IsAnInteractiveSession() (bool, error) {
ret, err := isWindowsService()
Adding a new function like "RunningInService" sounds fine (the code in the Go runtime is what I want to use and trust, so copying it to a public place like this package is good), but the downside to having two functions (even if one is deprecated) is we'd then want to document a bit what the difference between them are.
Documenting the old one as "basically the opposite of the other one, but kinda a different implementation, I dunno" isn't great documentation, but might be sufficient if you can find some slightly better explanation.
Also, the main reason I want to use the Go runtime implementation is because I understand and trust it, since it's the same implementation in dotnet.
Maybe for the old documentation, you can just explain in English exactly how it works implementation-wise. But looking at the implementation, I feel like you should do two passes over the SID list first and first return false if it contains service, and then do a pass looking for interactive (to return true), and then finally return false as the fallback.
Otherwise for a process in both interactive and service (such a services with the "Interact with Desktop" bit: https://docs.microsoft.com/en-us/windows/win32/services/interactive-services), the order in the list seems to matter, which seems random-ish.
So I'd fix that implementation and say that it "reports true if the current process is both a member of the Interactive group and not a member of the service group", perhaps with polished wording.
Then adding RunningInService isn't redundant too.
I am not sure that changing IsAnInteractiveSession implementation is a good idea - whatever that change is. We don't know what code we will break. And it is not easy to debug services, so breaking code that uses IsAnInteractiveSession is double bad.
And personally I never used IsAnInteractiveSession for anything but to determine, if running in a Windows service. I think that extending IsAnInteractiveSession usage just because its name means something else is not good idea.
I am happy to add more documentation to IsAnInteractiveSession, maybe I will find a way to explain why we need new function.
Do you want new function called svc.IsWindowsService or svc.RunningInService or svc.IsRunningInService or something else? We better come up with a good name this time around.
I am not worried about depreciating IsAnInteractiveSession. This package public API is not very large. So people will read the documentation.
Alex
To view, visit change 259397. To unsubscribe, or for help writing mail filters, visit settings.
Alex Brainman uploaded patch set #2 to this change.
windows/svc: add IsWindowsService function
CL 244958 includes isWindowsService function that determines if a
process is running as a service. The code of the function is based on
public .Net implementation.
IsAnInteractiveSession function implements similar functionality, but
is based on an old Stackoverflow post., which is not as authoritative
as code written by Microsoft for their official product.
This change copies CL 244958 isWindowsService function into svc package
and makes it public. The intention is that future users will prefer
IsWindowsService to IsAnInteractiveSession.
Also this change adds "Deprecated" comment to IsAnInteractiveSession to
point future users to IsWindowsService.
Call to IsAnInteractiveSession is also replaced with IsWindowsService
in golang.org/x/sys/windows/svc/example package.
Change-Id: I4a33b7f590ee8161d1134d8e83668e9da4e6b434
---
M windows/svc/example/main.go
M windows/svc/security.go
M windows/svc/svc_test.go
M windows/syscall_windows.go
M windows/zsyscall_windows.go
5 files changed, 130 insertions(+), 3 deletions(-)
To view, visit change 259397. To unsubscribe, or for help writing mail filters, visit settings.
Patch set 2:Run-TryBot +1
1 comment:
Patchset:
PTAL
Alex
To view, visit change 259397. To unsubscribe, or for help writing mail filters, visit settings.
1 comment:
Patchset:
What are you actually trying to use this for? I'm not totally convinced that IsWindowsService, even as .NET uses it, is foolproof.
What's your end goal?
To view, visit change 259397. To unsubscribe, or for help writing mail filters, visit settings.
1 comment:
File windows/svc/security.go:
Patch Set #2, Line 76: // Windows service. The below technique looks a bit hairy, but it's actually
Move this second sentence "The below technique" into the body of the function instead so it's not part of the godoc.
You can also reference the Go runtime function from the comment inside the func.
To view, visit change 259397. To unsubscribe, or for help writing mail filters, visit settings.
1 comment:
Patchset:
What are you actually trying to use this for? I'm not totally convinced that IsWindowsService, even […]
Alex wants a process to be able to determine at start-up whether it's being run "by hand" (e.g. cmd.exe) or as a service (so it can do the whole svc.Handler thing), without resorting to the user passing a flag to say what mode to run as.
To view, visit change 259397. To unsubscribe, or for help writing mail filters, visit settings.
3 comments:
Patchset:
Alex wants a process to be able to determine at start-up whether it's being run "by hand" (e.g. cmd. […]
Jason, I have single executable that I want to run as both command line app and as a Windows service. Command line app and the service, obviously, need to do different things / execute different code. So I need some way to determine if my process is running as command line app or as a service.
I plan to call svc.IsWindowsService from my executable to determine, if the process is started as a service or as command line / GUI app.
Something similar to what we already do in golang.org/x/sys/windows/svc/example.
I always used golang.org/x/sys/windows/svc.IsAnInteractiveSession for the same purpose, but Brad prefers your version from CL 244958.
Your CL 244958 runtime.isWindowsService serves the same purpose, but we cannot access private runtime.isWindowsService function from our normal code. So I copied it here.
Thank you fro review.
Alex
File windows/svc/security.go:
Patch Set #2, Line 76: // Windows service. The below technique looks a bit hairy, but it's actually
Move this second sentence "The below technique" into the body of the function instead so it's not pa […]
Done
To view, visit change 259397. To unsubscribe, or for help writing mail filters, visit settings.
Alex Brainman uploaded patch set #3 to this change.
windows/svc: add IsWindowsService function
CL 244958 includes isWindowsService function that determines if a
process is running as a service. The code of the function is based on
public .Net implementation.
IsAnInteractiveSession function implements similar functionality, but
is based on an old Stackoverflow post., which is not as authoritative
as code written by Microsoft for their official product.
This change copies CL 244958 isWindowsService function into svc package
and makes it public. The intention is that future users will prefer
IsWindowsService to IsAnInteractiveSession.
Also this change adds "Deprecated" comment to IsAnInteractiveSession to
point future users to IsWindowsService.
Call to IsAnInteractiveSession is also replaced with IsWindowsService
in golang.org/x/sys/windows/svc/example package.
Change-Id: I4a33b7f590ee8161d1134d8e83668e9da4e6b434
---
M windows/svc/example/main.go
M windows/svc/security.go
M windows/svc/svc_test.go
M windows/syscall_windows.go
M windows/zsyscall_windows.go
5 files changed, 132 insertions(+), 3 deletions(-)
To view, visit change 259397. To unsubscribe, or for help writing mail filters, visit settings.
Patch set 3:Run-TryBot +1
Patch set 3:Code-Review +1Trust +1
3 comments:
File windows/svc/security.go:
Patch Set #3, Line 75: // IsWindowsService returns whether the process is currently executing
Go style is "reports whether"
Patch Set #3, Line 83: // This code was copied from runtime.isWindowsService function.
move this comment to the top of the func body, followed by a blank line.
Patch Set #3, Line 122: servicesLower := "services.exe"
slight preference for these being const, but if you want to match runtime exactly, that's fine too.
Except IIRC I also left this comment on the runtime CL that added this.
To view, visit change 259397. To unsubscribe, or for help writing mail filters, visit settings.
Patch set 3:Code-Review +2
1 comment:
Patchset:
LGTM when comments are addressed.
To view, visit change 259397. To unsubscribe, or for help writing mail filters, visit settings.
4 comments:
Patchset:
Thank you for review.
Alex
File windows/svc/security.go:
Patch Set #3, Line 75: // IsWindowsService returns whether the process is currently executing
Go style is "reports whether"
Done
Patch Set #3, Line 83: // This code was copied from runtime.isWindowsService function.
move this comment to the top of the func body, followed by a blank line.
Done
Patch Set #3, Line 122: servicesLower := "services.exe"
slight preference for these being const, but if you want to match runtime exactly, that's fine too. […]
Done here.
Too bad for runtime CL. This code only exists on release-branch. I doubt we will be allowed to adjust our release-branch commit.
To view, visit change 259397. To unsubscribe, or for help writing mail filters, visit settings.
Alex Brainman uploaded patch set #4 to this change.
windows/svc: add IsWindowsService function
CL 244958 includes isWindowsService function that determines if a
process is running as a service. The code of the function is based on
public .Net implementation.
IsAnInteractiveSession function implements similar functionality, but
is based on an old Stackoverflow post., which is not as authoritative
as code written by Microsoft for their official product.
This change copies CL 244958 isWindowsService function into svc package
and makes it public. The intention is that future users will prefer
IsWindowsService to IsAnInteractiveSession.
Also this change adds "Deprecated" comment to IsAnInteractiveSession to
point future users to IsWindowsService.
Call to IsAnInteractiveSession is also replaced with IsWindowsService
in golang.org/x/sys/windows/svc/example package.
Change-Id: I4a33b7f590ee8161d1134d8e83668e9da4e6b434
---
M windows/svc/example/main.go
M windows/svc/security.go
M windows/svc/svc_test.go
M windows/syscall_windows.go
M windows/zsyscall_windows.go
5 files changed, 135 insertions(+), 3 deletions(-)
To view, visit change 259397. To unsubscribe, or for help writing mail filters, visit settings.
Patch set 4:Run-TryBot +1
Alex Brainman submitted this change.
windows/svc: add IsWindowsService function
CL 244958 includes isWindowsService function that determines if a
process is running as a service. The code of the function is based on
public .Net implementation.
IsAnInteractiveSession function implements similar functionality, but
is based on an old Stackoverflow post., which is not as authoritative
as code written by Microsoft for their official product.
This change copies CL 244958 isWindowsService function into svc package
and makes it public. The intention is that future users will prefer
IsWindowsService to IsAnInteractiveSession.
Also this change adds "Deprecated" comment to IsAnInteractiveSession to
point future users to IsWindowsService.
Call to IsAnInteractiveSession is also replaced with IsWindowsService
in golang.org/x/sys/windows/svc/example package.
Change-Id: I4a33b7f590ee8161d1134d8e83668e9da4e6b434
Reviewed-on: https://go-review.googlesource.com/c/sys/+/259397
Run-TryBot: Alex Brainman <alex.b...@gmail.com>
TryBot-Result: Go Bot <go...@golang.org>
Reviewed-by: Ian Lance Taylor <ia...@golang.org>
Trust: Brad Fitzpatrick <brad...@golang.org>
Trust: Alex Brainman <alex.b...@gmail.com>
---
M windows/svc/example/main.go
M windows/svc/security.go
M windows/svc/svc_test.go
M windows/syscall_windows.go
M windows/zsyscall_windows.go
5 files changed, 135 insertions(+), 3 deletions(-)
diff --git a/windows/svc/example/main.go b/windows/svc/example/main.go
index dc96c08..9bf9bbd 100644
--- a/windows/svc/example/main.go
+++ b/windows/svc/example/main.go
@@ -36,11 +36,11 @@
func main() {
const svcName = "myservice"
- isIntSess, err := svc.IsAnInteractiveSession()
+ inService, err := svc.IsWindowsService()
if err != nil {
- log.Fatalf("failed to determine if we are running in an interactive session: %v", err)
+ log.Fatalf("failed to determine if we are running in service: %v", err)
}
- if !isIntSess {
+ if inService {
runService(svcName, false)
return
}
diff --git a/windows/svc/security.go b/windows/svc/security.go
index 6502599..da6df1d 100644
--- a/windows/svc/security.go
+++ b/windows/svc/security.go
@@ -7,6 +7,10 @@
package svc
import (
+ "errors"
+ "syscall"
+ "unsafe"
+
"golang.org/x/sys/windows"
)
@@ -23,6 +27,8 @@
// IsAnInteractiveSession determines if calling process is running interactively.
// It queries the process token for membership in the Interactive group.
// http://stackoverflow.com/questions/2668851/how-do-i-detect-that-my-application-is-running-as-service-or-in-an-interactive-s
+//
+// Deprecated: Use IsWindowsService instead.
func IsAnInteractiveSession() (bool, error) {
interSid, err := allocSid(windows.SECURITY_INTERACTIVE_RID)
if err != nil {
@@ -57,3 +63,95 @@
}
return false, nil
}
+
+var (
+ ntdll = windows.NewLazySystemDLL("ntdll.dll")
+ _NtQueryInformationProcess = ntdll.NewProc("NtQueryInformationProcess")
+
+ kernel32 = windows.NewLazySystemDLL("kernel32.dll")
+ _QueryFullProcessImageNameA = kernel32.NewProc("QueryFullProcessImageNameA")
+)
+
+// IsWindowsService reports whether the process is currently executing
+// as a Windows service.
+func IsWindowsService() (bool, error) {
+ // This code was copied from runtime.isWindowsService function.
+
+ // The below technique looks a bit hairy, but it's actually
+ // exactly what the .NET framework does for the similarly named function:
+ // https://github.com/dotnet/extensions/blob/f4066026ca06984b07e90e61a6390ac38152ba93/src/Hosting/WindowsServices/src/WindowsServiceHelpers.cs#L26-L31
+ // Specifically, it looks up whether the parent process has session ID zero
+ // and is called "services".
+ const _CURRENT_PROCESS = ^uintptr(0)
+ // pbi is a PROCESS_BASIC_INFORMATION struct, where we just care about
+ // the 6th pointer inside of it, which contains the pid of the process
+ // parent:
+ // https://github.com/wine-mirror/wine/blob/42cb7d2ad1caba08de235e6319b9967296b5d554/include/winternl.h#L1294
+ var pbi [6]uintptr
+ var pbiLen uint32
+ r0, _, _ := syscall.Syscall6(_NtQueryInformationProcess.Addr(), 5, _CURRENT_PROCESS, 0, uintptr(unsafe.Pointer(&pbi[0])), uintptr(unsafe.Sizeof(pbi)), uintptr(unsafe.Pointer(&pbiLen)), 0)
+ if r0 != 0 {
+ return false, errors.New("NtQueryInformationProcess failed: error=" + itoa(int(r0)))
+ }
+ var psid uint32
+ err := windows.ProcessIdToSessionId(uint32(pbi[5]), &psid)
+ if err != nil {
+ return false, err
+ }
+ if psid != 0 {
+ // parent session id should be 0 for service process
+ return false, nil
+ }
+
+ pproc, err := windows.OpenProcess(windows.PROCESS_QUERY_LIMITED_INFORMATION, false, uint32(pbi[5]))
+ if err != nil {
+ return false, err
+ }
+ defer windows.CloseHandle(pproc)
+
+ // exeName gets the path to the executable image of the parent process
+ var exeName [261]byte
+ exeNameLen := uint32(len(exeName) - 1)
+ r0, _, e0 := syscall.Syscall6(_QueryFullProcessImageNameA.Addr(), 4, uintptr(pproc), 0, uintptr(unsafe.Pointer(&exeName[0])), uintptr(unsafe.Pointer(&exeNameLen)), 0, 0)
+ if r0 == 0 {
+ if e0 != 0 {
+ return false, e0
+ } else {
+ return false, syscall.EINVAL
+ }
+ }
+ const (
+ servicesLower = "services.exe"
+ servicesUpper = "SERVICES.EXE"
+ )
+ i := int(exeNameLen) - 1
+ j := len(servicesLower) - 1
+ if i < j {
+ return false, nil
+ }
+ for {
+ if j == -1 {
+ return i == -1 || exeName[i] == '\\', nil
+ }
+ if exeName[i] != servicesLower[j] && exeName[i] != servicesUpper[j] {
+ return false, nil
+ }
+ i--
+ j--
+ }
+}
+
+func itoa(val int) string { // do it here rather than with fmt to avoid dependency
+ if val < 0 {
+ return "-" + itoa(-val)
+ }
+ var buf [32]byte // big enough for int64
+ i := len(buf) - 1
+ for val >= 10 {
+ buf[i] = byte(val%10 + '0')
+ i--
+ val /= 10
+ }
+ buf[i] = byte(val + '0')
+ return string(buf[i:])
+}
diff --git a/windows/svc/svc_test.go b/windows/svc/svc_test.go
index 3a80e48..7e87053 100644
--- a/windows/svc/svc_test.go
+++ b/windows/svc/svc_test.go
@@ -132,3 +132,23 @@
t.Errorf("%q string does not contain %q", string(out), want)
}
}
+
+func TestIsAnInteractiveSession(t *testing.T) {
+ isInteractive, err := svc.IsAnInteractiveSession()
+ if err != nil {
+ t.Fatal(err)
+ }
+ if !isInteractive {
+ t.Error("IsAnInteractiveSession retuns false when running interactively.")
+ }
+}
+
+func TestIsWindowsService(t *testing.T) {
+ isSvc, err := svc.IsWindowsService()
+ if err != nil {
+ t.Fatal(err)
+ }
+ if isSvc {
+ t.Error("IsWindowsService retuns true when not running in a service.")
+ }
+}
diff --git a/windows/syscall_windows.go b/windows/syscall_windows.go
index 30ef641..65ca6d2 100644
--- a/windows/syscall_windows.go
+++ b/windows/syscall_windows.go
@@ -270,6 +270,7 @@
//sys RegEnumKeyEx(key Handle, index uint32, name *uint16, nameLen *uint32, reserved *uint32, class *uint16, classLen *uint32, lastWriteTime *Filetime) (regerrno error) = advapi32.RegEnumKeyExW
//sys RegQueryValueEx(key Handle, name *uint16, reserved *uint32, valtype *uint32, buf *byte, buflen *uint32) (regerrno error) = advapi32.RegQueryValueExW
//sys GetCurrentProcessId() (pid uint32) = kernel32.GetCurrentProcessId
+//sys ProcessIdToSessionId(pid uint32, sessionid *uint32) (err error) = kernel32.ProcessIdToSessionId
//sys GetConsoleMode(console Handle, mode *uint32) (err error) = kernel32.GetConsoleMode
//sys SetConsoleMode(console Handle, mode uint32) (err error) = kernel32.SetConsoleMode
//sys GetConsoleScreenBufferInfo(console Handle, info *ConsoleScreenBufferInfo) (err error) = kernel32.GetConsoleScreenBufferInfo
diff --git a/windows/zsyscall_windows.go b/windows/zsyscall_windows.go
index f1ae76b..f6cef2b 100644
--- a/windows/zsyscall_windows.go
+++ b/windows/zsyscall_windows.go
@@ -180,6 +180,7 @@
procRegEnumKeyExW = modadvapi32.NewProc("RegEnumKeyExW")
procRegQueryValueExW = modadvapi32.NewProc("RegQueryValueExW")
procGetCurrentProcessId = modkernel32.NewProc("GetCurrentProcessId")
+ procProcessIdToSessionId = modkernel32.NewProc("ProcessIdToSessionId")
procGetConsoleMode = modkernel32.NewProc("GetConsoleMode")
procSetConsoleMode = modkernel32.NewProc("SetConsoleMode")
procGetConsoleScreenBufferInfo = modkernel32.NewProc("GetConsoleScreenBufferInfo")
@@ -1943,6 +1944,18 @@
return
}
+func ProcessIdToSessionId(pid uint32, sessionid *uint32) (err error) {
+ r1, _, e1 := syscall.Syscall(procProcessIdToSessionId.Addr(), 2, uintptr(pid), uintptr(unsafe.Pointer(sessionid)), 0)
+ if r1 == 0 {
+ if e1 != 0 {
+ err = errnoErr(e1)
+ } else {
+ err = syscall.EINVAL
+ }
+ }
+ return
+}
+
func GetConsoleMode(console Handle, mode *uint32) (err error) {
r1, _, e1 := syscall.Syscall(procGetConsoleMode.Addr(), 2, uintptr(console), uintptr(unsafe.Pointer(mode)), 0)
if r1 == 0 {
To view, visit change 259397. To unsubscribe, or for help writing mail filters, visit settings.
Patch set 5:Run-TryBot +1Trust +1
2 comments:
Patchset:
Pinging Jason.
Can you help me, please. See below.
Thank you.
File windows/svc/security.go:
Patch Set #5, Line 99: return false, err
Jason.
I am trying to use this code. I am running this function in the process on my desktop.
I SOMETIMES get ERROR_INVALID_PARAMETER returned by ProcessIdToSessionId.
So it appears that pbi[5] is incorrectly set.
Why are we using NtQueryInformationProcess in the first place? Can we just use os.Getpid here instead? I tried to print both os.Getpid returned value and pbi[5], and they look different.
Why am I getting ERROR_INVALID_PARAMETER returned from ProcessIdToSessionId?
To view, visit change 259397. To unsubscribe, or for help writing mail filters, visit settings.
Patchset:
Never mind, Jason.
I have found my problem.
Alex
File windows/svc/security.go:
Patch Set #5, Line 99: return false, err
Jason. […]
I just re-read the comment above. pbi[5] is process *parent* pid.
So that explains why ProcessIdToSessionId - parent process, probably, exited.
We should return "false", here instead of returning error.
Just like you did in
To view, visit change 259397. To unsubscribe, or for help writing mail filters, visit settings.
Attention is currently required from: Alex Brainman.
5 comments:
Patchset:
This was merged prematurely and contains problems. Please address my comments in a follow-up commit.
File windows/svc/security.go:
Patch Set #5, Line 94: return false, errors.New("NtQueryInformationProcess failed: error=" + itoa(int(r0)))
Use fmt.Errorf with %x.
Patch Set #5, Line 99: return false, err
I just re-read the comment above. pbi[5] is process *parent* pid. […]
Is there a reason why you're not using my code more verbatim? I can re-do all the work I did before that I've already paged out of my brain, but it seems like it'd be easier to just start with what I did before, and then only modify parts one at a time that you a) understand and b) can verify work. This way we don't have regressions that take additional work to figure out.
Patch Set #5, Line 136: if exeName[i] != servicesLower[j] && exeName[i] != servicesUpper[j] {
The reason there's this manual compare loop is because I originally implemented this in runtime, which can't access packages like strings. However, this code here should just be a strings.EqualsFold or whatever it's called.
Patch Set #5, Line 144: func itoa(val int) string { // do it here rather than with fmt to avoid dependency
This does not belong in x/sys. See earlier comment.
To view, visit change 259397. To unsubscribe, or for help writing mail filters, visit settings.
Attention is currently required from: Alex Brainman.
3 comments:
Patchset:
More nits.
I'm going to open a revert CL for this, as this seems to be really quite rough, with manual syscalls and stuff in there.
File windows/svc/security.go:
Patch Set #5, Line 92: r0, _, _ := syscall.Syscall6(_NtQueryInformationProcess.Addr(), 5, _CURRENT_PROCESS, 0, uintptr(unsafe.Pointer(&pbi[0])), uintptr(unsafe.Sizeof(pbi)), uintptr(unsafe.Pointer(&pbiLen)), 0)
Shouldn't NtQueryInformationProcess and QueryFullProcessImageNameA just get added to x/sys like normal?
And don't we already a variable for currentprocess in x/sys?
Patch Set #5, Line 115: r0, _, e0 := syscall.Syscall6(_QueryFullProcessImageNameA.Addr(), 4, uintptr(pproc), 0, uintptr(unsafe.Pointer(&exeName[0])), uintptr(unsafe.Pointer(&exeNameLen)), 0, 0)
Again, this should be moved to x/sys.
To view, visit change 259397. To unsubscribe, or for help writing mail filters, visit settings.
Attention is currently required from: Alex Brainman.
Jason A. Donenfeld has created a revert of this change.
To view, visit change 259397. To unsubscribe, or for help writing mail filters, visit settings.
Patch set 5:Run-TryBot +1Trust +1
8 comments:
Patchset:
This was merged prematurely and contains problems. Please address my comments in a follow-up commit.
Thanks for your comments.
You had 4 days to comment on this CL. But then I received +2 so I submitted.
If you knew this CL contained problems, why did not you comment earlier?
I will address your comments in the new CL.
Jason,
Here is the CL that fixes the problem I discovered.
https://go-review.googlesource.com/c/sys/+/294729
I will address all your other comments in a separate CL or CLs (once CL 294729 is submitted). I don't want to mix bug fix with unrelated changes.
Let me know, if you have a problem with that.
Thank you.
Alex
File windows/svc/security.go:
Patch Set #5, Line 92: r0, _, _ := syscall.Syscall6(_NtQueryInformationProcess.Addr(), 5, _CURRENT_PROCESS, 0, uintptr(unsafe.Pointer(&pbi[0])), uintptr(unsafe.Sizeof(pbi)), uintptr(unsafe.Pointer(&pbiLen)), 0)
Shouldn't NtQueryInformationProcess and QueryFullProcessImageNameA just get added to x/sys like norm […]
Ack
Patch Set #5, Line 94: return false, errors.New("NtQueryInformationProcess failed: error=" + itoa(int(r0)))
Use fmt.Errorf with %x.
Ack
Patch Set #5, Line 99: return false, err
Is there a reason why you're not using my code more verbatim? I can re-do all the work I did before […]
I did the best I could do at a time.
Let's fix it instead of trying to understand why I made mistakes.
Patch Set #5, Line 115: r0, _, e0 := syscall.Syscall6(_QueryFullProcessImageNameA.Addr(), 4, uintptr(pproc), 0, uintptr(unsafe.Pointer(&exeName[0])), uintptr(unsafe.Pointer(&exeNameLen)), 0, 0)
Again, this should be moved to x/sys.
Ack
Patch Set #5, Line 136: if exeName[i] != servicesLower[j] && exeName[i] != servicesUpper[j] {
The reason there's this manual compare loop is because I originally implemented this in runtime, whi […]
Ack
Patch Set #5, Line 144: func itoa(val int) string { // do it here rather than with fmt to avoid dependency
This does not belong in x/sys. See earlier comment.
Ack
To view, visit change 259397. To unsubscribe, or for help writing mail filters, visit settings.