From 1866c02d512f63a8524a2d943cca2010e951daed Mon Sep 17 00:00:00 2001 From: Andrew Johnson Date: Mon, 5 Jan 2026 09:43:18 -0600 Subject: [PATCH 01/12] Update .ci-scripts (3 commits after v3.4.2) --- .ci | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.ci b/.ci index 20f8e05..fbbfced 160000 --- a/.ci +++ b/.ci @@ -1 +1 @@ -Subproject commit 20f8e053931fdef8a9413cc6229286c5d9ed152f +Subproject commit fbbfced900b5e87de9a176a68c40b8cafa3e8e01 From 818772fc2f405e2e1fa8791463bcc2063092f65d Mon Sep 17 00:00:00 2001 From: Andrew Johnson Date: Wed, 8 Apr 2026 21:46:06 -0500 Subject: [PATCH 02/12] Fixes for Appveyor CI configuration --- .appveyor.yml | 31 +++++++++++++++---------------- .gitignore | 3 +-- 2 files changed, 16 insertions(+), 18 deletions(-) diff --git a/.appveyor.yml b/.appveyor.yml index 5e9cb5b..898dba6 100644 --- a/.appveyor.yml +++ b/.appveyor.yml @@ -1,5 +1,5 @@ -# .appveyor.yml for use with EPICS Base ci-scripts -# (see: https://github.com/epics-base/ci-scripts) +# Appveyor configuration for EPICS 7 submodules +# (see also https://github.com/epics-base/ci-scripts) # This is YAML - indentation levels are crucial @@ -33,24 +33,26 @@ configuration: - dynamic-debug - static-debug -# Environment variables: compiler toolchain, base version, setup file, ... +# Default build worker image +# Despite the Appveyor doc's, don't use this image for vs2015 +image: Visual Studio 2019 + +# Environment variables: base version, compiler toolchain, etc. environment: # common / default variables for all jobs - SETUP_PATH: .ci-local + SETUP_PATH: .ci-local:.ci EPICS_TEST_IMPRECISE_TIMING: YES BASE: 7.0 matrix: - CMP: vs2019 - BASE: 3.15 - APPVEYOR_BUILD_WORKER_IMAGE: Visual Studio 2019 - - CMP: vs2019 - APPVEYOR_BUILD_WORKER_IMAGE: Visual Studio 2019 + - BASE: 3.15 + CMP: vs2019 - CMP: vs2017 APPVEYOR_BUILD_WORKER_IMAGE: Visual Studio 2017 - CMP: vs2015 + APPVEYOR_BUILD_WORKER_IMAGE: Visual Studio 2015 - CMP: gcc - APPVEYOR_BUILD_WORKER_IMAGE: Visual Studio 2019 # TODO: static linking w/ readline isn't working. Bypass auto-detect COMMANDLINE_LIBRARY: EPICS @@ -71,8 +73,9 @@ matrix: #---------------------------------# install: + - cmd: set PATH=C:\Python38-x64;%PATH% - cmd: git submodule update --init --recursive - - cmd: pip install git+https://github.com/mdavidsaver/ci-core-dumper#egg=ci-core-dumper + - cmd: python -m pip install git+https://github.com/mdavidsaver/ci-core-dumper#egg=ci-core-dumper - cmd: python .ci/cue.py prepare build_script: @@ -93,9 +96,9 @@ on_failure: # debugging # #---------------------------------# -## To connect by remote desktop to a failed build, uncomment the lines below. +## To connect by remote desktop to a failing build, uncomment the lines below. ## You must connect within the usual build timeout limit (60 minutes), -## so adjust the build matrix above to just build the config of interest. +## so adjust the build matrix above to build only the config of interest. #on_failure: # - ps: iex ((new-object net.webclient).DownloadString('https://raw.githubusercontent.com/appveyor/ci/master/scripts/enable-rdp.ps1')) @@ -106,8 +109,4 @@ on_failure: #---------------------------------# notifications: - - provider: Email - to: - - core-talk@aps.anl.gov - on_build_success: false - provider: GitHubPullRequest diff --git a/.gitignore b/.gitignore index fb6d786..25d8b1c 100644 --- a/.gitignore +++ b/.gitignore @@ -7,11 +7,10 @@ /include/ /templates/ /configure/*.local -/configure/RELEASE.* -/configure/CONFIG_SITE.* O.*/ /QtC-* envPaths *.orig *.log .*.swp +.iocsh_history From 7a23e8b082817f05e60b75befb95c25def31f7e3 Mon Sep 17 00:00:00 2001 From: Andrew Johnson Date: Thu, 9 Apr 2026 14:38:12 -0500 Subject: [PATCH 03/12] Do verbose builds on Appveyor --- .appveyor.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.appveyor.yml b/.appveyor.yml index 898dba6..547a0aa 100644 --- a/.appveyor.yml +++ b/.appveyor.yml @@ -43,6 +43,8 @@ environment: SETUP_PATH: .ci-local:.ci EPICS_TEST_IMPRECISE_TIMING: YES BASE: 7.0 + # Verbose builds don't seem to fail, and might be faster! + VV: 1 matrix: - CMP: vs2019 From 13b56e704c1c6e3428cc6345057e16be02f9ac2f Mon Sep 17 00:00:00 2001 From: Michael Davidsaver Date: Wed, 3 Jun 2026 14:42:30 -0700 Subject: [PATCH 04/12] doc: add leading * for @code listings --- src/misc/pv/pvUnitTest.h | 82 ++++++++++++++++++++-------------------- 1 file changed, 40 insertions(+), 42 deletions(-) diff --git a/src/misc/pv/pvUnitTest.h b/src/misc/pv/pvUnitTest.h index 1802a98..f6fe69b 100644 --- a/src/misc/pv/pvUnitTest.h +++ b/src/misc/pv/pvUnitTest.h @@ -69,8 +69,6 @@ inline testPassx testNotEqualx(const char *nLHS, const char *nRHS, const LHS& l, * * Helper functions for writing unit tests. * - @include unittest.cpp - * * @{ */ @@ -78,34 +76,34 @@ inline testPassx testNotEqualx(const char *nLHS, const char *nRHS, const LHS& l, * * Each invocation of TEST_METHOD() constructs a new instance of 'klass' on the stack. * Thus constructor and destructor can be used for common test setup and tear down. - @code - namespace { // anon - struct MyTest { - MyTest() { } // setup - ~MyTest() { } // tear down - void test1() {} - void test2() {} - }; - } // namespace anon - MAIN(somename) { - testPlan(0); - TEST_METHOD(MyTest, test1) - TEST_METHOD(MyTest, test2) - return testDone(); - } - @endcode + * @code + * namespace { // anon + * struct MyTest { + * MyTest() { } // setup + * ~MyTest() { } // tear down + * void test1() {} + * void test2() {} + * }; + * } // namespace anon + * MAIN(somename) { + * testPlan(0); + * TEST_METHOD(MyTest, test1) + * TEST_METHOD(MyTest, test2) + * return testDone(); + * } + * @endcode */ #define TEST_METHOD(klass, method) ::detail::test_method(#klass, #method) /** Compare equality. print left and right hand values and expression strings * - @code - int x=5; - testEqual(x, 5); - // prints "ok 1 - x (5) == 5 (5)\n" - testEqual(x, 6)<<" oops"; - // prints "not ok 1 - x (5) == 6 (6) oops\n" - @endcode + * @code + * int x=5; + * testEqual(x, 5); + * // prints "ok 1 - x (5) == 5 (5)\n" + * testEqual(x, 6)<<" oops"; + * // prints "not ok 1 - x (5) == 6 (6) oops\n" + * @endcode */ #define testEqual(LHS, RHS) ::detail::testEqualx(#LHS, #RHS, LHS, RHS) @@ -113,38 +111,38 @@ inline testPassx testNotEqualx(const char *nLHS, const char *nRHS, const LHS& l, /** Pass/fail from boolean * - @code - bool y=true; - testTrue(y); - // prints "ok 1 - y\n" - testTrue(!y)<<" oops"; - // prints "not ok 1 - !y oops\n" - @endcode + * @code + * bool y=true; + * testTrue(y); + * // prints "ok 1 - y\n" + * testTrue(!y)<<" oops"; + * // prints "not ok 1 - !y oops\n" + * @endcode */ #define testTrue(B) ::detail::testPassx(!!(B))<<#B /** Test that a given block throws an exception * - @code - testThrows(std::runtime_error, somefunc(5)) - @endcode + * @code + * testThrows(std::runtime_error, somefunc(5)) + * @endcode */ #define testThrows(EXC, CODE) try{ CODE; testFail("unexpected success of " #CODE); }catch(EXC& e){testPass("catch expected exception: %s", e.what());} /** Print test output w/o testing * - @code - testShow()<<"Foo"; - @endcode + * @code + * testShow()<<"Foo"; + * @endcode */ #define testShow() ::detail::testPassx() /** Compare value of PVStructure field * - @code - PVStructurePtr x(.....); - testFieldEqual(x, "alarm.severity", 1); - @endcode + * @code + * PVStructurePtr x(.....); + * testFieldEqual(x, "alarm.severity", 1); + * @endcode */ template ::detail::testPassx From 57158e0cbf32989a44f2b26a2f6d7d15ea958453 Mon Sep 17 00:00:00 2001 From: Andrew Johnson Date: Wed, 29 Jul 2026 14:05:57 -0500 Subject: [PATCH 05/12] Update configure/CONFIG --- configure/CONFIG | 34 +++++++++++++++++++++++++++++++--- 1 file changed, 31 insertions(+), 3 deletions(-) diff --git a/configure/CONFIG b/configure/CONFIG index c1a4703..f22aa54 100644 --- a/configure/CONFIG +++ b/configure/CONFIG @@ -1,3 +1,7 @@ +# SPDX-FileCopyrightText: 1997 Argonne National Laboratory +# +# SPDX-License-Identifier: EPICS + # CONFIG - Load build configuration data # # Do not make changes to this file! @@ -8,9 +12,19 @@ RULES = $(EPICS_BASE) # RELEASE files point to other application tops include $(TOP)/configure/RELEASE -include $(TOP)/configure/RELEASE.$(EPICS_HOST_ARCH).Common + ifdef T_A --include $(TOP)/configure/RELEASE.Common.$(T_A) --include $(TOP)/configure/RELEASE.$(EPICS_HOST_ARCH).$(T_A) + -include $(TOP)/configure/RELEASE.Common.$(T_A) + -include $(TOP)/configure/RELEASE.$(EPICS_HOST_ARCH).$(T_A) +endif + +# Check EPICS_BASE is set properly +ifneq (file,$(origin EPICS_BASE)) + $(error EPICS_BASE must be set in a configure/RELEASE file) +else + ifeq ($(wildcard $(EPICS_BASE)/configure/CONFIG_BASE),) + $(error EPICS_BASE does not point to an EPICS installation) + endif endif CONFIG = $(RULES)/configure @@ -19,11 +33,25 @@ include $(CONFIG)/CONFIG # Override the Base definition: INSTALL_LOCATION = $(TOP) -# CONFIG_SITE files contain other build configuration settings +# CONFIG_SITE files contain local build configuration settings include $(TOP)/configure/CONFIG_SITE + +# Host-arch specific settings -include $(TOP)/configure/CONFIG_SITE.$(EPICS_HOST_ARCH).Common + ifdef T_A + # Target-arch specific settings -include $(TOP)/configure/CONFIG_SITE.Common.$(T_A) + + # Host & target specific settings -include $(TOP)/configure/CONFIG_SITE.$(EPICS_HOST_ARCH).$(T_A) endif +# Load our cfg/CONFIG* files if installing elsewhere +ifneq ($(INSTALL_LOCATION),$(TOP)) + TOP_CFG_CONFIGS := $(wildcard $(INSTALL_CFG)/CONFIG*) + ifneq ($(TOP_CFG_CONFIGS),) + include $(TOP_CFG_CONFIGS) + endif +endif + From 777c9215350e4ff1118fded7ecc27fa51f21bf24 Mon Sep 17 00:00:00 2001 From: Michael Davidsaver Date: Thu, 2 Jul 2026 18:40:52 -0700 Subject: [PATCH 06/12] Check for match of complex element types eg. Array of Structure must have Structure elements. --- src/factory/FieldCreateFactory.cpp | 8 ++++++-- testApp/misc/testSerialization.cpp | 31 +++++++++++++++++++++++++++++- 2 files changed, 36 insertions(+), 3 deletions(-) diff --git a/src/factory/FieldCreateFactory.cpp b/src/factory/FieldCreateFactory.cpp index 561e8ee..b584ad9 100644 --- a/src/factory/FieldCreateFactory.cpp +++ b/src/factory/FieldCreateFactory.cpp @@ -1564,7 +1564,9 @@ FieldConstPtr FieldCreate::deserialize(ByteBuffer* buffer, DeserializableControl throw std::invalid_argument("fixed and bounded structure array not supported"); // Type type = Type.structureArray; - StructureConstPtr elementStructure = std::tr1::static_pointer_cast(control->cachedDeserialize(buffer)); + StructureConstPtr elementStructure = std::tr1::dynamic_pointer_cast(control->cachedDeserialize(buffer)); + if(!elementStructure) + throw std::invalid_argument("Expected Structure"); // TODO use std::make_shared std::tr1::shared_ptr sp(new StructureArray(elementStructure)); Helper::cache(this, sp); @@ -1577,7 +1579,9 @@ FieldConstPtr FieldCreate::deserialize(ByteBuffer* buffer, DeserializableControl throw std::invalid_argument("fixed and bounded structure array not supported"); // Type type = Type.unionArray; - UnionConstPtr elementUnion = std::tr1::static_pointer_cast(control->cachedDeserialize(buffer)); + UnionConstPtr elementUnion = std::tr1::dynamic_pointer_cast(control->cachedDeserialize(buffer)); + if(!elementUnion) + throw std::invalid_argument("Expected Union"); // TODO use std::make_shared std::tr1::shared_ptr sp(new UnionArray(elementUnion)); Helper::cache(this, sp); diff --git a/testApp/misc/testSerialization.cpp b/testApp/misc/testSerialization.cpp index dcbef23..617586a 100644 --- a/testApp/misc/testSerialization.cpp +++ b/testApp/misc/testSerialization.cpp @@ -849,11 +849,38 @@ void testFromString(int byteOrder) testOk1(_data->getSubFieldT("Y")->get()=="testing"); } +void testDeserializeTypeFail(const char *buf, size_t buflen) +{ + + std::vector bytes(buf, buf + buflen); + ByteBuffer B(&bytes[0], bytes.size(), EPICS_ENDIAN_BIG); + + try{ + FieldConstPtr ptr(getFieldCreate()->deserialize(&B, control)); + }catch(std::exception& e) { + testPass("Expected exception: %s", e.what()); + return; + } + testFail("Unexpected success? %d", __LINE__); +} + +void testMalformed() +{ + testDiag("testMalformed()"); + +#define CASE(STR) testDeserializeTypeFail(STR, sizeof(STR)-1) + // a structure array where the element type is not a structure + CASE("\x88\x83\x2b\x01\x01"); + // a union array where ... + CASE("\x89\x83\x2b\x01\x01"); +#undef CASE +} + } // end namespace MAIN(testSerialization) { - testPlan(234); + testPlan(236); flusher = new SerializableControlImpl(); control = new DeserializableControlImpl(); @@ -880,6 +907,8 @@ MAIN(testSerialization) { testFromString(EPICS_ENDIAN_BIG); testFromString(EPICS_ENDIAN_LITTLE); + testMalformed(); + delete buffer; delete control; delete flusher; From abc95982ed5d750b727e1ace7b944e7964b8ab9f Mon Sep 17 00:00:00 2001 From: Michael Davidsaver Date: Fri, 3 Jul 2026 18:54:35 -0700 Subject: [PATCH 07/12] bound deserialize() recursion --- src/factory/FieldCreateFactory.cpp | 22 +++++++++++++++++++++- src/factory/PVStructure.cpp | 3 +++ src/factory/PVStructureArray.cpp | 3 +++ src/factory/PVUnion.cpp | 3 +++ src/factory/PVUnionArray.cpp | 3 +++ src/factory/factoryPvt.h | 14 ++++++++++++++ testApp/misc/testSerialization.cpp | 24 +++++++++++++++++++++++- 7 files changed, 70 insertions(+), 2 deletions(-) create mode 100644 src/factory/factoryPvt.h diff --git a/src/factory/FieldCreateFactory.cpp b/src/factory/FieldCreateFactory.cpp index b584ad9..c4ed717 100644 --- a/src/factory/FieldCreateFactory.cpp +++ b/src/factory/FieldCreateFactory.cpp @@ -28,6 +28,8 @@ #include #include +#include "factoryPvt.h" + using std::tr1::static_pointer_cast; using std::size_t; using std::string; @@ -36,7 +38,6 @@ namespace epics { namespace pvData { size_t Field::num_instances; - struct Field::Helper { static unsigned hash(Field *fld) { std::ostringstream key; @@ -1476,6 +1477,7 @@ static int decodeScalar(int8 code) FieldConstPtr FieldCreate::deserialize(ByteBuffer* buffer, DeserializableControl* control) const { + RecurseGuard RG; control->ensureData(1); int8 code = buffer->getByte(); if (code == -1) @@ -1604,6 +1606,7 @@ FieldConstPtr FieldCreate::deserialize(ByteBuffer* buffer, DeserializableControl namespace detail { struct field_factory { FieldCreatePtr fieldCreate; + epicsThreadPrivate deserDepth; field_factory() :fieldCreate(new FieldCreate()) { registerRefCounter("Field", &Field::num_instances); registerRefCounter("Thread", &Thread::num_instances); @@ -1614,6 +1617,23 @@ struct field_factory { static detail::field_factory* field_factory_s; static epicsThreadOnceId field_factory_once = EPICS_THREAD_ONCE_INIT; +#if defined(__rtems__) || defined(vxWorks) +// lower limit on embedded targets +const unsigned maxDepth = 10; +#else +const unsigned maxDepth = 20; +#endif +RecurseGuard::RecurseGuard() { + size_t current = (size_t)field_factory_s->deserDepth.get(); + if(current>=maxDepth) + throw std::logic_error("FieldCreateFactory recursion depth exceeded"); + field_factory_s->deserDepth.set((unsigned*)(current+1)); +} +RecurseGuard::~RecurseGuard() { + size_t current = (size_t)field_factory_s->deserDepth.get(); + field_factory_s->deserDepth.set((unsigned*)(current-1)); +} + static void field_factory_init(void*) { try { diff --git a/src/factory/PVStructure.cpp b/src/factory/PVStructure.cpp index ff14b81..0a8d91c 100644 --- a/src/factory/PVStructure.cpp +++ b/src/factory/PVStructure.cpp @@ -18,6 +18,8 @@ #include #include +#include "factoryPvt.h" + using std::tr1::static_pointer_cast; using std::size_t; using std::string; @@ -224,6 +226,7 @@ void PVStructure::serialize(ByteBuffer *pbuffer, void PVStructure::deserialize(ByteBuffer *pbuffer, DeserializableControl *pcontrol) { + RecurseGuard G; size_t fieldsSize = pvFields.size(); for(size_t i = 0; ideserialize(pbuffer, pcontrol); diff --git a/src/factory/PVStructureArray.cpp b/src/factory/PVStructureArray.cpp index 66577b5..45fdf18 100644 --- a/src/factory/PVStructureArray.cpp +++ b/src/factory/PVStructureArray.cpp @@ -16,6 +16,8 @@ #include #include +#include "factoryPvt.h" + using std::tr1::static_pointer_cast; using std::size_t; @@ -158,6 +160,7 @@ void PVStructureArray::serialize(ByteBuffer *pbuffer, void PVStructureArray::deserialize(ByteBuffer *pbuffer, DeserializableControl *pcontrol) { + RecurseGuard G; svector data(reuse()); size_t size = this->getArray()->getArraySizeType() == Array::fixed ? diff --git a/src/factory/PVUnion.cpp b/src/factory/PVUnion.cpp index a52687b..8d917d4 100644 --- a/src/factory/PVUnion.cpp +++ b/src/factory/PVUnion.cpp @@ -18,6 +18,8 @@ #include #include +#include "factoryPvt.h" + using std::tr1::static_pointer_cast; using std::size_t; using std::string; @@ -143,6 +145,7 @@ void PVUnion::serialize(ByteBuffer *pbuffer, SerializableControl *pflusher) cons void PVUnion::deserialize(ByteBuffer *pbuffer, DeserializableControl *pcontrol) { + RecurseGuard G; if (variant) { FieldConstPtr field = pcontrol->cachedDeserialize(pbuffer); diff --git a/src/factory/PVUnionArray.cpp b/src/factory/PVUnionArray.cpp index 151bc5b..1b4240b 100644 --- a/src/factory/PVUnionArray.cpp +++ b/src/factory/PVUnionArray.cpp @@ -16,6 +16,8 @@ #include #include +#include "factoryPvt.h" + using std::tr1::static_pointer_cast; using std::size_t; @@ -157,6 +159,7 @@ void PVUnionArray::serialize(ByteBuffer *pbuffer, void PVUnionArray::deserialize(ByteBuffer *pbuffer, DeserializableControl *pcontrol) { + RecurseGuard G; svector data(reuse()); size_t size = this->getArray()->getArraySizeType() == Array::fixed ? diff --git a/src/factory/factoryPvt.h b/src/factory/factoryPvt.h new file mode 100644 index 0000000..baa08de --- /dev/null +++ b/src/factory/factoryPvt.h @@ -0,0 +1,14 @@ +#ifndef FACTORYPVT_H +#define FACTORYPVT_H + +namespace epics { namespace pvData { + +// use to bound recursion in FieldCreate and PVDataCreator singletons +struct RecurseGuard { + RecurseGuard(); + ~RecurseGuard(); +}; + +}} // namespace epics::pvData + +#endif // FACTORYPVT_H diff --git a/testApp/misc/testSerialization.cpp b/testApp/misc/testSerialization.cpp index 617586a..97bf244 100644 --- a/testApp/misc/testSerialization.cpp +++ b/testApp/misc/testSerialization.cpp @@ -874,13 +874,35 @@ void testMalformed() // a union array where ... CASE("\x89\x83\x2b\x01\x01"); #undef CASE + { + std::vector inp(500u); + for(size_t i=0; i inp(500u); + for(size_t i=0; i Date: Sun, 5 Jul 2026 20:54:59 -0500 Subject: [PATCH 08/12] Harden array allocation in BitSet::deserialize() Move bounds check to before the allocation. Treat NULL bitset values as empty to avoid overflow. Add tests for excessive allocation, and Null values. --- src/misc/bitSet.cpp | 10 ++++----- testApp/misc/testBitSet.cpp | 44 ++++++++++++++++++++++++++++++++++++- 2 files changed, 47 insertions(+), 7 deletions(-) diff --git a/src/misc/bitSet.cpp b/src/misc/bitSet.cpp index d53aaeb..a3bbd4e 100644 --- a/src/misc/bitSet.cpp +++ b/src/misc/bitSet.cpp @@ -337,17 +337,15 @@ namespace epics { namespace pvData { } void BitSet::deserialize(ByteBuffer* buffer, DeserializableControl* control) { + size_t size = SerializeHelper::readSize(buffer, control); + uint32 bytes = (size == (size_t) -1) ? 0 : static_cast(size); - uint32 bytes = static_cast(SerializeHelper::readSize(buffer, control)); // in bytes + // Validate availability before allocating storage + control->ensureData(bytes); size_t wordsInUse = (bytes + 7) / BYTES_PER_WORD; words.resize(wordsInUse); - if (wordsInUse == 0) - return; - - control->ensureData(bytes); - uint32 i = 0; uint32 longs = bytes / 8; while (i < longs) diff --git a/testApp/misc/testBitSet.cpp b/testApp/misc/testBitSet.cpp index dbecfaf..87e2c71 100644 --- a/testApp/misc/testBitSet.cpp +++ b/testApp/misc/testBitSet.cpp @@ -303,15 +303,57 @@ static void testSerialize() #undef TOFRO } +static void testDeserializeOversize() +{ + testDiag("testDeserializeOversize"); + + // Size field declares INT32_MAX bytes but no payload follows: 0xFE + // long-form marker + big-endian int32. deserialize() must reject this + // via ensureData() before allocating a ~2GB backing store. + const char msg[] = {(char)0xFE, 0x7F, (char)0xFF, (char)0xFF, (char)0xFF}; + BitSet dut; + ByteBuffer buf((char*)msg, sizeof(msg), EPICS_ENDIAN_BIG); + try { + deserializeFromBuffer(&dut, buf); + testFail("oversized BitSet size was not rejected"); + } catch(std::exception& e) { + testPass("oversized BitSet size rejected: %s", e.what()); + } +} + +static void testDeserializeNull() +{ + testDiag("testDeserializeNull"); + + // A NULL size is encoded on the wire as the single byte 0xFF. Without + // the null check in deserialize() this would be cast to UINT32_MAX bytes + // and overflow the allocation size; it must instead yield an empty BitSet. + const char msg[] = {(char)0xFF}; + BitSet dut; + dut.set(5); // ensure deserialize actually clears/replaces existing bits + ByteBuffer buf((char*)msg, sizeof(msg), EPICS_ENDIAN_BIG); + try { + deserializeFromBuffer(&dut, buf); + testPass("NULL BitSet size accepted"); + } catch(std::exception& e) { + testFail("NULL BitSet size rejected: %s", e.what()); + } + testOk1(dut.isEmpty()); + testOk(buf.getRemaining()==0, "buffer remaining 0 == %u", + (unsigned)buf.getRemaining()); +} + } // namespace MAIN(testBitSet) { - testPlan(90); + testPlan(94); testInitialize(); testGetSetClearFlip(); testOperators(); testLogical(); testSerialize(); + testDeserializeOversize(); + testDeserializeNull(); return testDone(); } From 2c5854a7ec6c4a0430c0d04482a8b858bbecdaeb Mon Sep 17 00:00:00 2001 From: Andrew Johnson Date: Tue, 21 Jul 2026 16:00:00 -0500 Subject: [PATCH 09/12] Add a max string len param to deserializeString() Defaults to INT32_MAX if not provided, but several places in pvAccess can set limits, e.g. MAX_CHANNEL_NAME_LENGTH, and this is the best way to harden them against huge values. Document that SerializeHelper::readSize() returns -1 for NULL. --- src/misc/pv/serializeHelper.h | 24 ++++++++---- src/misc/serializeHelper.cpp | 72 ++++++++++++++++------------------- 2 files changed, 50 insertions(+), 46 deletions(-) diff --git a/src/misc/pv/serializeHelper.h b/src/misc/pv/serializeHelper.h index 5ced0d6..57f74e6 100644 --- a/src/misc/pv/serializeHelper.h +++ b/src/misc/pv/serializeHelper.h @@ -50,7 +50,7 @@ namespace epics { * * @param[in] buffer deserialization buffer. * @param[in] control the DeserializableControl. - * @returns array size. + * @returns array size, or (size_t) -1. */ static std::size_t readSize(ByteBuffer* buffer, DeserializableControl* control); @@ -80,20 +80,22 @@ namespace epics { /** * std::string deserialization helper method. - * TODO This method cannot return "null", but Java implementation - * could have serialized "null" value as well. We need to decide - * how to deserialize "null". * * @param[in] buffer deserialization buffer * @param[in] control control + * @param[in] max length limit * @returns deserialized string * * @todo This method cannot return "null", but Java implementation - * could have serialized "null" value as well. We need to decide - * how to deserialize "null". + * could have serialized "null" value as well. This implementation + * returns an empty string in that event. + * + * @since UNRELEASED Added the max argument. + * Test for the DESERIALIZE_STRING_HAS_LIMIT macro to detect. + * Retroactive default is 2 GiB */ static std::string deserializeString(ByteBuffer* buffer, - DeserializableControl* control); + DeserializableControl* control, std::size_t max = 0x7fffffff); private: SerializeHelper() {}; @@ -112,4 +114,12 @@ namespace epics { } } +/** Feature test macro for max argument + * + * of SerializeHelper::deserializeString() + * + * @since UNRELEASED added + */ +#define DESERIALIZE_STRING_HAS_LIMIT + #endif /* SERIALIZEHELPER_H_ */ diff --git a/src/misc/serializeHelper.cpp b/src/misc/serializeHelper.cpp index 822ddec..62b8196 100644 --- a/src/misc/serializeHelper.cpp +++ b/src/misc/serializeHelper.cpp @@ -47,12 +47,13 @@ namespace epics { DeserializableControl* control) { control->ensureData(1); int8 b = buffer->getByte(); - if(b==-1) - return -1; - else if(b==-2) { + if (b == -1) + return -1; // NULL + else if (b == -2) { control->ensureData(sizeof(int32)); int32 s = buffer->getInt(); - if(s<0) THROW_BASE_EXCEPTION("negative size"); + if (s < 0) + THROW_BASE_EXCEPTION("negative size"); return s; } else @@ -100,44 +101,37 @@ namespace epics { } string SerializeHelper::deserializeString(ByteBuffer* buffer, - DeserializableControl* control) { + DeserializableControl* control, std::size_t max) { std::size_t size = SerializeHelper::readSize(buffer, control); - if(size!=(size_t)-1) // TODO null strings check, to be removed in the future - { - if (buffer->getRemaining()>=size) - { - // entire string is in buffer, simply create a string out of it (copy) - std::size_t pos = buffer->getPosition(); - string str(buffer->getBuffer()+pos, size); - buffer->setPosition(pos+size); - return str; - } - else - { - string str; - str.reserve(size); - try { - std::size_t i = 0; - while(true) { - std::size_t toRead = min(size-i, buffer->getRemaining()); - std::size_t pos = buffer->getPosition(); - str.append(buffer->getBuffer()+pos, toRead); - buffer->setPosition(pos+toRead); - i += toRead; - if(iensureData(1); // at least one - else - break; - } - return str; - } catch(...) { - throw; - } - } + if (size == (std::size_t) -1) + return std::string(); // NULL from old implementations + + if (size > max) + THROW_BASE_EXCEPTION("string too long"); + + if (buffer->getRemaining() >= size) { // All present + std::size_t pos = buffer->getPosition(); + string str(buffer->getBuffer() + pos, size); + buffer->setPosition(pos + size); + return str; } - else - return std::string(); + + string str; + str.reserve(size); + std::size_t i = 0; + while (true) { + std::size_t toRead = min(size-i, buffer->getRemaining()); + std::size_t pos = buffer->getPosition(); + str.append(buffer->getBuffer() + pos, toRead); + buffer->setPosition(pos + toRead); + i += toRead; + if (i < size) + control->ensureData(1); // at least one + else + break; + } + return str; } } } From e7c69cb5d258321abef9cfb22056fcb8cae402fa Mon Sep 17 00:00:00 2001 From: Andrew Johnson Date: Fri, 31 Jul 2026 15:21:59 -0500 Subject: [PATCH 10/12] Protect PVUnion selector in deserialize --- src/factory/PVUnion.cpp | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/src/factory/PVUnion.cpp b/src/factory/PVUnion.cpp index 8d917d4..787a5ba 100644 --- a/src/factory/PVUnion.cpp +++ b/src/factory/PVUnion.cpp @@ -161,21 +161,26 @@ void PVUnion::deserialize(ByteBuffer *pbuffer, DeserializableControl *pcontrol) } else { - int32 previousSelector = selector; - selector = static_cast(SerializeHelper::readSize(pbuffer, pcontrol)); - if (selector != UNDEFINED_INDEX) + int32 index = static_cast(SerializeHelper::readSize(pbuffer, pcontrol)); + if (index == UNDEFINED_INDEX) { - if (selector != previousSelector) + selector = index; + value.reset(); + } + else if (index < 0 || size_t(index) >= unionPtr->getFields().size()) + throw std::runtime_error("union index out of bounds"); + else + { + if (selector != index) { - FieldConstPtr field = unionPtr->getField(selector); + selector = index; + FieldConstPtr field = unionPtr->getField(index); // try to reuse existing field instance if (!value.get() || *value->getField() != *field) value = pvDataCreate->createPVField(field); } value->deserialize(pbuffer, pcontrol); } - else - value.reset(); } } From 89d2702cdcbacdc9a01601913ec0feb27a226960 Mon Sep 17 00:00:00 2001 From: Michael Davidsaver Date: Sun, 9 Aug 2026 18:03:13 -0700 Subject: [PATCH 11/12] treat unknown Status codes as FATAL --- src/misc/status.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/misc/status.cpp b/src/misc/status.cpp index 0edd0f3..ed3f4fa 100644 --- a/src/misc/status.cpp +++ b/src/misc/status.cpp @@ -74,6 +74,8 @@ void Status::deserialize(ByteBuffer *buffer, DeserializableControl *flusher) } else { + if(typeCode < 0 || typeCode > STATUSTYPE_FATAL) + typeCode = STATUSTYPE_FATAL; m_statusType = (StatusType)typeCode; m_message = SerializeHelper::deserializeString(buffer, flusher); m_stackDump = SerializeHelper::deserializeString(buffer, flusher); From 891052652ee68cc11f5a570261c4f116bf632eac Mon Sep 17 00:00:00 2001 From: Jeremy Lorelli Date: Tue, 17 Mar 2026 23:07:28 -0700 Subject: [PATCH 12/12] Add -S option to pvget/pvmonitor to stringify byte arrays Behaves similar to caget's '-S' option, except that NULL termination is not respected. Instead, string length is determined solely based on the array length, with single trailing NULLs being stripped. All other characters within the range [0, len) are escaped and/or printed. --- src/factory/printer.cpp | 25 +++++++++++++++++++++++-- src/pv/pvData.h | 4 ++++ 2 files changed, 27 insertions(+), 2 deletions(-) diff --git a/src/factory/printer.cpp b/src/factory/printer.cpp index 846f2de..ab89229 100644 --- a/src/factory/printer.cpp +++ b/src/factory/printer.cpp @@ -434,12 +434,33 @@ std::ostream& operator<<(std::ostream& strm, const PVStructure::Formatter& forma return strm; case scalarArray: + { strm<(value.get())<<'\n'; - return strm; + const PVScalarArray* pvArray = static_cast(value.get()); + ScalarArrayConstPtr scalarArray = pvArray->getScalarArray(); + + // Stringify byte arrays if requested + if (format.xarrAsStr && scalarArray && scalarArray->getElementType() == pvByte) { + shared_vector byteArray; + pvArray->getAs(byteArray); + + if (!byteArray.empty()) { + const char* data = reinterpret_cast(byteArray.data()); + size_t slen = byteArray.size(); + if (!data[slen-1]) + --slen; // Trim trailing null so we don't end up with "mystring\0" + strm<