Skip to content

Commit 069666f

Browse files
authored
fix: Guard against a undefined device on data receipt (#1580)
1 parent dec6584 commit 069666f

File tree

3 files changed

+114
-2
lines changed

3 files changed

+114
-2
lines changed

src/controller/controller.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -991,7 +991,7 @@ export class Controller extends events.EventEmitter<ControllerEventMap> {
991991
}
992992

993993
if (!device) {
994-
if (typeof payload.address === "number") {
994+
if (typeof payload.address === "number" && !Device.isDeletedByNetworkAddress(payload.address)) {
995995
device = await this.identifyUnknownDevice(payload.address);
996996
}
997997

src/controller/model/device.ts

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -323,6 +323,11 @@ export class Device extends Entity<ControllerEventMap> {
323323
}
324324

325325
public async onZclData(dataPayload: AdapterEvents.ZclPayload, frame: Zcl.Frame, endpoint: Endpoint): Promise<void> {
326+
if (!Device.devices.has(this.ieeeAddr)) {
327+
// prevent race conditions where device gets deleted during processing
328+
return;
329+
}
330+
326331
// Respond to enroll requests
327332
if (frame.header.isSpecific && frame.isCluster("ssIasZone") && frame.isCommand("enrollReq")) {
328333
logger.debug(`IAS - '${this.ieeeAddr}' responding to enroll response`, NS);
@@ -414,7 +419,7 @@ export class Device extends Entity<ControllerEventMap> {
414419
const commandHasResponse = frame.command.response !== undefined;
415420
const disableDefaultResponse = frame.header.frameControl.disableDefaultResponse;
416421
/* v8 ignore next */
417-
const disableTuyaDefaultResponse = endpoint.getDevice().manufacturerName?.startsWith("_TZ") && process.env.DISABLE_TUYA_DEFAULT_RESPONSE;
422+
const disableTuyaDefaultResponse = this.manufacturerName?.startsWith("_TZ") && process.env.DISABLE_TUYA_DEFAULT_RESPONSE;
418423
// Sometimes messages are received twice, prevent responding twice
419424
const alreadyResponded = this._lastDefaultResponseSequenceNumber === frame.header.transactionSequenceNumber;
420425

@@ -603,6 +608,22 @@ export class Device extends Entity<ControllerEventMap> {
603608
return devices;
604609
}
605610

611+
/** Check if a device is explicitly deleted */
612+
public static isDeletedByIeeeAddr(ieeeAddr: string): boolean {
613+
Device.loadFromDatabaseIfNecessary();
614+
615+
return Device.deletedDevices.has(ieeeAddr);
616+
}
617+
618+
/** Check if a device is explicitly deleted */
619+
public static isDeletedByNetworkAddress(networkAddress: number): boolean {
620+
Device.loadFromDatabaseIfNecessary();
621+
622+
const ieeeAddr = Device.nwkToIeeeCache.get(networkAddress);
623+
624+
return ieeeAddr ? Device.deletedDevices.has(ieeeAddr) : false;
625+
}
626+
606627
/**
607628
* @deprecated use allIterator()
608629
*/

test/controller.test.ts

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2615,6 +2615,51 @@ describe("Controller", () => {
26152615
expect(events.message.length).toBe(0);
26162616
});
26172617

2618+
it("Receive zclData with device deleted during processing (prevent race condition)", async () => {
2619+
const frame = Zcl.Frame.create(
2620+
Zcl.FrameType.GLOBAL,
2621+
Zcl.Direction.CLIENT_TO_SERVER,
2622+
true,
2623+
undefined,
2624+
40,
2625+
"read",
2626+
"genTime",
2627+
[{attrId: 0}, {attrId: 1}, {attrId: 2}, {attrId: 3}, {attrId: 4}, {attrId: 5}, {attrId: 6}, {attrId: 7}, {attrId: 8}, {attrId: 9}],
2628+
{},
2629+
);
2630+
await controller.start();
2631+
await mockAdapterEvents.deviceJoined({networkAddress: 129, ieeeAddr: "0x129"});
2632+
const device = controller.getDeviceByIeeeAddr("0x129")!;
2633+
const endpoint = device.getEndpoint(1)!;
2634+
const deviceOnZclDataSpy = vi.spyOn(device, "onZclData");
2635+
const endpointCommandSpy = vi.spyOn(endpoint, "command");
2636+
const endpointReadResponseSpy = vi.spyOn(endpoint, "readResponse");
2637+
const endpointReadSpy = vi.spyOn(endpoint, "read");
2638+
const endpointDefaultResponseSpy = vi.spyOn(endpoint, "defaultResponse");
2639+
2640+
deviceOnZclDataSpy.mockImplementationOnce(async (a, b, c) => {
2641+
await mockAdapterEvents.deviceLeave({networkAddress: 129, ieeeAddr: undefined});
2642+
await device.onZclData(a, b, c);
2643+
});
2644+
2645+
await mockAdapterEvents.zclPayload({
2646+
wasBroadcast: false,
2647+
address: "0x129",
2648+
clusterID: frame.cluster.ID,
2649+
data: frame.toBuffer(),
2650+
header: frame.header,
2651+
endpoint: 1,
2652+
linkquality: 50,
2653+
groupID: 1,
2654+
});
2655+
2656+
expect(deviceOnZclDataSpy).toHaveBeenCalledTimes(2); // once for wrapper mock, once for real
2657+
expect(endpointCommandSpy).toHaveBeenCalledTimes(0);
2658+
expect(endpointReadResponseSpy).toHaveBeenCalledTimes(0);
2659+
expect(endpointReadSpy).toHaveBeenCalledTimes(0);
2660+
expect(endpointDefaultResponseSpy).toHaveBeenCalledTimes(0);
2661+
});
2662+
26182663
it("Receive readResponse from unknown endpoint", async () => {
26192664
const buffer = Buffer.from([8, 1, 1, 1, 0, 0, 32, 3]);
26202665
const frame = Zcl.Frame.fromBuffer(Zcl.Utils.getCluster("genBasic", undefined, {}).ID, Zcl.Header.fromBuffer(buffer), buffer, {});
@@ -9252,6 +9297,52 @@ describe("Controller", () => {
92529297
expect(events.deviceNetworkAddressChanged.length).toBe(0);
92539298
});
92549299

9300+
it("does not try to identify unknown device when deleted", async () => {
9301+
const nwkAddr = 40369;
9302+
const database = `
9303+
{"id":1,"type":"Coordinator","ieeeAddr":"0x0000012300000000","nwkAddr":0,"manufId":0,"epList":[11,6,5,4,3,2,1],"endpoints":{"1":{"profId":260,"epId":1,"devId":5,"inClusterList":[],"meta":{},"outClusterList":[],"clusters":{}},"2":{"profId":257,"epId":2,"devId":5,"inClusterList":[],"meta":{},"outClusterList":[],"clusters":{}},"3":{"profId":261,"epId":3,"devId":5,"inClusterList":[],"meta":{},"outClusterList":[],"clusters":{}},"4":{"profId":263,"epId":4,"devId":5,"inClusterList":[],"meta":{},"outClusterList":[],"clusters":{}},"5":{"profId":264,"epId":5,"devId":5,"inClusterList":[],"meta":{},"outClusterList":[],"clusters":{}},"6":{"profId":265,"epId":6,"devId":5,"inClusterList":[],"meta":{},"outClusterList":[],"clusters":{}},"11":{"profId":260,"epId":11,"devId":1024,"inClusterList":[],"meta":{},"outClusterList":[1280],"clusters":{}}},"interviewCompleted":false,"meta":{},"_id":"aM341ldunExFmJ3u"}
9304+
{"id":4,"type":"EndDevice","ieeeAddr":"0x0017880104e45517","lastSeen":123,"nwkAddr":${nwkAddr},"manufId":4107,"manufName":"Philips","powerSource":"Battery","modelId":"RWL021","epList":[1,2],"endpoints":{"1":{"profId":49246,"epId":1,"devId":2096,"inClusterList":[0],"meta":{},"outClusterList":[0,3,4,6,8,5],"binds":[{"type":"endpoint","endpointID":1,"deviceIeeeAddr":"0x000b57fffec6a5b2"}],"configuredReportings":[{"cluster":1,"attrId":0,"minRepIntval":1,"maxRepIntval":20,"repChange":2}],"clusters":{"genBasic":{"dir":{"value":3},"attrs":{"modelId":"RWL021"}}}},"2":{"profId":260,"epId":2,"devId":12,"inClusterList":[0,1,3,15,64512],"meta":{},"outClusterList":[25],"clusters":{}}},"appVersion":2,"stackVersion":1,"hwVersion":1,"dateCode":"20160302","swBuildId":"5.45.1.17846","zclVersion":1,"interviewState":"SUCCESSFUL","meta":{"configured":1},"_id":"qxhymbX6H2GXDw8Z"}
9305+
`;
9306+
fs.writeFileSync(options.databasePath, database);
9307+
await controller.start();
9308+
events.lastSeenChanged = [];
9309+
events.deviceNetworkAddressChanged = [];
9310+
const identifyUnknownDeviceSpy = vi.spyOn(controller, "identifyUnknownDevice");
9311+
9312+
const device = controller.getDeviceByIeeeAddr("0x0017880104e45517")!;
9313+
expect(device.networkAddress).toStrictEqual(nwkAddr);
9314+
9315+
device.removeFromDatabase();
9316+
expect(device.isDeleted).toStrictEqual(true);
9317+
expect(Device.isDeletedByIeeeAddr("0x0017880104e45517")).toStrictEqual(true);
9318+
expect(Device.isDeletedByNetworkAddress(nwkAddr)).toStrictEqual(true);
9319+
9320+
const frame2 = Zcl.Frame.create(
9321+
0,
9322+
1,
9323+
true,
9324+
undefined,
9325+
10,
9326+
"readRsp",
9327+
0,
9328+
[{attrId: 5, status: 0, dataType: 66, attrData: "new.model.id2"}],
9329+
{},
9330+
);
9331+
await mockAdapterEvents.zclPayload({
9332+
wasBroadcast: false,
9333+
address: nwkAddr,
9334+
clusterID: frame2.cluster.ID,
9335+
data: frame2.toBuffer(),
9336+
header: frame2.header,
9337+
endpoint: 1,
9338+
linkquality: 50,
9339+
groupID: 1,
9340+
});
9341+
9342+
expect(identifyUnknownDeviceSpy).toHaveBeenCalledTimes(0);
9343+
expect(mockLogger.debug).toHaveBeenCalledWith(`Data is from unknown device with address '${nwkAddr}', skipping...`, "zh:controller");
9344+
});
9345+
92559346
it("reads/writes to group with custom cluster when common to all members", async () => {
92569347
interface CustomManuHerdsman {
92579348
attributes: {customAttr: number};

0 commit comments

Comments
 (0)