-
Notifications
You must be signed in to change notification settings - Fork 1
Description
When butler pushes a macOS .app bundle, the wharf pipeline decomposes it into files tracked by the TLC container format. The TLC File message only stores path, mode, size, and offset. Extended attributes (xattrs) are silently dropped.
This breaks macOS code signing and notarization. A signed/notarized .app that works perfectly when zipped manually becomes "damaged" after a butler push/download cycle, because the code signing metadata stored in xattrs is lost. Users see:
".app is damaged and can't be opened"
This is reported in butler#234 and butler#274. The current workaround is to zip the .app manually and upload through the itch.io web UI, which bypasses the wharf pipeline entirely.
Proposed Change
Add an xattrs field to the TLC protobuf messages so extended attributes are preserved through the walk, diff, patch, and apply lifecycle.
1. Proto Schema (tlc/tlc.proto)
Add a map<string, bytes> field to File, Dir, and Symlink:
message File {
string path = 1;
uint32 mode = 2;
int64 size = 3;
int64 offset = 4;
map<string, bytes> xattrs = 5;
}
message Dir {
string path = 1;
uint32 mode = 2;
map<string, bytes> xattrs = 3;
}
message Symlink {
string path = 1;
uint32 mode = 2;
string dest = 3;
map<string, bytes> xattrs = 4;
}This is backwards-compatible in proto3: old readers ignore unknown fields, and old patch/signature files simply won't have xattrs populated.
2. Walking (tlc/walk.go)
In WalkDir, after collecting mode and size, read xattrs from the filesystem:
xattrs, err := xattr.LList(fullPath)
if err == nil && len(xattrs) > 0 {
file.Xattrs = make(map[string][]byte)
for _, name := range xattrs {
val, err := xattr.LGet(fullPath, name)
if err == nil {
file.Xattrs[name] = val
}
}
}Same for Dir and Symlink entries. WalkZip would leave the field empty since zip archives don't carry xattrs.
A library like github.com/pkg/xattr handles cross-platform concerns. On platforms without xattr support, list/get are no-ops.
3. Applying xattrs: RestoreXattrs method on Container
A new Container.RestoreXattrs(basePath) method (alongside the existing Prepare) would walk all files, dirs, and symlinks and call xattr.LSet for each stored attribute:
func (c *Container) RestoreXattrs(basePath string) error {
for _, f := range c.Files {
for name, val := range f.Xattrs {
xattr.LSet(filepath.Join(basePath, f.Path), name, val)
}
}
// same for c.Dirs and c.Symlinks
}This must NOT be called during Prepare(). Prepare creates empty/pre-allocated files before patch content is written. Setting xattrs at that point would be pointless (contents haven't been written yet) and could cause issues if the OS validates xattrs against file contents.
Instead, RestoreXattrs must be called after patch application is complete, specifically after the bowl commits. The call sites are in butler (not lake or wharf):
-
butler/cmd/apply/apply.go: afterbwl.Commit()(line 193). The source container is available viap.GetSourceContainer()(line 198):err = bwl.Commit() // ... sourceContainer := p.GetSourceContainer() err = sourceContainer.RestoreXattrs(outputPath)
-
butler/cmd/operate/upgrade.go: afterbowl.Commit()(line 259). The source container is available viap.GetSourceContainer()(line 264):err = bowl.Commit() // ... sourceContainer := p.GetSourceContainer() err = sourceContainer.RestoreXattrs(params.InstallFolder)
4. Wharf (diff, patch, signature): no changes needed
The wharf library serializes the TLC Container via proto.Marshal/proto.Unmarshal through wire.WriteMessage/wire.ReadMessage. The new xattr field flows through automatically in:
pwr/diff.go: writes both TargetContainer and SourceContainer into the patch filepwr/patcher/patcher.go: reads both containers back from the patch filepwr/sign.go: writes/reads the container in signature files
No code changes required in wharf.
5. Butler push: no changes needed
Butler's push logic passes the container through as-is. The xattr data will be captured during walk (step 2) and serialized into the patch automatically (step 4).
Open Questions
- Filter xattrs? Should we store all xattrs, or filter to a known set (e.g.
com.apple.*)? Storing all is simpler and more future-proof, but increases patch size for files with many xattrs. - Interaction with FixPermissions:
FixPermissionsmodifiesmodebits by detecting executables via magic bytes. If the original.appwas signed with specific permission bits, changing them also invalidates the signature. ShouldFixPermissionsbe skipped for files inside.appbundles, or is that a separate concern? - Dir and Symlink xattrs: Are xattrs on directories and symlinks relevant for code signing? If so, all three message types need the field. If not,
Filealone may suffice.
References
- butler#234: Original report, signed/notarized
.appis "damaged" after butler push - butler#274: Related report about permission issues
- itch.io macOS docs: Current guidance for
.appdistribution