diff --git a/examples/companion_radio/MyMesh.cpp b/examples/companion_radio/MyMesh.cpp index 803cf8197..d7f8e1cca 100644 --- a/examples/companion_radio/MyMesh.cpp +++ b/examples/companion_radio/MyMesh.cpp @@ -6983,7 +6983,9 @@ void MyMesh::handleCmdFrame(size_t len) { int i = 0; out_frame[i++] = RESP_CODE_CHANNEL_INFO; out_frame[i++] = channel_idx; - strcpy((char *)&out_frame[i], channel.name); + // This fixed-width field must not retain bytes from an earlier reply. + strncpy((char *)&out_frame[i], channel.name, 32); + out_frame[i + 31] = 0; i += 32; size_t key_size = 16; // Original app response remains byte-for-byte compatible. #if MESH_ENABLE_MESHTEXT_CHAT diff --git a/test/test_companion_channel_alias.py b/test/test_companion_channel_alias.py index b4a6d70a6..dcfafe675 100644 --- a/test/test_companion_channel_alias.py +++ b/test/test_companion_channel_alias.py @@ -822,6 +822,73 @@ int main() { } ''' +GET_PADDING_HARNESS = r''' +#include +#include +#include +#include +#undef assert +#define assert(condition) do { if(!(condition)) { \ + std::fprintf(stderr,"channel GET assertion: %s\n",#condition);std::exit(91); \ +} } while(false) +@CONSTANTS@ +struct ChannelDetails { + struct { uint8_t secret[32]; } channel; + char name[32]; +}; +struct Serial { + uint8_t frame[128]; int length=0; + void writeFrame(const uint8_t* bytes,int size) { + assert(size>0 && size<=int(sizeof(frame))); + memcpy(frame,bytes,size);length=size; + } +}; +struct MyMesh { + uint8_t out_frame[128];ChannelDetails saved{}; + Serial serial;Serial* _serial=&serial; + bool meshtext=false;int error=-1; + bool getChannelForSave(uint8_t slot,ChannelDetails& out) { + if(slot!=5)return false; + out=saved; + return true; + } + bool isMeshtextChannel(const ChannelDetails&)const{return meshtext;} + void writeErrFrame(int code){error=code;} + void command(uint8_t* cmd_frame,int len) { + @GET_BRANCH@ + } +}; +int main() { + MyMesh node; + for(size_t name_length=0;name_length<=32;++name_length) { + memset(node.saved.name,0,sizeof(node.saved.name)); + for(size_t i=0;i= 2)") + constants = "\n".join(re.search(r"^#define\s+" + name + r"\s+[^\r\n]+", companion, + flags=re.MULTILINE).group(0) for name in ( + "CMD_GET_CHANNEL", "RESP_CODE_CHANNEL_INFO", "ERR_CODE_NOT_FOUND")) + for meshtext in (0, 1): + with self.subTest(meshtext=meshtext): + source = (f"#define MESH_ENABLE_MESHTEXT_CHAT {meshtext}\n" + GET_PADDING_HARNESS + .replace("@CONSTANTS@", constants).replace("@GET_BRANCH@", branch)) + self.run_native_program(source, "padding") + # Preserve the original bug as an executable negative control: an + # unpadded strcpy response must fail the independent full-frame oracle. + old_branch = re.sub(r"strncpy\(([^\n]+), 32\);\s*out_frame\[i \+ 31\] = 0;", + r"strcpy(\1);", branch) + self.assertNotEqual(old_branch, branch) + source = ("#define MESH_ENABLE_MESHTEXT_CHAT 0\n" + GET_PADDING_HARNESS + .replace("@CONSTANTS@", constants).replace("@GET_BRANCH@", old_branch)) + self.run_native_program(source, "padding", expect_failure="memcmp(node.serial.frame,expected") + def test_persisted_emergency_join_seeds_quiet_local_history_and_defers_without_eviction(self): companion = (ROOT / "examples/companion_radio/MyMesh.cpp").read_text(encoding="utf-8") header = (ROOT / "examples/companion_radio/MyMesh.h").read_text(encoding="utf-8")