diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index da8e2ca..afe433f 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -13,19 +13,38 @@ jobs: with: go-version: ${{ matrix.go-version }} - name: Install PAM - run: sudo apt install -y libpam-dev + run: | + sudo apt update -y + sudo apt install -y libpam-dev + - name: Install Debug symbols + run: | + sudo apt install -y ubuntu-dev-tools + (cd /tmp && pull-lp-ddebs libpam0g $(lsb_release -c -s)) + (cd /tmp && pull-lp-ddebs libpam-modules $(lsb_release -c -s)) + sudo dpkg -i /tmp/libpam*-dbgsym_*.ddeb - name: Add a test user run: sudo useradd -d /tmp/test -p '$1$Qd8H95T5$RYSZQeoFbEB.gS19zS99A0' -s /bin/false test - name: Checkout code uses: actions/checkout@v4 + - name: Test + run: sudo go test -v -cover -coverprofile=coverage.out ./... + - name: Test with Address Sanitizer + env: + GO_PAM_TEST_WITH_ASAN: true + CGO_CFLAGS: "-O0 -g3 -fno-omit-frame-pointer" + run: | + # Do not run sudo-requiring go tests because as PAM has some leaks in 22.04 + go test -v -asan -cover -coverprofile=coverage-asan-tx.out -gcflags=all="-N -l" + + # Run the rest of tests normally + sudo go test -v -cover -coverprofile=coverage-asan-module.out -asan -gcflags=all="-N -l" -run Module + sudo go test -C cmd -coverprofile=coverage-asan.out -v -asan -gcflags=all="-N -l" ./... - name: Generate example module run: | rm -f example-module/pam_go.so go generate -C example-module -v test -e example-module/pam_go.so git diff --exit-code example-module - - name: Test - run: sudo go test -v -cover -coverprofile=coverage.out ./... - name: Upload coverage reports to Codecov uses: codecov/codecov-action@v3 env: diff --git a/.gitignore b/.gitignore index 0700a89..8206d2f 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,4 @@ -coverage.out +coverage*.out example-module/*.so example-module/*.h cmd/pam-moduler/tests/*/*.so diff --git a/module-transaction_test.go b/module-transaction_test.go index 2e678e0..cf64085 100644 --- a/module-transaction_test.go +++ b/module-transaction_test.go @@ -23,6 +23,7 @@ func ensureNoError(t *testing.T, err error) { func Test_NewNullModuleTransaction(t *testing.T) { t.Parallel() + t.Cleanup(maybeDoLeakCheck) mt := moduleTransaction{} if mt.handle != nil { @@ -137,6 +138,7 @@ func Test_NewNullModuleTransaction(t *testing.T) { tc := tc t.Run(name+"-error-check", func(t *testing.T) { t.Parallel() + t.Cleanup(maybeDoLeakCheck) data, err := tc.testFunc(t) switch d := data.(type) { @@ -202,6 +204,7 @@ func Test_NewNullModuleTransaction(t *testing.T) { func Test_ModuleTransaction_InvokeHandler(t *testing.T) { t.Parallel() + t.Cleanup(maybeDoLeakCheck) mt := &moduleTransaction{} err := mt.InvokeHandler(nil, 0, nil) @@ -308,6 +311,7 @@ func Test_ModuleTransaction_InvokeHandler(t *testing.T) { func testMockModuleTransaction(t *testing.T, mt *moduleTransaction) { t.Helper() t.Parallel() + t.Cleanup(maybeDoLeakCheck) tests := map[string]struct { testFunc func(mock *mockModuleTransaction) (any, error) @@ -898,6 +902,7 @@ func testMockModuleTransaction(t *testing.T, mt *moduleTransaction) { tc := tc t.Run(name, func(t *testing.T) { t.Parallel() + t.Cleanup(maybeDoLeakCheck) mock := newMockModuleTransaction(&mockModuleTransaction{T: t, Expectations: tc.mockExpectations, RetData: tc.mockRetData, ConversationHandler: tc.conversationHandler}) diff --git a/transaction_test.go b/transaction_test.go index d358b0e..3166159 100644 --- a/transaction_test.go +++ b/transaction_test.go @@ -39,6 +39,7 @@ func ensureTransactionEnds(t *testing.T, tx *Transaction) { } func TestPAM_001(t *testing.T) { + t.Cleanup(maybeDoLeakCheck) u, _ := user.Current() if u.Uid != "0" { t.Skip("run this test as root") @@ -67,6 +68,7 @@ func TestPAM_001(t *testing.T) { } func TestPAM_002(t *testing.T) { + t.Cleanup(maybeDoLeakCheck) u, _ := user.Current() if u.Uid != "0" { t.Skip("run this test as root") @@ -107,6 +109,7 @@ func (c Credentials) RespondPAM(s Style, msg string) (string, error) { } func TestPAM_003(t *testing.T) { + t.Cleanup(maybeDoLeakCheck) u, _ := user.Current() if u.Uid != "0" { t.Skip("run this test as root") @@ -128,6 +131,7 @@ func TestPAM_003(t *testing.T) { } func TestPAM_004(t *testing.T) { + t.Cleanup(maybeDoLeakCheck) u, _ := user.Current() if u.Uid != "0" { t.Skip("run this test as root") @@ -148,10 +152,14 @@ func TestPAM_004(t *testing.T) { } func TestPAM_005(t *testing.T) { + t.Cleanup(maybeDoLeakCheck) u, _ := user.Current() if u.Uid != "0" { t.Skip("run this test as root") } + if _, found := os.LookupEnv("GO_PAM_TEST_WITH_ASAN"); found { + t.Skip("test fails under ASAN") + } tx, err := StartFunc("passwd", "test", func(s Style, msg string) (string, error) { return "secret", nil }) @@ -174,6 +182,7 @@ func TestPAM_005(t *testing.T) { } func TestPAM_006(t *testing.T) { + t.Cleanup(maybeDoLeakCheck) u, _ := user.Current() if u.Uid != "0" { t.Skip("run this test as root") @@ -197,6 +206,7 @@ func TestPAM_006(t *testing.T) { } func TestPAM_007(t *testing.T) { + t.Cleanup(maybeDoLeakCheck) u, _ := user.Current() if u.Uid != "0" { t.Skip("run this test as root") @@ -223,6 +233,7 @@ func TestPAM_007(t *testing.T) { } func TestPAM_ConfDir(t *testing.T) { + t.Cleanup(maybeDoLeakCheck) u, _ := user.Current() c := Credentials{ // the custom service always permits even with wrong password. @@ -258,6 +269,7 @@ func TestPAM_ConfDir(t *testing.T) { } func TestPAM_ConfDir_FailNoServiceOrUnsupported(t *testing.T) { + t.Cleanup(maybeDoLeakCheck) if !CheckPamHasStartConfdir() { t.Skip("this requires PAM with Conf dir support") } @@ -286,6 +298,7 @@ func TestPAM_ConfDir_FailNoServiceOrUnsupported(t *testing.T) { } func TestPAM_ConfDir_InfoMessage(t *testing.T) { + t.Cleanup(maybeDoLeakCheck) u, _ := user.Current() var infoText string tx, err := StartConfDir("echo-service", u.Username, @@ -319,6 +332,7 @@ func TestPAM_ConfDir_InfoMessage(t *testing.T) { } func TestPAM_ConfDir_Deny(t *testing.T) { + t.Cleanup(maybeDoLeakCheck) if !CheckPamHasStartConfdir() { t.Skip("this requires PAM with Conf dir support") } @@ -350,6 +364,7 @@ func TestPAM_ConfDir_Deny(t *testing.T) { } func TestPAM_ConfDir_PromptForUserName(t *testing.T) { + t.Cleanup(maybeDoLeakCheck) c := Credentials{ User: "testuser", // the custom service only cares about correct user name. @@ -375,6 +390,7 @@ func TestPAM_ConfDir_PromptForUserName(t *testing.T) { } func TestPAM_ConfDir_WrongUserName(t *testing.T) { + t.Cleanup(maybeDoLeakCheck) c := Credentials{ User: "wronguser", Password: "wrongsecret", @@ -403,6 +419,7 @@ func TestPAM_ConfDir_WrongUserName(t *testing.T) { } func TestItem(t *testing.T) { + t.Cleanup(maybeDoLeakCheck) tx, err := StartFunc("passwd", "test", func(s Style, msg string) (string, error) { return "", nil }) @@ -442,6 +459,7 @@ func TestItem(t *testing.T) { } func TestEnv(t *testing.T) { + t.Cleanup(maybeDoLeakCheck) tx, err := StartFunc("", "", func(s Style, msg string) (string, error) { return "", nil }) @@ -511,6 +529,7 @@ func TestEnv(t *testing.T) { func Test_Error(t *testing.T) { t.Parallel() + t.Cleanup(maybeDoLeakCheck) if !CheckPamHasStartConfdir() { t.Skip("this requires PAM with Conf dir support") } @@ -624,6 +643,7 @@ func Test_Error(t *testing.T) { } func Test_Finalizer(t *testing.T) { + t.Cleanup(maybeDoLeakCheck) if !CheckPamHasStartConfdir() { t.Skip("this requires PAM with Conf dir support") } @@ -643,6 +663,7 @@ func Test_Finalizer(t *testing.T) { } func TestFailure_001(t *testing.T) { + t.Cleanup(maybeDoLeakCheck) tx := Transaction{} _, err := tx.GetEnvList() if err == nil { @@ -651,6 +672,7 @@ func TestFailure_001(t *testing.T) { } func TestFailure_002(t *testing.T) { + t.Cleanup(maybeDoLeakCheck) tx := Transaction{} err := tx.PutEnv("") if err == nil { @@ -659,6 +681,7 @@ func TestFailure_002(t *testing.T) { } func TestFailure_003(t *testing.T) { + t.Cleanup(maybeDoLeakCheck) tx := Transaction{} err := tx.CloseSession(0) if err == nil { @@ -667,6 +690,7 @@ func TestFailure_003(t *testing.T) { } func TestFailure_004(t *testing.T) { + t.Cleanup(maybeDoLeakCheck) tx := Transaction{} err := tx.OpenSession(0) if err == nil { @@ -675,6 +699,7 @@ func TestFailure_004(t *testing.T) { } func TestFailure_005(t *testing.T) { + t.Cleanup(maybeDoLeakCheck) tx := Transaction{} err := tx.ChangeAuthTok(0) if err == nil { @@ -683,6 +708,7 @@ func TestFailure_005(t *testing.T) { } func TestFailure_006(t *testing.T) { + t.Cleanup(maybeDoLeakCheck) tx := Transaction{} err := tx.AcctMgmt(0) if err == nil { @@ -691,6 +717,7 @@ func TestFailure_006(t *testing.T) { } func TestFailure_007(t *testing.T) { + t.Cleanup(maybeDoLeakCheck) tx := Transaction{} err := tx.SetCred(0) if err == nil { @@ -699,6 +726,7 @@ func TestFailure_007(t *testing.T) { } func TestFailure_008(t *testing.T) { + t.Cleanup(maybeDoLeakCheck) tx := Transaction{} err := tx.SetItem(User, "test") if err == nil { @@ -707,6 +735,7 @@ func TestFailure_008(t *testing.T) { } func TestFailure_009(t *testing.T) { + t.Cleanup(maybeDoLeakCheck) tx := Transaction{} _, err := tx.GetItem(User) if err == nil { @@ -715,6 +744,7 @@ func TestFailure_009(t *testing.T) { } func TestFailure_010(t *testing.T) { + t.Cleanup(maybeDoLeakCheck) tx := Transaction{} err := tx.End() if err != nil { diff --git a/utils.go b/utils.go new file mode 100644 index 0000000..d094c30 --- /dev/null +++ b/utils.go @@ -0,0 +1,31 @@ +// Package pam provides a wrapper for the PAM application API. +package pam + +/* +#ifdef __SANITIZE_ADDRESS__ +#include +#endif + +static inline void +maybe_do_leak_check (void) +{ +#ifdef __SANITIZE_ADDRESS__ + __lsan_do_leak_check(); +#endif +} +*/ +import "C" + +import ( + "os" + "runtime" + "time" +) + +func maybeDoLeakCheck() { + runtime.GC() + time.Sleep(time.Millisecond * 20) + if os.Getenv("GO_PAM_SKIP_LEAK_CHECK") == "" { + C.maybe_do_leak_check() + } +}