Skip to content

Bug: SinglePrecisionFloat(0) crashes in Zigbee FloatABC.lua with negative mantissa #2982

@ldeora

Description

@ldeora

Summary

Creating or sending a Zigbee SinglePrecisionFloat with the numeric value 0 can crash an Edge driver with:

[string "st/zigbee/data_types/base_defs/FloatABC.lua"]:201: SinglePrecisionFloat mantissa must be non-negative

Zero is a valid IEEE/ZCL float value and should serialize as exponent field 0, mantissa field 0, and sign bit 0 for positive zero.

This appears to be a bug in the SmartThings Edge Zigbee float data type handling, likely in the path that converts a Lua number into the internal FloatABC representation.

Affected area

st/zigbee/data_types/base_defs/FloatABC.lua
st/zigbee/data_types/SinglePrecisionFloat.lua

SinglePrecisionFloat is Zigbee data type 0x39.

Reproduction

A custom Zigbee Edge driver sends or writes a SinglePrecisionFloat value based on a capability command.

The crash is reproducible when the value is exactly 0, for example:

local data_types = require "st.zigbee.data_types"

local value = data_types.SinglePrecisionFloat(0)

or when a driver command handler eventually writes/sends a Zigbee attribute using:

data_types.SinglePrecisionFloat(0)

Observed in a custom driver for a SiHAS People Counter V2 / CSM-300-ZB device using a custom capability command where value = 0.

Original community report:

https://community.smartthings.com/t/bug-in-floatabc-lua-singleprecisionfloat-mantissa-crash-when-value-is-0/309492

Actual behavior

The driver crashes immediately with:

SinglePrecisionFloat mantissa must be non-negative

The crash happens before the value can be sent to the device.

Expected behavior

SinglePrecisionFloat(0) should be accepted and serialized as positive zero.

For single precision, the resulting payload should be:

00 00 00 00

The driver should not crash when a valid float value of 0 is used.

Technical analysis

FloatABC.lua already treats zero as a valid special case when reading and presenting float values:

if self.exponent == -1 * self.exponent_modifier and self.mantissa == 0 then
  -- Zero mantissa and all 0s exponent represents +/- zero
  return 0
end

and similarly in pretty_print().

So the representation expected by the existing implementation is:

exponent = -self.exponent_modifier
mantissa = 0

For SinglePrecisionFloat, where the exponent field is 8 bits, exponent_modifier is 127, so positive zero should be represented internally as:

sign_bit = 0
exponent = -127
mantissa = 0

The likely missing piece is an explicit zero guard in the numeric construction path before normal math.frexp()-based mantissa/exponent normalization is applied.

For zero, math.frexp(0) does not return a normalized mantissa. If the existing conversion path assumes a normal non-zero float and subtracts the implicit leading bit, it can produce a negative mantissa, triggering the current validation error.

Suggested fix

Add explicit handling for numeric zero before the normal float decomposition logic.

Suggested implementation approach:

local function float_from_value(self, value)
  if type(value) ~= "number" then
    error(string.format("%s values must be numbers", self.NAME), 2)
  end

  -- Handle zero before math.frexp() normalization.
  -- IEEE/ZCL zero: exponent field all zeroes, mantissa field all zeroes.
  if value == 0 then
    return self(0, -self.exponent_modifier, 0)
  end

  local sign_bit = value < 0 and 1 or 0
  local abs_value = math.abs(value)

  local frexp_mantissa, frexp_exponent = math.frexp(abs_value)

  -- math.frexp:
  --   abs_value = frexp_mantissa * 2^frexp_exponent
  --
  -- FloatABC normal representation:
  --   value = (1 + mantissa) * 2^exponent
  --
  -- Therefore:
  --   exponent = frexp_exponent - 1
  --   mantissa = (frexp_mantissa * 2) - 1

  local exponent = frexp_exponent - 1
  local mantissa = (frexp_mantissa * 2) - 1

  return self(sign_bit, exponent, mantissa)
end

If the numeric constructor is currently implemented outside FloatABC.lua, the same zero guard should be added there instead:

if value == 0 then
  return FloatType(0, -FloatType.exponent_modifier, 0)
end

Additional hardening

check_mantissa_is_valid() currently rejects mantissas greater than 1, but it should also reject negative mantissas explicitly:

i_table.check_mantissa_is_valid = function(self, mantissa)
  if type(mantissa) ~= "number" then
    error(string.format("%s mantissa values must be numbers", self.NAME), 2)
  elseif mantissa < 0 then
    error(string.format("%s mantissa must be non-negative", self.NAME), 2)
  elseif mantissa > 1 then
    error(string.format("%s mantissa must be less than 1", self.NAME))
  end
end

This does not replace the zero fix, but it makes the validation behavior clearer and more complete.

Suggested tests

The following should not throw:

local data_types = require "st.zigbee.data_types"

data_types.SinglePrecisionFloat(0)
data_types.SinglePrecisionFloat(0.0)
data_types.SinglePrecisionFloat(1.0)
data_types.SinglePrecisionFloat(-1.0)
data_types.SinglePrecisionFloat(3.14159)

Positive zero should serialize to:

00 00 00 00

Existing deserialization and pretty-print behavior for zero should continue to work unchanged.

Impact

Any Edge driver path that constructs a Zigbee SinglePrecisionFloat from a Lua numeric value of exactly 0 may crash.

This can affect custom attributes, manufacturer-specific attributes, or reportable-change/write paths that use ZCL float type 0x39.

It should not be worked around by sending epsilon values such as 0.0000001, because that changes the semantic value. Zero is valid and should be handled by the SDK.

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions