Skip to content

Commit 550d7d6

Browse files
committed
fix: objc parsing (relative offset calculation bug + bounds checking)
1 parent 5a988ae commit 550d7d6

File tree

6 files changed

+112
-15
lines changed

6 files changed

+112
-15
lines changed

README.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,23 @@ func main() {
4242
}
4343
```
4444

45+
## Logging
46+
47+
This library uses Go's structured logging (`log/slog`) for diagnostic messages. By default, no logs are emitted. To see library diagnostics (warnings about malformed Mach-O data, etc.), set up an slog handler in your application:
48+
49+
```go
50+
import ("log/slog"
51+
"os"
52+
)
53+
54+
// Enable debug logging
55+
slog.SetDefault(slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{
56+
Level: slog.LevelWarn, // Or slog.LevelDebug for more verbose output
57+
})))
58+
```
59+
60+
This allows you to control go-macho's logging output in your application without it polluting stdout by default.
61+
4562
## License
4663

4764
MIT Copyright (c) 2020-2025 **blacktop**

objc.go

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1225,16 +1225,23 @@ func (f *File) forEachObjCMethod(methodListVMAddr uint64, handler func(uint64, o
12251225
method.NameVMAddr = uint64(methodVMAddr + int64(m.NameOffset))
12261226
}
12271227
} else {
1228-
nameVMAddr := uint64(methodVMAddr + int64(m.NameOffset))
1229-
method.NameVMAddr, err = f.GetPointerAtAddress(nameVMAddr)
1228+
// RelativePointer offsets are relative to the field address, not the struct base
1229+
nameFieldAddr := uint64(methodVMAddr + int64(unsafe.Offsetof(m.NameOffset)))
1230+
nameIndirectAddr := uint64(int64(nameFieldAddr) + int64(m.NameOffset))
1231+
method.NameVMAddr, err = f.GetPointerAtAddress(nameIndirectAddr)
12301232
if err != nil {
12311233
return fmt.Errorf("failed to read relative_method_t name pointer: %v", err)
12321234
}
12331235
}
12341236

1235-
method.NameLocationVMAddr = uint64(methodVMAddr+int64(m.NameOffset)) + uint64(unsafe.Offsetof(m.NameOffset))
1236-
method.TypesVMAddr = uint64(methodVMAddr+int64(m.TypesOffset)) + uint64(unsafe.Offsetof(m.TypesOffset))
1237-
method.ImpVMAddr = uint64(methodVMAddr+int64(m.ImpOffset)) + uint64(unsafe.Offsetof(m.ImpOffset))
1237+
method.NameLocationVMAddr = uint64(methodVMAddr + int64(unsafe.Offsetof(m.NameOffset)))
1238+
1239+
// RelativePointer offsets are relative to the field address, not the struct base
1240+
// This matches Apple's objc4 RelativePointer implementation: actual_address = &offset_field + offset
1241+
typesFieldAddr := methodVMAddr + int64(unsafe.Offsetof(m.TypesOffset))
1242+
impFieldAddr := methodVMAddr + int64(unsafe.Offsetof(m.ImpOffset))
1243+
method.TypesVMAddr = uint64(typesFieldAddr + int64(m.TypesOffset))
1244+
method.ImpVMAddr = uint64(impFieldAddr + int64(m.ImpOffset))
12381245

12391246
method.Name, err = f.getCStringWithFallback(method.NameVMAddr, "selector", false)
12401247
if err != nil {

types/objc/category.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package objc
33
import (
44
"bytes"
55
"fmt"
6+
"log/slog"
67
"strings"
78
)
89

@@ -71,6 +72,10 @@ func (c *Category) dump(verbose, addrs bool) string {
7172
continue
7273
}
7374
if verbose {
75+
if meth.Types == "" {
76+
slog.Warn("category class method has empty type encoding", "method", meth.Name, "category", c.Name, "typesVMAddr", meth.TypesVMAddr)
77+
continue
78+
}
7479
rtype, args := decodeMethodTypes(meth.Types)
7580
if addrs {
7681
s.WriteString(fmt.Sprintf("// %#x\n", meth.ImpVMAddr))
@@ -92,6 +97,10 @@ func (c *Category) dump(verbose, addrs bool) string {
9297
continue
9398
}
9499
if verbose {
100+
if meth.Types == "" {
101+
slog.Warn("category instance method has empty type encoding", "method", meth.Name, "category", c.Name, "typesVMAddr", meth.TypesVMAddr)
102+
continue
103+
}
95104
rtype, args := decodeMethodTypes(meth.Types)
96105
if addrs {
97106
s.WriteString(fmt.Sprintf("// %#x\n", meth.ImpVMAddr))

types/objc/class.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package objc
33
import (
44
"bytes"
55
"fmt"
6+
"log/slog"
67
"strings"
78
"text/tabwriter"
89
)
@@ -227,6 +228,10 @@ func (c *Class) dump(verbose, addrs bool) string {
227228
continue
228229
}
229230
if verbose {
231+
if meth.Types == "" {
232+
slog.Warn("class method has empty type encoding", "method", meth.Name, "class", c.Name, "typesVMAddr", meth.TypesVMAddr)
233+
continue
234+
}
230235
rtype, args := decodeMethodTypes(meth.Types)
231236
if addrs {
232237
s.WriteString(fmt.Sprintf("// %#x\n", meth.ImpVMAddr))
@@ -249,6 +254,10 @@ func (c *Class) dump(verbose, addrs bool) string {
249254
continue
250255
}
251256
if verbose {
257+
if meth.Types == "" {
258+
slog.Warn("instance method has empty type encoding", "method", meth.Name, "class", c.Name, "typesVMAddr", meth.TypesVMAddr)
259+
continue
260+
}
252261
rtype, args := decodeMethodTypes(meth.Types)
253262
if addrs {
254263
s.WriteString(fmt.Sprintf("// %#x\n", meth.ImpVMAddr))

types/objc/protocol.go

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package objc
22

33
import (
44
"fmt"
5+
"log/slog"
56
"strings"
67
)
78

@@ -86,6 +87,10 @@ func (p *Protocol) dump(verbose, addrs bool) string {
8687
if len(p.ClassMethods) > 0 {
8788
for _, meth := range p.ClassMethods {
8889
if verbose {
90+
if meth.Types == "" {
91+
slog.Warn("protocol class method has empty type encoding", "method", meth.Name, "protocol", p.Name, "typesVMAddr", meth.TypesVMAddr)
92+
continue
93+
}
8994
rtype, args := decodeMethodTypes(meth.Types)
9095
cMethods += fmt.Sprintf("+ %s\n", getMethodWithArgs(meth.Name, rtype, args))
9196
} else {
@@ -99,6 +104,10 @@ func (p *Protocol) dump(verbose, addrs bool) string {
99104
if len(p.InstanceMethods) > 0 {
100105
for _, meth := range p.InstanceMethods {
101106
if verbose {
107+
if meth.Types == "" {
108+
slog.Warn("protocol instance method has empty type encoding", "method", meth.Name, "protocol", p.Name, "typesVMAddr", meth.TypesVMAddr)
109+
continue
110+
}
102111
rtype, args := decodeMethodTypes(meth.Types)
103112
iMethods += fmt.Sprintf("- %s\n", getMethodWithArgs(meth.Name, rtype, args))
104113
} else {
@@ -126,6 +135,10 @@ func (p *Protocol) dump(verbose, addrs bool) string {
126135
if len(p.OptionalInstanceMethods) > 0 {
127136
for _, meth := range p.OptionalInstanceMethods {
128137
if verbose {
138+
if meth.Types == "" {
139+
slog.Warn("protocol optional instance method has empty type encoding", "method", meth.Name, "protocol", p.Name, "typesVMAddr", meth.TypesVMAddr)
140+
continue
141+
}
129142
rtype, args := decodeMethodTypes(meth.Types)
130143
optMethods += fmt.Sprintf("- %s\n", getMethodWithArgs(meth.Name, rtype, args))
131144
} else {

types/objc/type_encoding.go

Lines changed: 52 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -552,9 +552,15 @@ func decodeStructOrUnion(typ, kind string) string {
552552
}
553553

554554
func skipFirstType(typStr string) string {
555+
if len(typStr) == 0 {
556+
return ""
557+
}
555558
i := 0
556559
typ := []byte(typStr)
557560
for {
561+
if i >= len(typ) {
562+
return ""
563+
}
558564
switch typ[i] {
559565
case '+': /* gnu register */
560566
fallthrough
@@ -585,36 +591,72 @@ func skipFirstType(typStr string) string {
585591
i++ /* Blocks */
586592
} else if i+1 < len(typ) && typ[i+1] == '"' {
587593
i++
588-
for typ[i+1] != '"' {
594+
for i+1 < len(typ) && typ[i+1] != '"' {
589595
i++ /* Class */
590596
}
591597
i++
592598
}
593-
return string(typ[i+1:])
599+
if i+1 <= len(typ) {
600+
return string(typ[i+1:])
601+
}
602+
return ""
594603
case '!': /* vectors */
595604
i += 2
596-
for typ[i] == ',' || typ[i] >= '0' && typ[i] <= '9' {
605+
for i < len(typ) && (typ[i] == ',' || typ[i] >= '0' && typ[i] <= '9') {
597606
i++
598607
}
599-
return string(typ[i+subtypeUntil(string(typ[i:]), ']')+1:])
608+
if i < len(typ) {
609+
skip := subtypeUntil(string(typ[i:]), ']')
610+
if i+skip+1 <= len(typ) {
611+
return string(typ[i+skip+1:])
612+
}
613+
}
614+
return ""
600615
case '[': /* arrays */
601616
i++
602-
for typ[i] >= '0' && typ[i] <= '9' {
617+
for i < len(typ) && typ[i] >= '0' && typ[i] <= '9' {
603618
i++
604619
}
605-
return string(typ[i+subtypeUntil(string(typ[i:]), ']')+1:])
620+
if i < len(typ) {
621+
skip := subtypeUntil(string(typ[i:]), ']')
622+
if i+skip+1 <= len(typ) {
623+
return string(typ[i+skip+1:])
624+
}
625+
}
626+
return ""
606627
case '{': /* structures */
607628
i++
608-
return string(typ[i+subtypeUntil(string(typ[i:]), '}')+1:])
629+
if i < len(typ) {
630+
skip := subtypeUntil(string(typ[i:]), '}')
631+
if i+skip+1 <= len(typ) {
632+
return string(typ[i+skip+1:])
633+
}
634+
}
635+
return ""
609636
case '(': /* unions */
610637
i++
611-
return string(typ[i+subtypeUntil(string(typ[i:]), ')')+1:])
638+
if i < len(typ) {
639+
skip := subtypeUntil(string(typ[i:]), ')')
640+
if i+skip+1 <= len(typ) {
641+
return string(typ[i+skip+1:])
642+
}
643+
}
644+
return ""
612645
case '<': /* block func prototype */
613646
i++
614-
return string(typ[i+subtypeUntil(string(typ[i:]), '>')+1:])
647+
if i < len(typ) {
648+
skip := subtypeUntil(string(typ[i:]), '>')
649+
if i+skip+1 <= len(typ) {
650+
return string(typ[i+skip+1:])
651+
}
652+
}
653+
return ""
615654
default: /* basic types */
616655
i++
617-
return string(typ[i:])
656+
if i <= len(typ) {
657+
return string(typ[i:])
658+
}
659+
return ""
618660
}
619661
}
620662
}

0 commit comments

Comments
 (0)