Crash if attribute read handler does nothing and is called after Attribute::set_value has been called

The following (very odd) pytango script crashes with a SIGSEGV during the read_attributes_5 call:

from tango.server import Device, run, attribute, command
from tango.test_context import DeviceTestContext

class MyDevice(Device):
    @command
    def SetValue(self) -> None:
        self.get_device_attr().get_attr_by_name("attr").set_value(1.0)

    @attribute(max_alarm=1.0)
    def attr(self) -> float:
        return None

with DeviceTestContext(MyDevice) as dp:
    dp.SetValue()
    print(dp.attr)

If we remove max_alarm=1.0 or the call to SetValue() then we get the expected API_AttrValueNotSet error.

If we return a value from attr() other than None, then we get the value returned without a crash.

Here is the relevant part of the stack trace:

#0  Tango::Attribute::general_check_alarm<double> (this=this@entry=0x7fffac031360, 
    alarm_type=alarm_type@entry=@0x7fffffffa6f4: Tango::ATTR_ALARM, 
    min_value=@0x7fffac031430: 7.7108650726560124e-43, max_value=@0x7fffac031438: 1)
    at /src/cppTango/src/server/attribute.cpp:2297
#1  0x00007fffe8c266f0 in Tango::Attribute::check_level_alarm (
    this=this@entry=0x7fffac031360) at /src/cppTango/src/server/attribute.cpp:2398
#2  0x00007fffe8c26a40 in Tango::Attribute::check_alarm (
    this=this@entry=0x7fffac031360) at /src/cppTango/src/server/attribute.cpp:2198
#3  0x00007fffe8cb5c1c in Tango::Device_3Impl::read_attributes_no_except (
    this=this@entry=0x7fffac02d020, names=..., aid=..., 
    second_try=second_try@entry=true, idx=std::vector of length 1, capacity 1 = {...})
    at /src/cppTango/src/server/device_3.cpp:1031
#4  0x00007fffe8cd3235 in Tango::Device_5Impl::read_attributes_5 (
    this=0x7fffac02d020, names=..., source=<optimized out>, cl_id=...)
    at /src/cppTango/src/server/device_5.cpp:342
#5  0x00007fffe8bd0c8a in _0RL_lcfn_6fe2f94a21a10053_84000000 (cd=0x7fffffffb040, 
    s)vnt=<optimized out>)
    at /src/cppTango/build/src/include/tango/idl/tangoSK.cpp:6638

It looks to me like read_attributes_5 is not detecting that the attribute value hasn't been set by the attribute read handler correctly and calling check_alarm when it shouldn't.

The following Catch2 test demonstrates the same behaviour:

template <class Base>
class AttrReadNoSet : public Base
{
  public:
    using Base::Base;

    void init_device() override { }

    void read_attr(Tango::Attribute &) override
    {
        /* Do nothing */
    }

    void set_value()
    {
        auto &att = Base::get_device_attr()->get_attr_by_name("attr_read_no_set");
        value = 1.0;
        att.set_value(&value);
    }

    static void attribute_factory(std::vector<Tango::Attr *> &attrs)
    {
        attrs.push_back(new TangoTest::AutoAttr<&AttrReadNoSet::read_attr>("attr_read_no_set", Tango::DEV_DOUBLE));
        Tango::UserDefaultAttrProp props;
        props.set_max_alarm("10.0");
        attrs.back()->set_default_properties(props);
    }

    static void command_factory(std::vector<Tango::Command *> &cmds)
    {
        cmds.push_back(new TangoTest::AutoCommand<&AttrReadNoSet::set_value>("SetValue"));
    }

  private:
    Tango::DevDouble value;
};

TANGO_TEST_AUTO_DEV_TMPL_INSTANTIATE(AttrReadNoSet, 6)

SCENARIO("Not setting a value")
{
    int idlver = GENERATE(TangoTest::idlversion(6));
    GIVEN("a device proxy to a simple IDLv" << idlver << " device")
    {
        TangoTest::Context ctx{"attr_read_no_set", "AttrReadNoSet", idlver};
        auto device = ctx.get_proxy();

        REQUIRE(idlver == device->get_idl_version());

        WHEN("we read a do nothing attribute")
        {
            std::string att{"attr_read_no_set"};

            Tango::DeviceAttribute da;
            REQUIRE_NOTHROW(da = device->read_attribute(att));

            THEN("we get a API_AttrValueNotSet exception")
            {
                using namespace TangoTest::Matchers;
                using namespace Catch::Matchers;

                double val_read{};
                REQUIRE_THROWS_MATCHES(
                    da >> val_read, Tango::DevFailed, ErrorListMatches(AnyMatch(Reason(Tango::API_AttrValueNotSet))));
            }
        }

        WHEN("we set an attribute value out-of-band")
        {
            REQUIRE_NOTHROW(device->command_inout("SetValue"));

            AND_WHEN("we read the do nothing attribute")
            {
                std::string att{"attr_read_no_set"};

                Tango::DeviceAttribute da;
                REQUIRE_NOTHROW(da = device->read_attribute(att));

                THEN("we get a API_AttrValueNotSet exception")
                {
                    using namespace TangoTest::Matchers;

                    double val_read{};
                    REQUIRE_THROWS_MATCHES(
                        da >> val_read, Tango::DevFailed, FirstErrorMatches(Reason(Tango::API_AttrValueNotSet)));
                }
            }
        }
    }
}
Edited by Thomas Ives