Repository: Azure/azure-osconfig
Pull request #992: Add clang static analysis worfklow
Author: login: jepio
Created: 2025-04-24T14:28:27Z | Merged: 2025-04-28T13:40:31Z

## Description

Add clang static analysis to our CI. This relies on clang's internal analyzer, called through a script shipped with clang called analyze-build (there exists also a helper tool called scan-build).

This PR also fixes all of the findings. The findings were (in quantity)

* dead stores (writes to variables that are never used or read)
* memory leaks (missing frees on some paths through functions)
* destroying uninitialized objects (OsConfigResource.c)
* mismatched new[]/delete[] (mmi payloads should be free'd using the modules free function)
* use after move in tests disabled using ifdef __clang_analyzer__ (the tests intentionally test to move constructor).

If there are findings in the future, the best way is to download the artifact from the workflow, unpack and open index.html which provides a step-by-step explanation for each finding.

## Checklist

- [x] I have read the [contribution guidelines](https://github.com/Azure/azure-osconfig/blob/main/CONTRIBUTING.md).
- [x] I added unit-tests to validate my changes. All unit tests are passing.
- [x] I have merged the latest `dev` branch prior to this PR submission.
- [x] I ran [pre-commit](https://pre-commit.com/) on my changes prior to this PR submission.
- [x] I submitted this PR against the `dev` branch.



Diff:
diff --git a/.github/workflows/static-analysis.yml b/.github/workflows/static-analysis.yml
new file mode 100644
index 0000000000..95462f6f74
--- /dev/null
+++ b/.github/workflows/static-analysis.yml
@@ -0,0 +1,48 @@
+name: Static analysis
+
+on: [push, pull_request]
+
+jobs:
+  analyze:
+    name: Analyze
+    runs-on: ubuntu-latest
+    container:
+      image: ghcr.io/azure/azure-osconfig/ubuntu-22.04-amd64
+
+    steps:
+    - uses: actions/checkout@v4
+      with:
+        submodules: recursive
+        clean: true
+
+    - name: Prepare compile command database
+      run: |
+        mkdir -p build
+        cd build
+        cmake -DCMAKE_EXPORT_COMPILE_COMMANDS=ON -DCMAKE_BUILD_TYPE=Debug ../src
+        cmake --build . --target ProcedureMap
+
+    - name: "Temporary: install clang-tools"
+      run: |
+        apt-get update
+        apt-get install -y clang-tools
+
+    - name: Run clang static analyzer
+      run: |
+        mkdir -p output
+        analyze-build-14 --verbose \
+          --status-bugs \
+          --exclude src/adapters/pnp/azure-iot-sdk-c \
+          --exclude src/adapters/pnp/PnpUtils.c \
+          --exclude src/adapters/pnp/AisUtils.c \
+          --exclude src/common/parson \
+          --exclude src/modules/commandrunner/src/lib \
+          --cdb build/compile_commands.json \
+          --output output/scan-build
+
+    - uses: actions/upload-artifact@v4
+      if: success() || failure()
+      with:
+        name: scan-build
+        path: |
+          output/scan-build
diff --git a/devops/docker/centos-8-amd64/Dockerfile b/devops/docker/centos-8-amd64/Dockerfile
index 32b542210a..1c2421be05 100644
--- a/devops/docker/centos-8-amd64/Dockerfile
+++ b/devops/docker/centos-8-amd64/Dockerfile
@@ -1,5 +1,5 @@
 # TODO: Add mcr.io retagged images
-FROM centos:latest
+FROM centos:8
 
 # CentOS Linux 8 had reached the End Of Life (EOL) on December 31st, 2021.
 # It means that CentOS 8 will no longer receive development resources from
diff --git a/devops/docker/ubuntu-22.04-amd64/Dockerfile b/devops/docker/ubuntu-22.04-amd64/Dockerfile
index a23aa3ea83..b9f9dbc95c 100644
--- a/devops/docker/ubuntu-22.04-amd64/Dockerfile
+++ b/devops/docker/ubuntu-22.04-amd64/Dockerfile
@@ -24,7 +24,8 @@ RUN apt -y update && apt-get -y install \
     file \
     libasan8 \
     libubsan1 \
-    clang
+    clang \
+    clang-tools
 
 WORKDIR /git
 
diff --git a/src/adapters/mc/OsConfigResource.c b/src/adapters/mc/OsConfigResource.c
index abd4d99b86..238f8948be 100644
--- a/src/adapters/mc/OsConfigResource.c
+++ b/src/adapters/mc/OsConfigResource.c
@@ -533,7 +533,7 @@ void MI_CALL OsConfigResource_Invoke_GetTargetResource(
     MI_Value miValueReasonResult = {0};
     MI_Boolean isCompliant = MI_FALSE;
 
-    OsConfigResource_GetTargetResource get_result_object;
+    OsConfigResource_GetTargetResource get_result_object = {{0},{0},{0},{0},{0}};
 
     if ((NULL == in) || (MI_FALSE == in->InputResource.exists) || (NULL == in->InputResource.value))
     {
@@ -1170,7 +1170,6 @@ void MI_CALL OsConfigResource_Invoke_TestTargetResource(
                 {
                     // We have both a procedure object name and value, we need to set then now to apply context for compliance
                     SetDesiredObjectValueToDevice("OsConfigResource.Test", g_componentName, g_procedureObjectName, g_procedureObjectValue, context);
-                    miResult = MI_RESULT_FAILED;
                 }
             }
             else
@@ -1261,7 +1260,7 @@ void MI_CALL OsConfigResource_Invoke_SetTargetResource(
     MI_Result miResult = MI_RESULT_OK;
     MI_Result miCleanup = MI_RESULT_OK;
 
-    OsConfigResource_SetTargetResource set_result_object;
+    OsConfigResource_SetTargetResource set_result_object = {{0},{0},{0},{0},{0}};
 
     if ((NULL == in) || (MI_FALSE == in->InputResource.exists) || (NULL == in->InputResource.value))
     {
diff --git a/src/adapters/pnp/PnpAgent.c b/src/adapters/pnp/PnpAgent.c
index 07c74e87c2..c2178e55dd 100644
--- a/src/adapters/pnp/PnpAgent.c
+++ b/src/adapters/pnp/PnpAgent.c
@@ -122,9 +122,6 @@ static void SignalInterrupt(int signal)
 {
     int logDescriptor = -1;
     char* errorMessage = NULL;
-    ssize_t writeResult = -1;
-
-    UNUSED(writeResult);
 
     if (SIGSEGV == signal)
     {
@@ -156,7 +153,9 @@ static void SignalInterrupt(int signal)
     {
         if (0 < (logDescriptor = open(LOG_FILE, O_APPEND | O_WRONLY | O_NONBLOCK)))
         {
+            ssize_t writeResult = -1;
             writeResult = write(logDescriptor, (const void*)errorMessage, strlen(errorMessage));
+            UNUSED(writeResult);
             close(logDescriptor);
         }
         _exit(signal);
@@ -597,6 +596,9 @@ int main(int argc, char *argv[])
     FREE_MEMORY(cpuType);
     FREE_MEMORY(cpuVendor);
     FREE_MEMORY(cpuModel);
+    FREE_MEMORY(kernelName);
+    FREE_MEMORY(kernelRelease);
+    FREE_MEMORY(kernelVersion);
     FREE_MEMORY(productName);
     FREE_MEMORY(productVendor);
     FREE_MEMORY(encodedProductInfo);
@@ -709,6 +711,7 @@ int main(int argc, char *argv[])
 done:
     OsConfigLogInfo(GetLog(), "OSConfig Agent (PID: %d) exiting with %d", pid, g_stopSignal);
 
+    FREE_MEMORY(jsonConfiguration);
     FREE_MEMORY(g_x509Certificate);
     FREE_MEMORY(g_x509PrivateKeyHandle);
     FREE_MEMORY(connectionString);
diff --git a/src/common/commonutils/CommonUtils.h b/src/common/commonutils/CommonUtils.h
index b29f007d8e..18df7370b5 100644
--- a/src/common/commonutils/CommonUtils.h
+++ b/src/common/commonutils/CommonUtils.h
@@ -230,7 +230,7 @@ typedef struct ReportedProperty
 } ReportedProperty;
 
 bool IsIotHubManagementEnabledInJsonConfig(const char* jsonString);
-int GetLoggingLevelFromJsonConfig(const char* jsonString, OsConfigLogHandle log);
+LoggingLevel GetLoggingLevelFromJsonConfig(const char* jsonString, OsConfigLogHandle log);
 int GetMaxLogSizeFromJsonConfig(const char* jsonString, OsConfigLogHandle log);
 int GetMaxLogSizeDebugMultiplierFromJsonConfig(const char* jsonString, OsConfigLogHandle log);
 int GetReportingIntervalFromJsonConfig(const char* jsonString, OsConfigLogHandle log);
diff --git a/src/common/commonutils/ConfigUtils.c b/src/common/commonutils/ConfigUtils.c
index 6c122fa5bd..82c59e5a19 100644
--- a/src/common/commonutils/ConfigUtils.c
+++ b/src/common/commonutils/ConfigUtils.c
@@ -131,7 +131,7 @@ static int GetIntegerFromJsonConfig(const char* valueName, const char* jsonStrin
     return valueToReturn;
 }
 
-int GetLoggingLevelFromJsonConfig(const char* jsonString, OsConfigLogHandle log)
+LoggingLevel GetLoggingLevelFromJsonConfig(const char* jsonString, OsConfigLogHandle log)
 {
     return GetIntegerFromJsonConfig(LOGGING_LEVEL, jsonString, DEFAULT_LOGGING_LEVEL, MIN_LOGGING_LEVEL, MAX_LOGGING_LEVEL, log);
 }
@@ -353,7 +353,6 @@ int SetLoggingLevelPersistently(LoggingLevel level, OsConfigLogHandle log)
     const char* loggingLevelTemplateWithComma = "  \"LoggingLevel\": %d,\n";
     const char* configurationTemplate = "{\n  \"LoggingLevel\": %d\n}\n";
 
-    LoggingLevel existingLevel = LoggingLevelInformational;
     char* jsonConfiguration = NULL;
     char* buffer = NULL;
     int result = 0;
@@ -369,7 +368,7 @@ int SetLoggingLevelPersistently(LoggingLevel level, OsConfigLogHandle log)
         {
             if (NULL != (jsonConfiguration = LoadStringFromFile(configurationFile, false, log)))
             {
-                if (level != (existingLevel = GetLoggingLevelFromJsonConfig(jsonConfiguration, log)))
+                if (level != GetLoggingLevelFromJsonConfig(jsonConfiguration, log))
                 {
                     if (NULL == (buffer = FormatAllocateString(strstr(jsonConfiguration, ",") ? loggingLevelTemplateWithComma : loggingLevelTemplate, level)))
                     {
diff --git a/src/common/commonutils/DaemonUtils.c b/src/common/commonutils/DaemonUtils.c
index 6684c641df..14544f5408 100644
--- a/src/common/commonutils/DaemonUtils.c
+++ b/src/common/commonutils/DaemonUtils.c
@@ -88,7 +88,7 @@ bool CheckDaemonNotActive(const char* daemonName, char** reason, OsConfigLogHand
 {
     bool result = false;
 
-    if (true == (result = IsDaemonActive(daemonName, log)))
+    if (true == IsDaemonActive(daemonName, log))
     {
         OsConfigLogInfo(log, "CheckDaemonNotActive: service '%s' is active", daemonName);
         OsConfigCaptureReason(reason, "Service '%s' is active", daemonName);
diff --git a/src/common/commonutils/DeviceInfoUtils.c b/src/common/commonutils/DeviceInfoUtils.c
index da0eeb87f2..6b3c5bf636 100644
--- a/src/common/commonutils/DeviceInfoUtils.c
+++ b/src/common/commonutils/DeviceInfoUtils.c
@@ -423,7 +423,6 @@ static char* GetOsReleaseEntry(const char* commandTemplate, const char* name, ch
     char* command = NULL;
     char* result = NULL;
     size_t commandLength = 0;
-    int status = 0;
 
     if ((NULL == commandTemplate) || (NULL == name) || (0 == strlen(name)))
     {
@@ -443,7 +442,7 @@ static char* GetOsReleaseEntry(const char* commandTemplate, const char* name, ch
             memset(command, 0, commandLength);
             snprintf(command, commandLength, commandTemplate, name);
 
-            if ((0 == (status = ExecuteCommand(NULL, command, true, false, 0, 0, &result, NULL, log))) && result)
+            if ((0 == ExecuteCommand(NULL, command, true, false, 0, 0, &result, NULL, log)) && result)
             {
                 RemovePrefixBlanks(result);
                 RemoveTrailingBlanks(result);
diff --git a/src/common/commonutils/FileUtils.c b/src/common/commonutils/FileUtils.c
index 7896eabc17..6e0d36ee63 100644
--- a/src/common/commonutils/FileUtils.c
+++ b/src/common/commonutils/FileUtils.c
@@ -657,7 +657,7 @@ static int SetAccess(bool directory, const char* name, unsigned int desiredOwner
 
     if (directory ? DirectoryExists(name) : FileExists(name))
     {
-        if (0 == (result = CheckAccess(directory, name, desiredOwnerId, desiredGroupId, desiredAccess, false, NULL, log)))
+        if (0 == CheckAccess(directory, name, desiredOwnerId, desiredGroupId, desiredAccess, false, NULL, log))
         {
             OsConfigLogInfo(log, "SetAccess: desired '%s' ownership (owner %u, group %u with access %03o) already set",
                 name, desiredOwnerId, desiredGroupId, desiredAccess);
diff --git a/src/common/commonutils/OtherUtils.c b/src/common/commonutils/OtherUtils.c
index 414c679d96..e3c55e24c6 100644
--- a/src/common/commonutils/OtherUtils.c
+++ b/src/common/commonutils/OtherUtils.c
@@ -278,7 +278,7 @@ int CheckAllWirelessInterfacesAreDisabled(char** reason, OsConfigLogHandle log)
     const char* command = "iwconfig 2>&1 | egrep -v 'no wireless extensions|not found' | grep Frequency";
     int status = 0;
 
-    if (0 == (status = ExecuteCommand(NULL, command, true, false, 0, 0, NULL, NULL, log)))
+    if (0 == ExecuteCommand(NULL, command, true, false, 0, 0, NULL, NULL, log))
     {
         OsConfigLogInfo(log, "CheckAllWirelessInterfacesAreDisabled: wireless interfaces are enabled");
         OsConfigCaptureReason(reason, "At least one active wireless interface is present");
diff --git a/src/common/commonutils/PackageUtils.c b/src/common/commonutils/PackageUtils.c
index 74dfa20c2c..5c077f78ee 100644
--- a/src/common/commonutils/PackageUtils.c
+++ b/src/common/commonutils/PackageUtils.c
@@ -471,7 +471,7 @@ int InstallPackage(const char* packageName, OsConfigLogHandle log)
 {
     int status = ENOENT;
 
-    if (0 != (status = IsPackageInstalled(packageName, log)))
+    if (0 != IsPackageInstalled(packageName, log))
     {
         if (0 == (status = InstallOrUpdatePackage(packageName, log)))
         {
diff --git a/src/common/commonutils/PassUtils.c b/src/common/commonutils/PassUtils.c
index af819342ec..479c075ab8 100644
--- a/src/common/commonutils/PassUtils.c
+++ b/src/common/commonutils/PassUtils.c
@@ -19,7 +19,7 @@ static char* FindPamModule(const char* pamModule, OsConfigLogHandle log)
         "/lib/x86_64-linux-gnu/security/%s" };
     int numPaths = ARRAY_SIZE(paths);
     char* result = NULL;
-    int status = 0, i = 0;
+    int i = 0;
 
     if (NULL == pamModule)
     {
@@ -31,7 +31,7 @@ static char* FindPamModule(const char* pamModule, OsConfigLogHandle log)
     {
         if (NULL != (result = FormatAllocateString(paths[i], pamModule)))
         {
-            if (0 == (status = CheckFileExists(result, NULL, log)))
+            if (0 == CheckFileExists(result, NULL, log))
             {
                 break;
             }
diff --git a/src/common/commonutils/SshUtils.c b/src/common/commonutils/SshUtils.c
index 2eb92447d1..9cf51b50b5 100644
--- a/src/common/commonutils/SshUtils.c
+++ b/src/common/commonutils/SshUtils.c
@@ -210,7 +210,6 @@ static int IsSshConfigIncludeSupported(OsConfigLogHandle log)
     const int minVersionMajor = 8;
     const int minVersionMinor = 2;
     char* textResult = NULL;
-    size_t textResultLength = 0;
     size_t textPrefixLength = 0;
     char* textCursor = NULL;
     char versionMajorString[2] = {0};
@@ -232,7 +231,8 @@ static int IsSshConfigIncludeSupported(OsConfigLogHandle log)
 
     if (NULL != textResult)
     {
-        if (((textPrefixLength = strlen(expectedPrefix)) + 3) < (textResultLength = strlen(textResult)))
+        textPrefixLength = strlen(expectedPrefix) + 3;
+        if (textPrefixLength < strlen(textResult))
         {
             textCursor = textResult + strlen(expectedPrefix) + 1;
             if (isdigit(textCursor[0]) && ('.' == textCursor[1]) && isdigit(textCursor[2]))
@@ -669,9 +669,9 @@ int CheckSshProtocol(char** reason, OsConfigLogHandle log)
     {
         OsConfigLogInfo(log, "CheckSshProtocol: the SSH Server configuration file '%s' is not present on this device", g_sshServerConfiguration);
         OsConfigCaptureReason(reason, "'%s' is not present on this device", g_sshServerConfiguration);
-        status = EEXIST;
+        status = ENOENT;
     }
-    if (0 == (status = CheckLineFoundNotCommentedOut(g_sshServerConfiguration, '#', protocol, reason, log)))
+    else if (0 == CheckLineFoundNotCommentedOut(g_sshServerConfiguration, '#', protocol, reason, log))
     {
         OsConfigLogInfo(log, "CheckSshProtocol: '%s' is found uncommented in %s", protocol, g_sshServerConfiguration);
         status = 0;
@@ -700,7 +700,7 @@ int CheckSshProtocol(char** reason, OsConfigLogHandle log)
                 OsConfigCaptureReason(reason, "'%s' is not found included in %s", g_osconfigRemediationConf, g_sshServerConfiguration);
                 status = ENOENT;
             }
-            else if (0 == (status = CheckLineFoundNotCommentedOut(g_osconfigRemediationConf, '#', protocol, reason, log)))
+            else if (0 == CheckLineFoundNotCommentedOut(g_osconfigRemediationConf, '#', protocol, reason, log))
             {
                 OsConfigLogInfo(log, "CheckSshProtocol: '%s' is found uncommented in %s", protocol, g_osconfigRemediationConf);
                 status = 0;
@@ -918,6 +918,7 @@ static int IncludeRemediationSshConfFile(OsConfigLogHandle log)
     else if ((NULL == (inclusion = FormatInclusionForRemediation(log))) || (0 == (inclusionSize = strlen(inclusion))))
     {
         OsConfigLogInfo(log, "IncludeRemediationSshConfFile: failed preparing the inclusion statement, cannot include '%s' into '%s'", g_osconfigRemediationConf, g_sshServerConfiguration);
+        FREE_MEMORY(inclusion);
         return ENOMEM;
     }
 
diff --git a/src/common/commonutils/UserUtils.c b/src/common/commonutils/UserUtils.c
index b1898f7ade..0e1a7248f8 100755
--- a/src/common/commonutils/UserUtils.c
+++ b/src/common/commonutils/UserUtils.c
@@ -3050,7 +3050,7 @@ int RemoveUserAccounts(const char* names, OsConfigLogHandle log)
         OsConfigLogError(log, "RemoveUserAccounts: cannot read from '%s'", g_passwdFile);
         return EPERM;
     }
-    else if (0 == (status = CheckUserAccountsNotFound(names, NULL, log)))
+    else if (0 == CheckUserAccountsNotFound(names, NULL, log))
     {
         OsConfigLogInfo(log, "RemoveUserAccounts: the requested user accounts '%s' appear already removed", names);
         return 0;
diff --git a/src/common/logging/Logging.c b/src/common/logging/Logging.c
index d018189358..565f5ac533 100644
--- a/src/common/logging/Logging.c
+++ b/src/common/logging/Logging.c
@@ -53,6 +53,7 @@ void SetConsoleLoggingEnabled(bool enabledOrDisabled)
 const char* GetLoggingLevelName(LoggingLevel level)
 {
     const char* result = g_debug;
+    (void)result;
 
     switch (level)
     {
@@ -196,8 +197,9 @@ char* GetFormattedTime(void)
 {
     time_t rawTime = {0};
     struct tm* timeInfo = NULL;
+    struct tm result = {0};
     time(&rawTime);
-    timeInfo = localtime(&rawTime);
+    timeInfo = localtime_r(&rawTime, &result);
     strftime(g_logTime, ARRAY_SIZE(g_logTime), "%Y-%m-%d %H:%M:%S", timeInfo);
     return g_logTime;
 }
@@ -241,7 +243,11 @@ void TrimLog(OsConfigLogHandle log)
 
             // Reapply restrictions once the file is recreated (also for backup, if any):
             RestrictFileAccessToCurrentAccountOnly(log->logFileName);
-            RestrictFileAccessToCurrentAccountOnly(log->backLogFileName);
+            if (NULL != log->backLogFileName)
+            {
+                // Reapply restrictions to the backup file:
+                RestrictFileAccessToCurrentAccountOnly(log->backLogFileName);
+            }
         }
     }
 
diff --git a/src/common/mpiclient/MpiClient.c b/src/common/mpiclient/MpiClient.c
index c70ecdb265..034f1d45a2 100644
--- a/src/common/mpiclient/MpiClient.c
+++ b/src/common/mpiclient/MpiClient.c
@@ -227,10 +227,14 @@ MPI_HANDLE CallMpiOpen(const char* clientName, const unsigned int maxPayloadSize
     FREE_MEMORY(request);
 
     mpiHandle = (MPI_OK == status) ? (MPI_HANDLE)response : NULL;
-
-    if ((NULL != mpiHandle) && (NULL == (mpiHandleValue = ParseString(log, (char*)mpiHandle))))
+    if (NULL == mpiHandle)
+    {
+        FREE_MEMORY(response);
+    }
+    else if (NULL == (mpiHandleValue = ParseString(log, (char*)mpiHandle)))
     {
         OsConfigLogError(log, "CallMpiOpen: invalid MPI handle '%s'", (char*)mpiHandle);
+        FREE_MEMORY(response);
         mpiHandle = NULL;
     }
 
@@ -242,6 +246,8 @@ MPI_HANDLE CallMpiOpen(const char* clientName, const unsigned int maxPayloadSize
     return mpiHandle;
 }
 
+// We're responsible for free-ing the handle even on failure.
+// The handle comes from the response allocated in CallMpiOpen.
 void CallMpiClose(MPI_HANDLE clientSession, OsConfigLogHandle log)
 {
     const char *name = "MpiClose";
@@ -255,6 +261,7 @@ void CallMpiClose(MPI_HANDLE clientSession, OsConfigLogHandle log)
     if ((NULL == clientSession) || (0 == strlen((char*)clientSession)))
     {
         OsConfigLogError(log, "CallMpiClose(%p) called with invalid argument", clientSession);
+        FREE_MEMORY(clientSession);
         return;
     }
 
@@ -264,6 +271,7 @@ void CallMpiClose(MPI_HANDLE clientSession, OsConfigLogHandle log)
     if (NULL == request)
     {
         OsConfigLogError(log, "CallMpiClose(%p): failed to allocate memory for request", clientSession);
+        FREE_MEMORY(clientSession);
         return;
     }
 
@@ -275,6 +283,7 @@ void CallMpiClose(MPI_HANDLE clientSession, OsConfigLogHandle log)
     FREE_MEMORY(response);
 
     OsConfigLogInfo(log, "CallMpiClose(%p)", clientSession);
+    FREE_MEMORY(clientSession);
 }
 
 int CallMpiSet(const char* componentName, const char* propertyName, const MPI_JSON_STRING payload, const int payloadSizeBytes, OsConfigLogHandle log)
diff --git a/src/common/tests/CommonUtilsUT.cpp b/src/common/tests/CommonUtilsUT.cpp
index 4718fe2463..0244889a7c 100644
--- a/src/common/tests/CommonUtilsUT.cpp
+++ b/src/common/tests/CommonUtilsUT.cpp
@@ -1161,7 +1161,7 @@ TEST_F(CommonUtilsTest, FormatAllocateString)
     FREE_MEMORY(formatted);
     EXPECT_STREQ("Test ABC 123", formatted = FormatAllocateString("Test %s %d", "ABC", 123));
     FREE_MEMORY(formatted);
-    ASSERT_NE(nullptr, longString = (char*)malloc(longStringLength + 1));
+    EXPECT_NE(nullptr, longString = (char*)malloc(longStringLength + 1));
     memset(longString, 'a', longStringLength);
     longString[4095] = 0;
     EXPECT_STREQ(longString, formatted = FormatAllocateString("%s", longString));
diff --git a/src/modules/compliance/src/lib/CMakeLists.txt b/src/modules/compliance/src/lib/CMakeLists.txt
index bd48611c1e..581fce1078 100644
--- a/src/modules/compliance/src/lib/CMakeLists.txt
+++ b/src/modules/compliance/src/lib/CMakeLists.txt
@@ -40,6 +40,9 @@ add_library(compliancelib STATIC
 
 set_property(TARGET compliancelib PROPERTY POSITION_INDEPENDENT_CODE ON)
 
+add_custom_target(ProcedureMap
+    DEPENDS ProcedureMap.cpp ProcedureMap.h
+)
 add_custom_command(
     OUTPUT ProcedureMap.cpp ProcedureMap.h
     COMMAND ${CMAKE_CURRENT_SOURCE_DIR}/GenProcedureMap.sh ${CMAKE_CURRENT_SOURCE_DIR}/procedures/
diff --git a/src/modules/compliance/tests/OptionalTest.cpp b/src/modules/compliance/tests/OptionalTest.cpp
index 5994f3328e..2c79041367 100644
--- a/src/modules/compliance/tests/OptionalTest.cpp
+++ b/src/modules/compliance/tests/OptionalTest.cpp
@@ -40,7 +40,9 @@ TEST_F(OptionalTest, MoveContructor)
 {
     Optional<int> opt1(42);
     Optional<int> opt2(std::move(opt1));
+#ifndef __clang_analyzer__
     ASSERT_FALSE(opt1.HasValue());
+#endif
     ASSERT_TRUE(opt2.HasValue());
     ASSERT_EQ(opt2.Value(), 42);
 }
@@ -60,7 +62,9 @@ TEST_F(OptionalTest, MoveAssignment)
     Optional<int> opt1(42);
     Optional<int> opt2;
     opt2 = std::move(opt1);
+#ifndef __clang_analyzer__
     ASSERT_FALSE(opt1.HasValue());
+#endif
     ASSERT_TRUE(opt2.HasValue());
     ASSERT_EQ(opt2.Value(), 42);
 }
diff --git a/src/modules/compliance/tests/ResultTest.cpp b/src/modules/compliance/tests/ResultTest.cpp
index 0318db8605..04baa60f66 100644
--- a/src/modules/compliance/tests/ResultTest.cpp
+++ b/src/modules/compliance/tests/ResultTest.cpp
@@ -42,7 +42,9 @@ TEST_F(ResultTest, MoveContructor)
 {
     Result<int> result1(42);
     Result<int> result2(std::move(result1));
+#ifndef __clang_analyzer__
     ASSERT_FALSE(result1.HasValue());
+#endif
     ASSERT_TRUE(result2.HasValue());
     ASSERT_EQ(result2.Value(), 42);
 }
@@ -60,7 +62,9 @@ TEST_F(ResultTest, MoveAssignment)
 {
     Result<int> result1(42);
     Result<int> result2 = std::move(result1);
+#ifndef __clang_analyzer__
     ASSERT_FALSE(result1.HasValue());
+#endif
     ASSERT_TRUE(result2.HasValue());
     ASSERT_EQ(result2.Value(), 42);
 }
diff --git a/src/modules/compliance/tests/procedures/EnsureFilePermissionsTest.cpp b/src/modules/compliance/tests/procedures/EnsureFilePermissionsTest.cpp
index d49eba6c64..eb77564cce 100644
--- a/src/modules/compliance/tests/procedures/EnsureFilePermissionsTest.cpp
+++ b/src/modules/compliance/tests/procedures/EnsureFilePermissionsTest.cpp
@@ -51,15 +51,19 @@ class EnsureFilePermissionsTest : public ::testing::Test
     void CreateFile(std::string& filename, int owner, int group, short permissions)
     {
         char* newFileName = strdup(fileTemplate);
-        ASSERT_NE(newFileName, nullptr);
+        EXPECT_NE(newFileName, nullptr);
         int f = mkstemp(newFileName);
-        ASSERT_GE(f, 0);
+        if (f < 0)
+        {
+            free(newFileName);
+            GTEST_FAIL() << "Failed to create temporary file";
+        }
         close(f);
-        ASSERT_EQ(chown(newFileName, owner, group), 0);
-        ASSERT_EQ(chmod(newFileName, permissions), 0);
         filename = newFileName;
         files.push_back(filename);
         free(newFileName);
+        ASSERT_EQ(chown(filename.c_str(), owner, group), 0);
+        ASSERT_EQ(chmod(filename.c_str(), permissions), 0);
     }
 };
 
diff --git a/src/modules/test/Module.c b/src/modules/test/Module.c
index 53b5ab1643..7c00765ae4 100644
--- a/src/modules/test/Module.c
+++ b/src/modules/test/Module.c
@@ -211,17 +211,16 @@ int ParseModuleInfo(const JSON_Value* value, MODULE_INFO** moduleInfo)
                     }
                 }
             }
+        }
 
-            if (0 == status)
-            {
-                *moduleInfo = info;
-            }
-            else
-            {
-                FreeModuleInfo(info);
-                info = NULL;
-            }
-
+        if (0 == status)
+        {
+            *moduleInfo = info;
+        }
+        else
+        {
+            FreeModuleInfo(info);
+            info = NULL;
         }
     }
 
@@ -339,7 +338,7 @@ MANAGEMENT_MODULE* LoadModule(const char* client, const char* path)
     if ((status != 0) && (module != NULL))
     {
         UnloadModule(module);
-        module = NULL;
+        FREE_MEMORY(module);
     }
     json_value_free(value);
     FREE_MEMORY(payload);
@@ -355,7 +354,8 @@ void UnloadModule(MANAGEMENT_MODULE* module)
 
     if (NULL != module->handle)
     {
-        module->close(module->session);
+        if ((NULL != module->close) && (module->session != NULL))
+            module->close(module->session);
         module->session = NULL;
 
         module->getInfo = NULL;
diff --git a/src/platform/Main.c b/src/platform/Main.c
index 395dac21d5..5bff3e2eba 100644
--- a/src/platform/Main.c
+++ b/src/platform/Main.c
@@ -59,8 +59,6 @@ static void SignalInterrupt(int signal)
     size_t sizeOfMpiMessage = 0;
     ssize_t writeResult = -1;
 
-    UNUSED(writeResult);
-
     if (SIGSEGV == signal)
     {
         errorMessage = ERROR_MESSAGE_SIGSEGV;
@@ -91,7 +89,7 @@ static void SignalInterrupt(int signal)
     {
         if (0 < (logDescriptor = open(LOG_FILE, O_APPEND | O_WRONLY | O_NONBLOCK)))
         {
-            if (0 < (writeResult = write(logDescriptor, (const void*)errorMessage, strlen(errorMessage))))
+            if (0 < write(logDescriptor, (const void*)errorMessage, strlen(errorMessage)))
             {
                 sizeOfMpiMessage = strlen(g_mpiCall);
                 if (sizeOfMpiMessage > 0)
@@ -102,6 +100,7 @@ static void SignalInterrupt(int signal)
                 {
                     writeResult = write(logDescriptor, (const void*)EOL_TERMINATOR, sizeof(char));
                 }
+                UNUSED(writeResult);
             }
             close(logDescriptor);
         }
diff --git a/src/platform/MmiClient.c b/src/platform/MmiClient.c
index ec308ff8c0..1ce324b457 100644
--- a/src/platform/MmiClient.c
+++ b/src/platform/MmiClient.c
@@ -204,15 +204,14 @@ static int ParseModuleInfo(const JSON_Value* value, MODULE_INFO** moduleInfo)
                     }
                 }
             }
-
-            if (0 == status)
-            {
-                *moduleInfo = info;
-            }
-            else
-            {
-                FreeModuleInfo(info);
-            }
+        }
+        if (0 == status)
+        {
+            *moduleInfo = info;
+        }
+        else
+        {
+            FreeModuleInfo(info);
         }
     }
 
@@ -318,6 +317,14 @@ MODULE* LoadModule(const char* client, const char* path)
         }
     }
 
+    if (value)
+    {
+        json_value_free(value);
+    }
+    if (payload)
+    {
+        module->free(payload);
+    }
     if (0 != status)
     {
         UnloadModule(module);
diff --git a/src/platform/ModulesManager.c b/src/platform/ModulesManager.c
index 0bc6646584..2173f5809f 100644
--- a/src/platform/ModulesManager.c
+++ b/src/platform/ModulesManager.c
@@ -140,6 +140,7 @@ static void LoadModules(const char* directory, const char* configJson)
 
                 FREE_MEMORY(path);
             }
+            closedir(dir);
         }
         else
         {
@@ -221,6 +222,7 @@ static void LoadModules(const char* directory, const char* configJson)
     {
         json_value_free(config);
     }
+    FREE_MEMORY(clientName);
 }
 
 void AreModulesLoadedAndLoadIfNot(const char* directory, const char* configJson)
@@ -260,6 +262,7 @@ static void FreeSessions(SESSION* sessions)
         {
             moduleSession = curr->modules;
             curr->modules = moduleSession->next;
+            moduleSession->module->close(moduleSession->handle);
             FREE_MEMORY(moduleSession);
         }
 
@@ -288,8 +291,8 @@ static void FreeReportedObjects(REPORTED_OBJECT* reportedObjects, int numReporte
 
 void UnloadModules(void)
 {
-    FreeModules(g_modules);
     FreeSessions(g_sessions);
+    FreeModules(g_modules);
     FreeReportedObjects(g_reported, g_reportedTotal);
 
     g_reportedTotal = 0;
@@ -448,6 +451,13 @@ void MpiClose(MPI_HANDLE handle)
     else
     {
         OsConfigLogDebug(GetPlatformLog(), "MpiClose: closing session with UUID '%s'", session->uuid);
+        while (session->modules)
+        {
+            MODULE_SESSION* moduleSession = session->modules;
+            session->modules = moduleSession->next;
+            moduleSession->module->close(moduleSession->handle);
+            FREE_MEMORY(moduleSession);
+        }
 
         // Remove the session from the linked list
         if (session == g_sessions)
@@ -770,7 +780,8 @@ int MpiGetReported(MPI_HANDLE handle, MPI_JSON_STRING* payload, int* payloadSize
                 }
                 else
                 {
-                    memcpy(payloadJson, mmiPayload, mmiPayloadSizeBytes + 1);
+                    memcpy(payloadJson, mmiPayload, mmiPayloadSizeBytes);
+                    payloadJson[mmiPayloadSizeBytes] = '\0';
 
                     if (NULL == (objectValue = json_parse_string(payloadJson)))
                     {
@@ -804,7 +815,8 @@ int MpiGetReported(MPI_HANDLE handle, MPI_JSON_STRING* payload, int* payloadSize
                     FREE_MEMORY(payloadJson);
                 }
 
-                FREE_MEMORY(mmiPayload);
+                moduleSession->module->free(mmiPayload);
+                mmiPayload = NULL;
             }
         }
 
diff --git a/src/platform/MpiServer.c b/src/platform/MpiServer.c
index 615725697c..ef0fca0a1a 100644
--- a/src/platform/MpiServer.c
+++ b/src/platform/MpiServer.c
@@ -32,6 +32,7 @@ static struct sockaddr_un g_socketaddr = {0};
 static socklen_t g_socketlen = 0;
 
 static pthread_t g_mpiServerWorker = 0;
+static int g_mpiServerWorkerError = -1;
 static bool g_serverActive = false;
 
 char g_mpiCall[MPI_CALL_MESSAGE_LENGTH] = {0};
@@ -547,8 +548,6 @@ static void* MpiServerWorker(void* arguments)
 
             contentLength = 0;
             responseSize = 0;
-            actualSize = 0;
-            estimatedSize = 0;
 
             FREE_MEMORY(requestBody);
             FREE_MEMORY(responseBody);
@@ -596,7 +595,7 @@ void MpiInitialize(void)
                 OsConfigLogInfo(GetPlatformLog(), "Listening on socket '%s'", g_mpiSocket);
 
                 g_serverActive = true;
-                g_mpiServerWorker = pthread_create(&g_mpiServerWorker, NULL, MpiServerWorker, NULL);
+                g_mpiServerWorkerError = pthread_create(&g_mpiServerWorker, NULL, MpiServerWorker, NULL);
             }
             else
             {
@@ -618,9 +617,10 @@ void MpiShutdown(void)
 {
     g_serverActive = false;
 
-    if (g_mpiServerWorker > 0)
+    if (g_mpiServerWorkerError == 0)
     {
         pthread_join(g_mpiServerWorker, NULL);
+        g_mpiServerWorkerError = -1;
     }
 
     UnloadModules();


Reviews:
user: login: MariusNi
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-25T00:36:48Z
user: login: MariusNi
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-25T00:39:58Z
user: login: MariusNi
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-25T00:41:49Z
user: login: MariusNi
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-25T00:43:48Z
user: login: MariusNi
body: :shipit:
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-04-25T00:47:44Z
user: login: jepio
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-25T10:51:20Z
user: login: jepio
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-25T11:08:23Z
user: login: jepio
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-25T11:09:03Z
user: login: jepio
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-25T11:14:54Z
user: login: jepio
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-25T16:00:29Z
user: login: jepio
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-25T16:04:42Z
user: login: kkanas
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-04-28T13:40:30Z

Review comments:
diff_hunk: @@ -407,7 +406,7 @@ void CloseAgent(void)
     if (NULL != g_mpiHandle)
     {
         CallMpiClose(g_mpiHandle, GetLog());
-        g_mpiHandle = NULL;
+        FREE_MEMORY(g_mpiHandle);
path: src/adapters/pnp/PnpAgent.c
user: login: MariusNi
body: >FREE_MEMORY(g_mpiHandle); [](http://example.com/codeflow?start=8&length=25)

The right place to free the memory for this is in the Modules Manager's MpiClose implementation, which is invoked by this code over the MPI REST API (the MPI Client -> the MPI Server -> the Modules Manager).
created_at: 2025-04-25T00:36:48Z
author_association: CONTRIBUTOR
diff_hunk: @@ -232,7 +231,8 @@ static int IsSshConfigIncludeSupported(OsConfigLogHandle log)
 
     if (NULL != textResult)
     {
-        if (((textPrefixLength = strlen(expectedPrefix)) + 3) < (textResultLength = strlen(textResult)))
+        textPrefixLength = strlen(expectedPrefix) + 3;
+        if (textPrefixLength < strlen(textResult))
path: src/common/commonutils/SshUtils.c
user: login: MariusNi
body: Just curious, why is this necessary/why is gets flagged by the analysis? 
created_at: 2025-04-25T00:39:58Z
author_association: CONTRIBUTOR
diff_hunk: @@ -53,6 +53,7 @@ void SetConsoleLoggingEnabled(bool enabledOrDisabled)
 const char* GetLoggingLevelName(LoggingLevel level)
 {
     const char* result = g_debug;
+    (void)result;
path: src/common/logging/Logging.c
user: login: MariusNi
body: >(void)result; [](http://example.com/codeflow?start=4&length=13)

Line 90 ensures that result is always set after the initialization on line 55. Could you please summarize why is it necessary?
created_at: 2025-04-25T00:41:49Z
author_association: CONTRIBUTOR
diff_hunk: @@ -196,8 +197,9 @@ char* GetFormattedTime(void)
 {
     time_t rawTime = {0};
     struct tm* timeInfo = NULL;
+    struct tm result = {0};
     time(&rawTime);
-    timeInfo = localtime(&rawTime);
+    timeInfo = localtime_r(&rawTime, &result);
path: src/common/logging/Logging.c
user: login: MariusNi
body: >localtime_r( [](http://example.com/codeflow?start=15&length=12)

If I am not mistaken, localtime_r does not include tzset (for timezone information) which may need to be separately called (something that localtime does not need)
created_at: 2025-04-25T00:43:48Z
author_association: CONTRIBUTOR
diff_hunk: @@ -232,7 +231,8 @@ static int IsSshConfigIncludeSupported(OsConfigLogHandle log)
 
     if (NULL != textResult)
     {
-        if (((textPrefixLength = strlen(expectedPrefix)) + 3) < (textResultLength = strlen(textResult)))
+        textPrefixLength = strlen(expectedPrefix) + 3;
+        if (textPrefixLength < strlen(textResult))
path: src/common/commonutils/SshUtils.c
user: login: jepio
body: The assignments to `textResultLength` and `textPrefixLength` are both not used outside the `if` condition, and that gets flagged as a dead store (a store that is never read).

I opted to remove the `textResultLength` variables as `strlen(textResult)` seems equally readable.
For `textPrefixLength` I found assigning `strlen(expectedPrefix) + 3` to the variable does add some clarity.

created_at: 2025-04-25T10:51:20Z
author_association: CONTRIBUTOR
diff_hunk: @@ -53,6 +53,7 @@ void SetConsoleLoggingEnabled(bool enabledOrDisabled)
 const char* GetLoggingLevelName(LoggingLevel level)
 {
     const char* result = g_debug;
+    (void)result;
path: src/common/logging/Logging.c
user: login: jepio
body: This gets flagged as dead store, since the value assigned to `result` in line 55 always gets overwritten by all branches (including `default`) in the `switch` statement. But removing the initialization here would be in conflict with our requirement to always initialize variables.

I would agree that this is a false-positive, that's why I added the `(void)result` (it suppresses this warning https://clang.llvm.org/docs/analyzer/user-docs/FAQ.html#dead-store).

Other alternatives didn't work well: removing the `default` label changes nothing as there are cases for all possible enum values, variable initializer still gets overwritten.
created_at: 2025-04-25T11:08:22Z
author_association: CONTRIBUTOR
diff_hunk: @@ -53,6 +53,7 @@ void SetConsoleLoggingEnabled(bool enabledOrDisabled)
 const char* GetLoggingLevelName(LoggingLevel level)
 {
     const char* result = g_debug;
+    (void)result;
path: src/common/logging/Logging.c
user: login: jepio
body: The only potential alternative would to get rid of the local variable and replace `result = ...` with `return g_...` everywhere. Do you prefer this approach?
created_at: 2025-04-25T11:09:03Z
author_association: CONTRIBUTOR
diff_hunk: @@ -407,7 +406,7 @@ void CloseAgent(void)
     if (NULL != g_mpiHandle)
     {
         CallMpiClose(g_mpiHandle, GetLog());
-        g_mpiHandle = NULL;
+        FREE_MEMORY(g_mpiHandle);
path: src/adapters/pnp/PnpAgent.c
user: login: jepio
body: Thanks for the suggestion, I looked a bit deeper and I believe this handle needs to be freed in the MpiClient's `CallMpiClose`, I moved the free to happen there so that all callers can benefit.

`g_mpiHandle` contains memory allocated on the client side through `CallMpiOpen` (the handle is the response, see here: https://github.com/Azure/azure-osconfig/blob/7ff178a7a343dc317c49b84ffff2593ede080e85/src/common/mpiclient/MpiClient.c#L225-L243.

created_at: 2025-04-25T11:14:54Z
author_association: CONTRIBUTOR
diff_hunk: @@ -196,8 +197,9 @@ char* GetFormattedTime(void)
 {
     time_t rawTime = {0};
     struct tm* timeInfo = NULL;
+    struct tm result = {0};
     time(&rawTime);
-    timeInfo = localtime(&rawTime);
+    timeInfo = localtime_r(&rawTime, &result);
path: src/common/logging/Logging.c
user: login: jepio
body: That is correct, and part of what makes `localtime` non-thread safe/non-reentrant. We don't need the sideeffects of that internal `tzset` because the timezone metadata is captured in the returned `struct tm` (passed by the caller as the second argument to `localtime_r`).

We don't rely on the timezone information as we don't include `%z` or `%Z` in the call format string passed to strftime that consumes the return of `localtime_r`.
created_at: 2025-04-25T16:00:29Z
author_association: CONTRIBUTOR
diff_hunk: @@ -407,7 +406,7 @@ void CloseAgent(void)
     if (NULL != g_mpiHandle)
     {
         CallMpiClose(g_mpiHandle, GetLog());
-        g_mpiHandle = NULL;
+        FREE_MEMORY(g_mpiHandle);
path: src/adapters/pnp/PnpAgent.c
user: login: jepio
body: I pushed the fix here: https://github.com/Azure/azure-osconfig/pull/992/commits/6c2c541180c18ffa9b762f4b5faf3addd2b97127
created_at: 2025-04-25T16:04:42Z
author_association: CONTRIBUTOR

PR comments:
user: login: github-actions[bot]
created_at: 2025-04-24T14:38:03Z
body: ## Test Results
 44 files  ±0   44 suites  ±0   27m 42s ⏱️ -50s
  4 tests ±0    4 ✅ ±0   0 💤 ±0  0 ❌ ±0 
176 runs  ±0  154 ✅ ±0  22 💤 ±0  0 ❌ ±0 

Results for commit 1635bfd1. ± Comparison against base commit 8283de31.

[test-results]:data:application/gzip;base64,H4sIADx/D2gC/1WMyw6DIBBFf8Ww7sIRGGx/pgF0ElKVhseq6b+XWrG6u+fc5LwYuWmM7NYIcWlYzC79achBJ+eXwoDYFVPOtN5132O29iwe7llEuwvSbjqJMQQfNhPy8u2Bwg1qEKTYza/YdVUciisfg9bPs0sFGCCXhgZAskahhmsPRMr0miNHEGSk1tj2nL0/Cb9UeQUBAAA=

:recycle: This comment has been updated with latest results.
author_association: NONE
user: login: jepio
created_at: 2025-04-25T16:05:19Z
body: (i temporarily resolved some discussions while rebasing to avoid getting into a broken state again - github thought there was a merge conflict)
author_association: CONTRIBUTOR
user: login: jepio
created_at: 2025-04-25T16:14:28Z
body: Pushed again due to another conflict.
author_association: CONTRIBUTOR