fix(snapshot): decode dirents with aligned storage
- commit
- d8d4bf61ad0c8184bfc4701c8f94b31d044ed440
- author
- brice · · 2026-07-22 05:55 UTC
- parent
- fb7c7c4
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
2 files · +78 -9
M internal/adapters/snapshot/maintenance_cursor.go +30 -9
| @@ -150,18 +150,16 @@ func (r *Repository) readMaintenanceDirent(dir string, limit int, cursor *mainte | |||
| 150 | 150 | if err != nil { | |
| 151 | 151 | return nil, false, maintenanceDirectoryError("read maintenance directory", err) | |
| 152 | 152 | } | |
| 153 | + | if count < 0 || count > len(buffer) { | |
| 154 | + | return nil, false, syscall.EIO | |
| 155 | + | } | |
| 153 | 156 | if count == 0 { | |
| 154 | 157 | return entries, true, nil | |
| 155 | 158 | } | |
| 156 | 159 | for data := buffer[:count]; len(data) != 0 && (len(entries) < limit || drainMaintenanceDirentBatch()); { | |
| 157 | - | if len(data) < int(unsafe.Offsetof(syscall.Dirent{}.Name)) { | |
| 158 | - | return nil, false, syscall.EIO | |
| 159 | - | } | |
| 160 | - | record := (*syscall.Dirent)(unsafe.Pointer(&data[0])) | |
| 161 | - | reclen := int(record.Reclen) | |
| 162 | - | nameOffset := int(unsafe.Offsetof(syscall.Dirent{}.Name)) | |
| 163 | - | if reclen < nameOffset || reclen > len(data) { | |
| 164 | - | return nil, false, syscall.EIO | |
| 160 | + | record, nameBytes, reclen, decodeErr := decodeMaintenanceDirent(data) | |
| 161 | + | if decodeErr != nil { | |
| 162 | + | return nil, false, decodeErr | |
| 165 | 163 | } | |
| 166 | 164 | data = data[reclen:] | |
| 167 | 165 | // Advance past every raw record, including dot and disappeared entries. | |
| @@ -170,7 +168,6 @@ func (r *Repository) readMaintenanceDirent(dir string, limit int, cursor *mainte | |||
| 170 | 168 | return nil, false, maintenanceDirectoryError("tell maintenance directory", cookieErr) | |
| 171 | 169 | } | |
| 172 | 170 | cursor.offset = offset | |
| 173 | - | nameBytes := unsafe.Slice((*byte)(unsafe.Pointer(&record.Name[0])), reclen-nameOffset) | |
| 174 | 171 | if end := strings.IndexByte(string(nameBytes), 0); end >= 0 { | |
| 175 | 172 | nameBytes = nameBytes[:end] | |
| 176 | 173 | } | |
| @@ -204,6 +201,30 @@ func (r *Repository) readMaintenanceDirent(dir string, limit int, cursor *mainte | |||
| 204 | 201 | return entries, false, nil | |
| 205 | 202 | } | |
| 206 | 203 | ||
| 204 | + | // decodeMaintenanceDirent copies a bounded raw record into aligned storage | |
| 205 | + | // before reading its fields. syscall.ReadDirent writes to a byte buffer, whose | |
| 206 | + | // start and record boundaries are not guaranteed to satisfy syscall.Dirent's | |
| 207 | + | // alignment requirements on Darwin. | |
| 208 | + | func decodeMaintenanceDirent(data []byte) (*syscall.Dirent, []byte, int, error) { | |
| 209 | + | nameOffset := int(unsafe.Offsetof(syscall.Dirent{}.Name)) | |
| 210 | + | if len(data) < nameOffset { | |
| 211 | + | return nil, nil, 0, syscall.EIO | |
| 212 | + | } | |
| 213 | + | ||
| 214 | + | var record syscall.Dirent | |
| 215 | + | header := unsafe.Slice((*byte)(unsafe.Pointer(&record)), nameOffset) | |
| 216 | + | copy(header, data[:nameOffset]) | |
| 217 | + | reclen := int(record.Reclen) | |
| 218 | + | if reclen < nameOffset || reclen > len(data) || reclen > int(unsafe.Sizeof(record)) { | |
| 219 | + | return nil, nil, 0, syscall.EIO | |
| 220 | + | } | |
| 221 | + | ||
| 222 | + | recordBytes := unsafe.Slice((*byte)(unsafe.Pointer(&record)), reclen) | |
| 223 | + | copy(recordBytes, data[:reclen]) | |
| 224 | + | name := unsafe.Slice((*byte)(unsafe.Pointer(&record.Name[0])), reclen-nameOffset) | |
| 225 | + | return &record, name, reclen, nil | |
| 226 | + | } | |
| 227 | + | ||
| 207 | 228 | func (r *Repository) requeueMaintenanceEntries(dir, purpose string, entries []maintenanceDirEntry) { | |
| 208 | 229 | if len(entries) == 0 { | |
| 209 | 230 | return | |
| @@ -150,18 +150,16 @@ func (r *Repository) readMaintenanceDirent(dir string, limit int, cursor *mainte | |||||
| 150 | if err != nil { | 150 | if err != nil { | ||
| 151 | return nil, false, maintenanceDirectoryError("read maintenance directory", err) | 151 | return nil, false, maintenanceDirectoryError("read maintenance directory", err) | ||
| 152 | } | 152 | } | ||
| 153 | + | if count < 0 || count > len(buffer) { | |||
| 154 | + | return nil, false, syscall.EIO | |||
| 155 | + | } | |||
| 153 | if count == 0 { | 156 | if count == 0 { | ||
| 154 | return entries, true, nil | 157 | return entries, true, nil | ||
| 155 | } | 158 | } | ||
| 156 | for data := buffer[:count]; len(data) != 0 && (len(entries) < limit || drainMaintenanceDirentBatch()); { | 159 | for data := buffer[:count]; len(data) != 0 && (len(entries) < limit || drainMaintenanceDirentBatch()); { | ||
| 157 | - | if len(data) < int(unsafe.Offsetof(syscall.Dirent{}.Name)) { | 160 | + | record, nameBytes, reclen, decodeErr := decodeMaintenanceDirent(data) |
| 158 | - | return nil, false, syscall.EIO | 161 | + | if decodeErr != nil { |
| 159 | - | } | 162 | + | return nil, false, decodeErr |
| 160 | - | record := (*syscall.Dirent)(unsafe.Pointer(&data[0])) | |||
| 161 | - | reclen := int(record.Reclen) | |||
| 162 | - | nameOffset := int(unsafe.Offsetof(syscall.Dirent{}.Name)) | |||
| 163 | - | if reclen < nameOffset || reclen > len(data) { | |||
| 164 | - | return nil, false, syscall.EIO | |||
| 165 | } | 163 | } | ||
| 166 | data = data[reclen:] | 164 | data = data[reclen:] | ||
| 167 | // Advance past every raw record, including dot and disappeared entries. | 165 | // Advance past every raw record, including dot and disappeared entries. | ||
| @@ -170,7 +168,6 @@ func (r *Repository) readMaintenanceDirent(dir string, limit int, cursor *mainte | |||||
| 170 | return nil, false, maintenanceDirectoryError("tell maintenance directory", cookieErr) | 168 | return nil, false, maintenanceDirectoryError("tell maintenance directory", cookieErr) | ||
| 171 | } | 169 | } | ||
| 172 | cursor.offset = offset | 170 | cursor.offset = offset | ||
| 173 | - | nameBytes := unsafe.Slice((*byte)(unsafe.Pointer(&record.Name[0])), reclen-nameOffset) | |||
| 174 | if end := strings.IndexByte(string(nameBytes), 0); end >= 0 { | 171 | if end := strings.IndexByte(string(nameBytes), 0); end >= 0 { | ||
| 175 | nameBytes = nameBytes[:end] | 172 | nameBytes = nameBytes[:end] | ||
| 176 | } | 173 | } | ||
| @@ -204,6 +201,30 @@ func (r *Repository) readMaintenanceDirent(dir string, limit int, cursor *mainte | |||||
| 204 | return entries, false, nil | 201 | return entries, false, nil | ||
| 205 | } | 202 | } | ||
| 206 | 203 | ||||
| 204 | + | // decodeMaintenanceDirent copies a bounded raw record into aligned storage | |||
| 205 | + | // before reading its fields. syscall.ReadDirent writes to a byte buffer, whose | |||
| 206 | + | // start and record boundaries are not guaranteed to satisfy syscall.Dirent's | |||
| 207 | + | // alignment requirements on Darwin. | |||
| 208 | + | func decodeMaintenanceDirent(data []byte) (*syscall.Dirent, []byte, int, error) { | |||
| 209 | + | nameOffset := int(unsafe.Offsetof(syscall.Dirent{}.Name)) | |||
| 210 | + | if len(data) < nameOffset { | |||
| 211 | + | return nil, nil, 0, syscall.EIO | |||
| 212 | + | } | |||
| 213 | + | ||||
| 214 | + | var record syscall.Dirent | |||
| 215 | + | header := unsafe.Slice((*byte)(unsafe.Pointer(&record)), nameOffset) | |||
| 216 | + | copy(header, data[:nameOffset]) | |||
| 217 | + | reclen := int(record.Reclen) | |||
| 218 | + | if reclen < nameOffset || reclen > len(data) || reclen > int(unsafe.Sizeof(record)) { | |||
| 219 | + | return nil, nil, 0, syscall.EIO | |||
| 220 | + | } | |||
| 221 | + | ||||
| 222 | + | recordBytes := unsafe.Slice((*byte)(unsafe.Pointer(&record)), reclen) | |||
| 223 | + | copy(recordBytes, data[:reclen]) | |||
| 224 | + | name := unsafe.Slice((*byte)(unsafe.Pointer(&record.Name[0])), reclen-nameOffset) | |||
| 225 | + | return &record, name, reclen, nil | |||
| 226 | + | } | |||
| 227 | + | ||||
| 207 | func (r *Repository) requeueMaintenanceEntries(dir, purpose string, entries []maintenanceDirEntry) { | 228 | func (r *Repository) requeueMaintenanceEntries(dir, purpose string, entries []maintenanceDirEntry) { | ||
| 208 | if len(entries) == 0 { | 229 | if len(entries) == 0 { | ||
| 209 | return | 230 | return | ||
M internal/adapters/snapshot/maintenance_test.go +48 -0
| @@ -96,6 +96,54 @@ func TestReadMaintenanceDirentRejectsRecordShorterThanNameOffset(t *testing.T) { | |||
| 96 | 96 | } | |
| 97 | 97 | } | |
| 98 | 98 | ||
| 99 | + | func TestDecodeMaintenanceDirentBoundsRecords(t *testing.T) { | |
| 100 | + | nameOffset := int(unsafe.Offsetof(syscall.Dirent{}.Name)) | |
| 101 | + | reclenOffset := unsafe.Offsetof(syscall.Dirent{}.Reclen) | |
| 102 | + | valid := make([]byte, nameOffset+len("entry")+1) | |
| 103 | + | binary.NativeEndian.PutUint16(valid[reclenOffset:], uint16(len(valid))) | |
| 104 | + | copy(valid[nameOffset:], "entry") | |
| 105 | + | ||
| 106 | + | tests := []struct { | |
| 107 | + | name string | |
| 108 | + | data []byte | |
| 109 | + | wantName string | |
| 110 | + | wantErr bool | |
| 111 | + | }{ | |
| 112 | + | {name: "truncated header", data: make([]byte, nameOffset-1), wantErr: true}, | |
| 113 | + | {name: "short record", data: append([]byte(nil), valid...), wantErr: true}, | |
| 114 | + | {name: "truncated record", data: valid[:nameOffset], wantErr: true}, | |
| 115 | + | {name: "record exceeds aligned storage", data: make([]byte, int(unsafe.Sizeof(syscall.Dirent{}))+1), wantErr: true}, | |
| 116 | + | {name: "valid", data: valid, wantName: "entry"}, | |
| 117 | + | } | |
| 118 | + | binary.NativeEndian.PutUint16(tests[1].data[reclenOffset:], uint16(nameOffset-1)) | |
| 119 | + | binary.NativeEndian.PutUint16(tests[2].data[reclenOffset:], uint16(len(valid))) | |
| 120 | + | binary.NativeEndian.PutUint16(tests[3].data[reclenOffset:], uint16(len(tests[3].data))) | |
| 121 | + | ||
| 122 | + | for _, tc := range tests { | |
| 123 | + | t.Run(tc.name, func(t *testing.T) { | |
| 124 | + | record, name, reclen, err := decodeMaintenanceDirent(tc.data) | |
| 125 | + | if tc.wantErr { | |
| 126 | + | if !errors.Is(err, syscall.EIO) { | |
| 127 | + | t.Fatalf("decodeMaintenanceDirent() error = %v, want EIO", err) | |
| 128 | + | } | |
| 129 | + | return | |
| 130 | + | } | |
| 131 | + | if err != nil { | |
| 132 | + | t.Fatal(err) | |
| 133 | + | } | |
| 134 | + | if record == nil { | |
| 135 | + | t.Fatal("decodeMaintenanceDirent() record = nil") | |
| 136 | + | } | |
| 137 | + | if reclen != len(valid) { | |
| 138 | + | t.Fatalf("decodeMaintenanceDirent() reclen = %d, want %d", reclen, len(valid)) | |
| 139 | + | } | |
| 140 | + | if got := string(name[:len(tc.wantName)]); got != tc.wantName { | |
| 141 | + | t.Fatalf("decodeMaintenanceDirent() name = %q, want %q", got, tc.wantName) | |
| 142 | + | } | |
| 143 | + | }) | |
| 144 | + | } | |
| 145 | + | } | |
| 146 | + | ||
| 99 | 147 | type fakeMaintenanceDirectory struct { | |
| 100 | 148 | data []byte | |
| 101 | 149 | seekOffset int64 | |
| @@ -96,6 +96,54 @@ func TestReadMaintenanceDirentRejectsRecordShorterThanNameOffset(t *testing.T) { | |||||
| 96 | } | 96 | } | ||
| 97 | } | 97 | } | ||
| 98 | 98 | ||||
| 99 | + | func TestDecodeMaintenanceDirentBoundsRecords(t *testing.T) { | |||
| 100 | + | nameOffset := int(unsafe.Offsetof(syscall.Dirent{}.Name)) | |||
| 101 | + | reclenOffset := unsafe.Offsetof(syscall.Dirent{}.Reclen) | |||
| 102 | + | valid := make([]byte, nameOffset+len("entry")+1) | |||
| 103 | + | binary.NativeEndian.PutUint16(valid[reclenOffset:], uint16(len(valid))) | |||
| 104 | + | copy(valid[nameOffset:], "entry") | |||
| 105 | + | ||||
| 106 | + | tests := []struct { | |||
| 107 | + | name string | |||
| 108 | + | data []byte | |||
| 109 | + | wantName string | |||
| 110 | + | wantErr bool | |||
| 111 | + | }{ | |||
| 112 | + | {name: "truncated header", data: make([]byte, nameOffset-1), wantErr: true}, | |||
| 113 | + | {name: "short record", data: append([]byte(nil), valid...), wantErr: true}, | |||
| 114 | + | {name: "truncated record", data: valid[:nameOffset], wantErr: true}, | |||
| 115 | + | {name: "record exceeds aligned storage", data: make([]byte, int(unsafe.Sizeof(syscall.Dirent{}))+1), wantErr: true}, | |||
| 116 | + | {name: "valid", data: valid, wantName: "entry"}, | |||
| 117 | + | } | |||
| 118 | + | binary.NativeEndian.PutUint16(tests[1].data[reclenOffset:], uint16(nameOffset-1)) | |||
| 119 | + | binary.NativeEndian.PutUint16(tests[2].data[reclenOffset:], uint16(len(valid))) | |||
| 120 | + | binary.NativeEndian.PutUint16(tests[3].data[reclenOffset:], uint16(len(tests[3].data))) | |||
| 121 | + | ||||
| 122 | + | for _, tc := range tests { | |||
| 123 | + | t.Run(tc.name, func(t *testing.T) { | |||
| 124 | + | record, name, reclen, err := decodeMaintenanceDirent(tc.data) | |||
| 125 | + | if tc.wantErr { | |||
| 126 | + | if !errors.Is(err, syscall.EIO) { | |||
| 127 | + | t.Fatalf("decodeMaintenanceDirent() error = %v, want EIO", err) | |||
| 128 | + | } | |||
| 129 | + | return | |||
| 130 | + | } | |||
| 131 | + | if err != nil { | |||
| 132 | + | t.Fatal(err) | |||
| 133 | + | } | |||
| 134 | + | if record == nil { | |||
| 135 | + | t.Fatal("decodeMaintenanceDirent() record = nil") | |||
| 136 | + | } | |||
| 137 | + | if reclen != len(valid) { | |||
| 138 | + | t.Fatalf("decodeMaintenanceDirent() reclen = %d, want %d", reclen, len(valid)) | |||
| 139 | + | } | |||
| 140 | + | if got := string(name[:len(tc.wantName)]); got != tc.wantName { | |||
| 141 | + | t.Fatalf("decodeMaintenanceDirent() name = %q, want %q", got, tc.wantName) | |||
| 142 | + | } | |||
| 143 | + | }) | |||
| 144 | + | } | |||
| 145 | + | } | |||
| 146 | + | ||||
| 99 | type fakeMaintenanceDirectory struct { | 147 | type fakeMaintenanceDirectory struct { | ||
| 100 | data []byte | 148 | data []byte | ||
| 101 | seekOffset int64 | 149 | seekOffset int64 | ||