This is an automated email from the ASF dual-hosted git repository. sjanc pushed a commit to branch master in repository https://gitbox.apache.org/repos/asf/mynewt-newtmgr.git
commit b92f1adaeadb25dd5d866f420b70c71a685e5f97 Author: Szymon Janc <[email protected]> AuthorDate: Thu Oct 22 00:24:52 2026 +0200 newtmgr/core: Add unit tests for corefile to ELF conversion Build corefiles in memory, convert them with ConvertFilenames() and parse the result with debug/elf. Check the ELF header (ET_CORE, EM_ARM), the PT_NOTE segment carrying the registers in an NT_PRSTATUS note (including truncation to 18 registers and short register areas), PT_LOAD segments with their addresses, sizes and contents, file offsets with padding of odd-length memory areas, and the image hash. Also cover error handling: empty file, short header, bad magic, short TLV header, unknown TLV type, truncated TLV data, register area size not a multiple of 4, and unopenable files. Assisted-by: Claude:claude-opus-5-5 --- newtmgr/core/core_convert_test.go | 312 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 312 insertions(+) diff --git a/newtmgr/core/core_convert_test.go b/newtmgr/core/core_convert_test.go new file mode 100644 index 0000000..aa38611 --- /dev/null +++ b/newtmgr/core/core_convert_test.go @@ -0,0 +1,312 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package core + +import ( + "bytes" + "debug/elf" + "encoding/binary" + "io" + "os" + "path/filepath" + "testing" +) + +type testTlv struct { + typ uint8 + off uint32 + data []byte +} + +// buildCore builds a corefile image consisting of a header followed by the +// given TLVs. +func buildCore(magic uint32, tlvs []testTlv) []byte { + var b bytes.Buffer + + binary.Write(&b, binary.LittleEndian, magic) + binary.Write(&b, binary.LittleEndian, uint32(0)) + for _, t := range tlvs { + b.WriteByte(t.typ) + b.WriteByte(0) + binary.Write(&b, binary.LittleEndian, uint16(len(t.data))) + binary.Write(&b, binary.LittleEndian, t.off) + b.Write(t.data) + } + + return b.Bytes() +} + +// convert writes the corefile to a temporary directory, converts it and +// returns the converter and the path of the resulting ELF file. +func convert(t *testing.T, core []byte) (*CoreConvert, string, error) { + t.Helper() + + dir := t.TempDir() + src := filepath.Join(dir, "core") + dst := filepath.Join(dir, "core.elf") + if err := os.WriteFile(src, core, 0644); err != nil { + t.Fatal(err) + } + + cc, err := ConvertFilenames(src, dst) + return cc, dst, err +} + +func seq(n int, start byte) []byte { + b := make([]byte, n) + for i := range b { + b[i] = start + byte(i) + } + return b +} + +func readProg(t *testing.T, p *elf.Prog) []byte { + t.Helper() + b, err := io.ReadAll(p.Open()) + if err != nil { + t.Fatalf("reading segment: %v", err) + } + return b +} + +func TestConvert(t *testing.T) { + hash := seq(32, 0xa0) + mem1 := seq(16, 0x10) + mem2 := seq(5, 0x50) // odd length; padded in the file + regs := make([]byte, 4*20) + for i := 0; i < 20; i++ { + binary.LittleEndian.PutUint32(regs[i*4:], 0x1000+uint32(i)) + } + + core := buildCore(COREDUMP_MAGIC, []testTlv{ + {COREDUMP_TLV_IMAGE, 0, hash}, + {COREDUMP_TLV_REGS, 0, regs}, + {COREDUMP_TLV_MEM, 0x20000000, mem1}, + {COREDUMP_TLV_MEM, 0x20001000, mem2}, + }) + + cc, dst, err := convert(t, core) + if err != nil { + t.Fatalf("ConvertFilenames: %v", err) + } + if !bytes.Equal(cc.ImageHash, hash) { + t.Errorf("ImageHash = %x, want %x", cc.ImageHash, hash) + } + + f, err := elf.Open(dst) + if err != nil { + t.Fatalf("elf.Open: %v", err) + } + defer f.Close() + + if f.Class != elf.ELFCLASS32 || f.Data != elf.ELFDATA2LSB { + t.Errorf("class/data = %v/%v", f.Class, f.Data) + } + if f.Type != elf.ET_CORE { + t.Errorf("Type = %v, want ET_CORE", f.Type) + } + if f.Machine != elf.EM_ARM { + t.Errorf("Machine = %v, want EM_ARM", f.Machine) + } + if len(f.Sections) != 0 { + t.Errorf("expected no sections, got %d", len(f.Sections)) + } + if len(f.Progs) != 3 { + t.Fatalf("expected 3 program headers, got %d", len(f.Progs)) + } + + // Program headers appear in TLV order. + note := f.Progs[0] + if note.Type != elf.PT_NOTE { + t.Errorf("phdr 0 type = %v, want PT_NOTE", note.Type) + } + nb := readProg(t, note) + if len(nb) != 12+8+148 { + t.Fatalf("note size = %d, want %d", len(nb), 12+8+148) + } + if n := binary.LittleEndian.Uint32(nb[0:]); n != 5 { + t.Errorf("note namesz = %d, want 5", n) + } + if n := binary.LittleEndian.Uint32(nb[4:]); n != 148 { + t.Errorf("note descsz = %d, want 148", n) + } + if n := binary.LittleEndian.Uint32(nb[8:]); n != uint32(elf.NT_PRSTATUS) { + t.Errorf("note type = %d, want NT_PRSTATUS", n) + } + if !bytes.Equal(nb[12:20], []byte(".reg\x00\x00\x00\x00")) { + t.Errorf("note name = %q", nb[12:20]) + } + // prstatus: 18 words of padding, then 18 registers. Registers beyond + // the 18th are dropped. + desc := nb[20:] + if !bytes.Equal(desc[:72], make([]byte, 72)) { + t.Errorf("prstatus padding not zero: %x", desc[:72]) + } + for i := 0; i < 18; i++ { + r := binary.LittleEndian.Uint32(desc[72+i*4:]) + if r != 0x1000+uint32(i) { + t.Errorf("reg %d = %#x, want %#x", i, r, 0x1000+i) + } + } + if r := binary.LittleEndian.Uint32(desc[144:]); r != 0 { + t.Errorf("trailing word = %#x, want 0", r) + } + + wantLoads := []struct { + vaddr uint32 + data []byte + }{ + {0x20000000, mem1}, + {0x20001000, mem2}, + } + for i, w := range wantLoads { + p := f.Progs[i+1] + if p.Type != elf.PT_LOAD { + t.Errorf("phdr %d type = %v, want PT_LOAD", i+1, p.Type) + } + if p.Vaddr != uint64(w.vaddr) { + t.Errorf("phdr %d vaddr = %#x, want %#x", i+1, p.Vaddr, w.vaddr) + } + if p.Filesz != uint64(len(w.data)) || p.Memsz != p.Filesz { + t.Errorf("phdr %d filesz/memsz = %d/%d, want %d", i+1, + p.Filesz, p.Memsz, len(w.data)) + } + if p.Flags != elf.PF_R || p.Align != 4 { + t.Errorf("phdr %d flags/align = %v/%d", i+1, p.Flags, p.Align) + } + if p.Off%4 != 0 { + t.Errorf("phdr %d offset %#x not 4-byte aligned", i+1, p.Off) + } + if got := readProg(t, p); !bytes.Equal(got, w.data) { + t.Errorf("phdr %d data = %x, want %x", i+1, got, w.data) + } + } + + // Segment data is laid out back to back after the program headers, + // with the odd-length segment padded to a multiple of 4. + const ehdrSz, phdrSz = 52, 32 + off := uint64(ehdrSz + 3*phdrSz) + for i, p := range f.Progs { + if p.Off != off { + t.Errorf("phdr %d offset = %#x, want %#x", i, p.Off, off) + } + off += (p.Filesz + 3) &^ 3 + } + st, err := os.Stat(dst) + if err != nil { + t.Fatal(err) + } + if uint64(st.Size()) != off { + t.Errorf("file size = %d, want %d", st.Size(), off) + } +} + +func TestConvertShortRegs(t *testing.T) { + regs := make([]byte, 8) + binary.LittleEndian.PutUint32(regs[0:], 0xdeadbeef) + binary.LittleEndian.PutUint32(regs[4:], 0xcafef00d) + + _, dst, err := convert(t, buildCore(COREDUMP_MAGIC, []testTlv{ + {COREDUMP_TLV_REGS, 0, regs}, + })) + if err != nil { + t.Fatalf("ConvertFilenames: %v", err) + } + + f, err := elf.Open(dst) + if err != nil { + t.Fatalf("elf.Open: %v", err) + } + defer f.Close() + + if len(f.Progs) != 1 || f.Progs[0].Type != elf.PT_NOTE { + t.Fatalf("unexpected program headers: %v", f.Progs) + } + desc := readProg(t, f.Progs[0])[20:] + want := []uint32{0xdeadbeef, 0xcafef00d, 0} + for i, w := range want { + if r := binary.LittleEndian.Uint32(desc[72+i*4:]); r != w { + t.Errorf("reg %d = %#x, want %#x", i, r, w) + } + } +} + +func TestConvertErrors(t *testing.T) { + valid := buildCore(COREDUMP_MAGIC, []testTlv{ + {COREDUMP_TLV_MEM, 0x100, seq(8, 0)}, + }) + + tests := []struct { + name string + core []byte + err string + }{ + {"empty file", nil, "Error reading: EOF"}, + {"short header", []byte{0xc3, 0x47, 0x0c, 0x69}, "Short read"}, + {"bad magic", buildCore(0x12345678, nil), + "Source file is not corefile"}, + {"short tlv header", append(buildCore(COREDUMP_MAGIC, nil), + COREDUMP_TLV_MEM, 0, 4), "Short read"}, + {"unknown tlv", buildCore(COREDUMP_MAGIC, []testTlv{ + {9, 0, seq(4, 0)}, + }), "Unknown TLV type"}, + {"tlv data truncated", valid[:len(valid)-3], "Short file"}, + {"tlv data missing", valid[:len(valid)-8], "Error reading: EOF"}, + {"regs not multiple of 4", buildCore(COREDUMP_MAGIC, []testTlv{ + {COREDUMP_TLV_REGS, 0, seq(6, 0)}, + }), "Invalid register area size"}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + _, _, err := convert(t, tt.core) + if err == nil { + t.Fatalf("expected error") + } + if err.Error() != tt.err { + t.Errorf("error = %q, want %q", err.Error(), tt.err) + } + }) + } +} + +func TestConvertMissingFiles(t *testing.T) { + dir := t.TempDir() + + _, err := ConvertFilenames(filepath.Join(dir, "nope"), + filepath.Join(dir, "out")) + if err == nil { + t.Errorf("expected error for missing source") + } + + src := filepath.Join(dir, "core") + if err := os.WriteFile(src, buildCore(COREDUMP_MAGIC, nil), 0644); err != nil { + t.Fatal(err) + } + _, err = ConvertFilenames(src, filepath.Join(dir, "nodir", "out")) + if err == nil { + t.Errorf("expected error for unwritable target") + } + + if err := NewCoreConvert().Convert(); err == nil || + err.Error() != "Missing file parameters" { + t.Errorf("Convert() without files: err = %v", err) + } +}
