SSH流程中PAM模块内Goroutine无法启动问题求助
解决Go编写的PAM模块在SSH环境中Goroutine无法运行的问题
我用Go开发了一个SSH授权的PAM模块,用pamtester测试时一切正常,但在SSH流程中,模块内的Goroutine完全无法启动。核心需求是通过HTTP请求完成授权,但标准库http.Client会自动创建Goroutine,导致请求卡住。以下是相关代码和环境信息:
代码示例
package main /* #include <security/pam_modules.h> typedef const char cchar_t; */ import "C" import ( "log" ) func main() {} //export pam_sm_authenticate func pam_sm_authenticate(pamh *C.pam_handle_t, flags C.int, argc C.int, argv **C.cchar_t) C.int { log.Printf("golib.so: before start goroutine") go func() { // SSH环境下这段代码永远不会执行 log.Printf("golib.so: something out of the goroutine") }() log.Printf("golib.so: after start goroutine") return C.PAM_SUCCESS } //export pam_sm_setcred func pam_sm_setcred(pamh *C.pam_handle_t, flags C.int, argc C.int, argv **C.cchar_t) C.int { return C.PAM_IGNORE }
环境信息
- OS: Ubuntu 22.04.4 LTS
- SSH: OpenSSH_8.9p1 Ubuntu-3ubuntu0.10, OpenSSL 3.0.2 15 Mar 2022
- Go: go1.22.5 linux/amd64
- pamtester: 0.1.2
解决方案
方案一:让Goroutine在SSH的PAM环境中正常运行
问题根源在于SSH调用PAM模块时,运行环境可能限制了线程创建,或者当前线程在Go函数返回后脱离了Go runtime的控制,导致Goroutine无法被调度执行。可以通过以下方式解决:
- 同步等待Goroutine完成
如果逻辑需要等待Goroutine执行完毕(比如HTTP请求的回调),可以使用sync.WaitGroup阻塞当前线程,直到Goroutine完成:
import "sync" //export pam_sm_authenticate func pam_sm_authenticate(pamh *C.pam_handle_t, flags C.int, argc C.int, argv **C.cchar_t) C.int { log.Printf("golib.so: before start goroutine") var wg sync.WaitGroup wg.Add(1) go func() { defer wg.Done() log.Printf("golib.so: something out of the goroutine") // 这里执行HTTP请求或其他逻辑 }() wg.Wait() // 等待Goroutine完成后再返回 log.Printf("golib.so: after goroutine finished") return C.PAM_SUCCESS }
- 强制Go调度器执行Goroutine
如果不需要等待Goroutine完成,但需要确保它能被调度,可以在启动Goroutine后调用runtime.Gosched()让出当前线程的执行权,同时短暂等待调度器切换:
import "runtime" import "time" //export pam_sm_authenticate func pam_sm_authenticate(pamh *C.pam_handle_t, flags C.int, argc C.int, argv **C.cchar_t) C.int { log.Printf("golib.so: before start goroutine") go func() { log.Printf("golib.so: something out of the goroutine") }() runtime.Gosched() // 让出当前线程,让调度器处理其他Goroutine time.Sleep(50 * time.Millisecond) // 短暂等待确保Goroutine有机会执行 log.Printf("golib.so: after start goroutine") return C.PAM_SUCCESS }
- 绑定线程到Go runtime
在PAM函数开头调用runtime.LockOSThread(),将当前C线程绑定到Go的调度线程,确保Go runtime不会在函数返回后销毁该线程,从而让后续Goroutine有机会执行:
import "runtime" //export pam_sm_authenticate func pam_sm_authenticate(pamh *C.pam_handle_t, flags C.int, argc C.int, argv **C.cchar_t) C.int { runtime.LockOSThread() defer runtime.UnlockOSThread() log.Printf("golib.so: before start goroutine") go func() { log.Printf("golib.so: something out of the goroutine") }() log.Printf("golib.so: after start goroutine") return C.PAM_SUCCESS }
方案二:使用无Goroutine的HTTP请求
如果不想依赖Go的Goroutine,可以手动实现基于TCP的HTTP请求,完全在当前线程同步执行,避免任何Goroutine创建:
import ( "fmt" "io" "net" "net/url" "strings" ) // 手动发送HTTP GET请求,无Goroutine依赖 func syncHTTPGet(rawURL string) ([]byte, error) { // 解析URL u, err := url.Parse(rawURL) if err != nil { return nil, err } // 处理主机和端口 host := u.Host if !strings.Contains(host, ":") { host += ":80" // 默认HTTP端口 } // 建立TCP连接 conn, err := net.Dial("tcp", host) if err != nil { return nil, err } defer conn.Close() // 构造HTTP请求 request := fmt.Sprintf( "GET %s HTTP/1.1\r\nHost: %s\r\nConnection: close\r\n\r\n", u.Path, u.Host, ) // 发送请求 _, err = conn.Write([]byte(request)) if err != nil { return nil, err } // 读取响应 var response []byte buf := make([]byte, 4096) for { n, err := conn.Read(buf) if err != nil { if err == io.EOF { break } return nil, err } response = append(response, buf[:n]...) } return response, nil } // 在PAM函数中调用 //export pam_sm_authenticate func pam_sm_authenticate(pamh *C.pam_handle_t, flags C.int, argc C.int, argv **C.cchar_t) C.int { resp, err := syncHTTPGet("http://your-auth-server.com/check") if err != nil { log.Printf("HTTP request failed: %v", err) return C.PAM_AUTH_ERR } log.Printf("Auth response: %s", resp) return C.PAM_SUCCESS }
为什么pamtester正常而SSH不行?
pamtester运行在普通进程环境中,允许Go runtime创建新的操作系统线程来调度Goroutine;而SSH调用PAM模块时,可能处于受限的线程上下文(比如单线程环境、进程资源限制),导致Go无法创建新线程,Goroutine被永久挂起无法执行。
内容的提问来源于stack exchange,提问作者GreNodge
相关产品推荐
相关产品推荐

