Skip to content

fix dangling spans when copying door lock user/credential info#72739

Open
kali834x wants to merge 1 commit into
project-chip:masterfrom
kali834x:doorlock-rebind-spans-on-copy
Open

fix dangling spans when copying door lock user/credential info#72739
kali834x wants to merge 1 commit into
project-chip:masterfrom
kali834x:doorlock-rebind-spans-on-copy

Conversation

@kali834x

Copy link
Copy Markdown
Contributor

Summary

In DOOR_LOCK_USE_LOCAL_BUFFER builds, EmberAfPluginDoorLockUserInfo and EmberAfPluginDoorLockCredentialInfo bind their userName/credentials and credentialData spans to their own internal nameBuffer/credentialsBuffer/credentialDataBuffer in the default constructor, but neither struct declares copy control. The compiler-generated copy then copies the span members verbatim, so a copy's spans keep pointing into the source object's buffers.

door-lock-server.cpp copies these structs by value: emberAfPluginDoorLockGetUser/emberAfPluginDoorLockGetCredential fill a local struct, and findUserIndexByCredential does userInfo = user before returning. Once the source goes out of scope the copy's spans dangle, so reading userName/credentials/credentialData afterwards is a use-after-free.

The fix gives both structs a copy constructor and copy-assignment operator, guarded by the same DOOR_LOCK_USE_LOCAL_BUFFER, that copy the buffer contents and re-point the spans at the destination's own buffers. This is the same dangling-span-on-copy problem that was fixed for the Push-AV TransportOptionsStorage in #72530. The default DOOR_LOCK_USE_LOCAL_BUFFER=0 build is unchanged, since the spans there reference application-owned storage.

Related issues

None.

Testing

Built a minimal reproducer mirroring the struct layout (fixed buffer plus a span bound to it in the default ctor) and the userInfo = user copy. Under AddressSanitizer the unpatched shape reports a use-after-scope read when the copied span is dereferenced after the source is destroyed (heap-use-after-free for heap-allocated infos); with the copy/assignment operators added it runs clean and the spans resolve to the copy's own buffers. The operators were also compiled in isolation with -Wall -Wextra under DOOR_LOCK_USE_LOCAL_BUFFER=1. The default DOOR_LOCK_USE_LOCAL_BUFFER=0 build is unaffected because the new code sits inside that #if.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request adds custom copy constructors and assignment operators to EmberAfPluginDoorLockCredentialInfo and EmberAfPluginDoorLockUserInfo to perform deep copies of internal buffers and re-bind spans, preventing dangling references. The review feedback highlights potential issues with self-assignment and passing null pointers to std::memcpy when the source spans are empty, which can lead to undefined behavior. It is recommended to add self-assignment checks and guard the std::memcpy calls.

Comment on lines +797 to +810
EmberAfPluginDoorLockCredentialInfo & operator=(const EmberAfPluginDoorLockCredentialInfo & other)
{
status = other.status;
credentialType = other.credentialType;
creationSource = other.creationSource;
createdBy = other.createdBy;
modificationSource = other.modificationSource;
lastModifiedBy = other.lastModifiedBy;

size_t dataLen = std::min(other.credentialData.size(), sizeof(credentialDataBuffer));
memcpy(credentialDataBuffer, other.credentialData.data(), dataLen);
credentialData = chip::MutableByteSpan(credentialDataBuffer, dataLen);
return *this;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

There are two potential issues here:

  1. Self-assignment: If an object is assigned to itself (e.g., info = info;), other.credentialData.data() will point to credentialDataBuffer. Passing overlapping memory regions to std::memcpy is undefined behavior.
  2. Null pointer in std::memcpy: If other.credentialData is empty, other.credentialData.data() can be nullptr. Calling std::memcpy with a null pointer as the source is undefined behavior, even if the size (dataLen) is 0.

Adding a self-assignment check and guarding the std::memcpy call with if (dataLen > 0) resolves both issues.

    EmberAfPluginDoorLockCredentialInfo & operator=(const EmberAfPluginDoorLockCredentialInfo & other)
    {
        if (this == &other)
        {
            return *this;
        }

        status             = other.status;
        credentialType     = other.credentialType;
        creationSource     = other.creationSource;
        createdBy          = other.createdBy;
        modificationSource = other.modificationSource;
        lastModifiedBy     = other.lastModifiedBy;

        size_t dataLen = std::min(other.credentialData.size(), sizeof(credentialDataBuffer));
        if (dataLen > 0)
        {
            memcpy(credentialDataBuffer, other.credentialData.data(), dataLen);
        }
        credentialData = chip::MutableByteSpan(credentialDataBuffer, dataLen);
        return *this;
    }

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

please integrate Gemini's guard; its good defensive hardening

Comment on lines +850 to +872
EmberAfPluginDoorLockUserInfo & operator=(const EmberAfPluginDoorLockUserInfo & other)
{
userUniqueId = other.userUniqueId;
userStatus = other.userStatus;
userType = other.userType;
credentialRule = other.credentialRule;
creationSource = other.creationSource;
createdBy = other.createdBy;
modificationSource = other.modificationSource;
lastModifiedBy = other.lastModifiedBy;

size_t nameLen = std::min(other.userName.size(), sizeof(nameBuffer));
memcpy(nameBuffer, other.userName.data(), nameLen);
userName = chip::MutableCharSpan(nameBuffer, nameLen);

size_t credentialCount = std::min(other.credentials.size(), sizeof(credentialsBuffer) / sizeof(credentialsBuffer[0]));
for (size_t i = 0; i < credentialCount; i++)
{
credentialsBuffer[i] = other.credentials[i];
}
credentials = chip::Span<CredentialStruct>(credentialsBuffer, credentialCount);
return *this;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

There are two potential issues here:

  1. Self-assignment: If an object is assigned to itself, other.userName.data() will point to nameBuffer. Passing overlapping memory regions to std::memcpy is undefined behavior.
  2. Null pointer in std::memcpy: If other.userName is empty, other.userName.data() can be nullptr. Calling std::memcpy with a null pointer as the source is undefined behavior, even if the size (nameLen) is 0.

Adding a self-assignment check and guarding the std::memcpy call with if (nameLen > 0) resolves both issues.

    EmberAfPluginDoorLockUserInfo & operator=(const EmberAfPluginDoorLockUserInfo & other)
    {
        if (this == &other)
        {
            return *this;
        }

        userUniqueId       = other.userUniqueId;
        userStatus         = other.userStatus;
        userType           = other.userType;
        credentialRule     = other.credentialRule;
        creationSource     = other.creationSource;
        createdBy          = other.createdBy;
        modificationSource = other.modificationSource;
        lastModifiedBy     = other.lastModifiedBy;

        size_t nameLen = std::min(other.userName.size(), sizeof(nameBuffer));
        if (nameLen > 0)
        {
            memcpy(nameBuffer, other.userName.data(), nameLen);
        }
        userName = chip::MutableCharSpan(nameBuffer, nameLen);

        size_t credentialCount = std::min(other.credentials.size(), sizeof(credentialsBuffer) / sizeof(credentialsBuffer[0]));
        for (size_t i = 0; i < credentialCount; i++)
        {
            credentialsBuffer[i] = other.credentials[i];
        }
        credentials = chip::Span<CredentialStruct>(credentialsBuffer, credentialCount);
        return *this;
    }

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

please integrate Gemini's guard; its good defensive hardening

@github-actions

github-actions Bot commented Jun 25, 2026

Copy link
Copy Markdown

PR #72739: Size comparison from e2077a4 to 5b0ac22

Full report (33 builds for bl602, bl702, bl702l, cc13x4_26x4, cc32xx, efr32, esp32, nrfconnect, psoc6, qpg, realtek, stm32, telink)
platform target config section e2077a4 5b0ac22 change % change
bl602 lighting-app bl602+mfd+littlefs+rpc FLASH 1099176 1099176 0 0.0
RAM 133418 133418 0 0.0
bl702 lighting-app bl702+eth FLASH 1085726 1085726 0 0.0
RAM 109029 109029 0 0.0
bl702l contact-sensor-app bl702l+mfd+littlefs FLASH 882218 882218 0 0.0
RAM 108596 108596 0 0.0
cc13x4_26x4 lighting-app LP_EM_CC1354P10_6 FLASH 777368 777368 0 0.0
RAM 103404 103404 0 0.0
lock-ftd LP_EM_CC1354P10_6 FLASH 790120 790120 0 0.0
RAM 108684 108684 0 0.0
pump-app LP_EM_CC1354P10_6 FLASH 739376 739376 0 0.0
RAM 97612 97612 0 0.0
pump-controller-app LP_EM_CC1354P10_6 FLASH 719548 719548 0 0.0
RAM 97644 97644 0 0.0
cc32xx air-purifier CC3235SF_LAUNCHXL FLASH 569654 569654 0 0.0
RAM 205112 205112 0 0.0
lock CC3235SF_LAUNCHXL FLASH 597214 597214 0 0.0
RAM 205272 205272 0 0.0
efr32 lighting-app BRD4187C FLASH 1094924 1094924 0 0.0
RAM 135256 135256 0 0.0
lock-app BRD4187C FLASH 995184 995248 64 0.0
RAM 131292 131292 0 0.0
BRD4338a FLASH 799809 800001 192 0.0
RAM 243432 243432 0 0.0
esp32 all-clusters-app c3devkit DRAM 99556 99556 0 0.0
FLASH 1626146 1626146 0 0.0
IRAM 94776 94776 0 0.0
nrfconnect all-clusters-app nrf52840dk_nrf52840 FLASH 844772 844772 0 0.0
RAM 157771 157771 0 0.0
psoc6 all-clusters cy8ckit_062s2_43012 FLASH 1750756 1750756 0 0.0
RAM 215492 215492 0 0.0
all-clusters-minimal cy8ckit_062s2_43012 FLASH 1626548 1626548 0 0.0
RAM 211604 211604 0 0.0
light cy8ckit_062s2_43012 FLASH 1470860 1470860 0 0.0
RAM 197436 197436 0 0.0
lock cy8ckit_062s2_43012 FLASH 1504308 1504308 0 0.0
RAM 225268 225268 0 0.0
qpg lighting-app qpg6200+debug FLASH 843156 843156 0 0.0
RAM 127908 127908 0 0.0
lock-app qpg6200+debug FLASH 782976 782976 0 0.0
RAM 118840 118840 0 0.0
realtek light-switch-app rtl8777g FLASH 689368 689368 0 0.0
RAM 101780 101780 0 0.0
lighting-app rtl8777g FLASH 730304 730304 0 0.0
RAM 102052 102052 0 0.0
stm32 light STM32WB5MM-DK FLASH 478976 478976 0 0.0
RAM 141492 141492 0 0.0
telink all-devices-app tl7218x FLASH 881716 881716 0 0.0
RAM 99716 99716 0 0.0
tlsr9118bdk40d FLASH 673322 673322 0 0.0
RAM 120848 120848 0 0.0
bridge-app tl7218x FLASH 734156 734156 0 0.0
RAM 97700 97700 0 0.0
light-app-ota-compress-lzma-factory-data tl3218x FLASH 800682 800682 0 0.0
RAM 42380 42380 0 0.0
light-app-ota-compress-lzma-shell-factory-data tl7218x FLASH 845822 845822 0 0.0
RAM 101492 101492 0 0.0
light-switch-app-ota-compress-lzma-factory-data tl7218x_retention FLASH 734714 734714 0 0.0
RAM 57824 57824 0 0.0
light-switch-app-ota-compress-lzma-shell-factory-data tlsr9528a FLASH 795802 795802 0 0.0
RAM 75176 75176 0 0.0
light-switch-app-ota-factory-data tl3218x_retention FLASH 734630 734630 0 0.0
RAM 34480 34480 0 0.0
lighting-app-ota-factory-data tlsr9118bdk40d FLASH 615214 615214 0 0.0
RAM 118508 118508 0 0.0
lighting-app-ota-rpc-factory-data-4mb tlsr9518adk80d FLASH 842038 842042 4 0.0
RAM 97376 97376 0 0.0

@codecov

codecov Bot commented Jun 25, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 56.79%. Comparing base (e2077a4) to head (5b0ac22).
⚠️ Report is 1 commits behind head on master.

Additional details and impacted files
@@           Coverage Diff           @@
##           master   #72739   +/-   ##
=======================================
  Coverage   56.79%   56.79%           
=======================================
  Files        1642     1642           
  Lines      112757   112757           
  Branches    13139    13139           
=======================================
  Hits        64040    64040           
  Misses      48717    48717           

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

#if DOOR_LOCK_USE_LOCAL_BUFFER
uint8_t credentialDataBuffer[DOOR_LOCK_CREDENTIAL_BUFFER_LENGTH];
EmberAfPluginDoorLockCredentialInfo() { credentialData = chip::MutableByteSpan(credentialDataBuffer); }

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can you please add a unit test for this change?

@Alami-Amine Alami-Amine Jun 26, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hmm, a unit test here wouldn't run in CI anyways since DOOR_LOCK_USE_LOCAL_BUFFER is never 1 in our CI

@Alami-Amine

Copy link
Copy Markdown
Contributor

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants