From 1e5882c55649678760d8640f8a0de812252ee803 Mon Sep 17 00:00:00 2001 From: Hannes Achleitner Date: Sun, 27 Sep 2026 21:45:05 +0200 Subject: [PATCH 1/4] Migrate GTFilter off the deprecated apply callback --- ObjectiveGit/GTFilter.m | 74 ++++++++++++++++++++++++++++++++++++----- 1 file changed, 66 insertions(+), 8 deletions(-) diff --git a/ObjectiveGit/GTFilter.m b/ObjectiveGit/GTFilter.m index 56b5eb50..2e6af56b 100644 --- a/ObjectiveGit/GTFilter.m +++ b/ObjectiveGit/GTFilter.m @@ -13,7 +13,6 @@ #import "git2/errors.h" #import "git2/sys/filter.h" -#import "git2/deprecated.h" NSString * const GTFilterErrorDomain = @"GTFilterErrorDomain"; @@ -57,7 +56,7 @@ - (instancetype)initWithName:(NSString *)name attributes:(NSString *)attributes _filter.version = GIT_FILTER_VERSION; _filter.attributes = attributes.UTF8String; - _filter.apply = >FilterApply; + _filter.stream = >FilterStreamNew; _name = [name copy]; _applyBlock = [applyBlock copy]; @@ -103,14 +102,73 @@ static int GTFilterCheck(git_filter *filter, void **payload, const git_filter_so return accept ? 0 : GIT_PASSTHROUGH; } -static int GTFilterApply(git_filter *filter, void **payload, git_buf *to, const git_buf *from, const git_filter_source *src) { - GTFilter *self = GTFiltersGitFilterToRegisteredFilters[[NSValue valueWithPointer:filter]]; - NSData *fromData = [NSData dataWithBytesNoCopy:from->ptr length:from->size freeWhenDone:NO]; +// Backing storage for the `git_writestream` handed to libgit2 by +// `GTFilterStreamNew`. `git_writestream` structs are used with C-style +// "inheritance": `parent` must be the first field so a `GTFilterWriteStream *` +// can be reinterpreted as a `git_writestream *`. +// +// The apply block operates on a complete buffer (an `NSData`), not a stream, +// so incoming chunks are accumulated in `bufferRef` and only handed to the +// block once `close` is called (i.e. once all data has been written). +typedef struct { + git_writestream parent; + void *filterRef; // (GTFilter *), retained via CFBridgingRetain + void *sourceRef; // (GTFilterSource *), retained via CFBridgingRetain + void *bufferRef; // (NSMutableData *), retained via CFBridgingRetain + void **payload; + git_writestream *next; +} GTFilterWriteStream; + +static int GTFilterStreamWrite(git_writestream *s, const char *buffer, size_t len) { + GTFilterWriteStream *stream = (GTFilterWriteStream *)s; + NSMutableData *data = (__bridge NSMutableData *)stream->bufferRef; + [data appendBytes:buffer length:len]; + return 0; +} + +static int GTFilterStreamClose(git_writestream *s) { + GTFilterWriteStream *stream = (GTFilterWriteStream *)s; + GTFilter *filter = (__bridge GTFilter *)stream->filterRef; + GTFilterSource *source = (__bridge GTFilterSource *)stream->sourceRef; + NSData *fromData = (__bridge NSData *)stream->bufferRef; + BOOL applied = YES; - NSData *toData = self.applyBlock(payload, fromData, [[GTFilterSource alloc] initWithGitFilterSource:src], &applied); - if (!applied) return GIT_PASSTHROUGH; + NSData *toData = filter.applyBlock(stream->payload, fromData, source, &applied); + NSData *outputData = applied ? toData : fromData; + + git_writestream *next = stream->next; + int result = next->write(next, outputData.bytes, outputData.length); + if (result < 0) return result; + + return next->close(next); +} + +static void GTFilterStreamFree(git_writestream *s) { + GTFilterWriteStream *stream = (GTFilterWriteStream *)s; + CFBridgingRelease(stream->filterRef); + CFBridgingRelease(stream->sourceRef); + CFBridgingRelease(stream->bufferRef); + free(stream); +} + +static int GTFilterStreamNew(git_writestream **out, git_filter *filter, void **payload, const git_filter_source *src, git_writestream *next) { + GTFilter *self = GTFiltersGitFilterToRegisteredFilters[[NSValue valueWithPointer:filter]]; + GTFilterSource *source = [[GTFilterSource alloc] initWithGitFilterSource:src]; + NSCAssert(source != nil, @"Unexpected nil filter source"); + + GTFilterWriteStream *stream = calloc(1, sizeof(GTFilterWriteStream)); + if (stream == NULL) return GIT_ERROR; + + stream->parent.write = GTFilterStreamWrite; + stream->parent.close = GTFilterStreamClose; + stream->parent.free = GTFilterStreamFree; + stream->filterRef = (void *)CFBridgingRetain(self); + stream->sourceRef = (void *)CFBridgingRetain(source); + stream->bufferRef = (void *)CFBridgingRetain([NSMutableData data]); + stream->payload = payload; + stream->next = next; - git_buf_set(to, toData.bytes, toData.length); + *out = (git_writestream *)stream; return 0; } From 71dbb12291482b5c3cf68d4e8d02ce9614cbfe98 Mon Sep 17 00:00:00 2001 From: Hannes Achleitner Date: Mon, 28 Sep 2026 07:59:03 +0200 Subject: [PATCH 2/4] Release resources and free stream in GTFilter Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- ObjectiveGit/GTFilter.m | 1 + 1 file changed, 1 insertion(+) diff --git a/ObjectiveGit/GTFilter.m b/ObjectiveGit/GTFilter.m index 2e6af56b..da769244 100644 --- a/ObjectiveGit/GTFilter.m +++ b/ObjectiveGit/GTFilter.m @@ -148,6 +148,7 @@ static void GTFilterStreamFree(git_writestream *s) { CFBridgingRelease(stream->filterRef); CFBridgingRelease(stream->sourceRef); CFBridgingRelease(stream->bufferRef); + stream->next->free(stream->next); free(stream); } From 2868f6cc0f7d2e7fb08693ba6220d39cbd8070bc Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 28 Sep 2026 06:21:46 +0000 Subject: [PATCH 3/4] Add GTFilterStreamNew declaration Co-authored-by: hannesa2 <3314607+hannesa2@users.noreply.github.com> --- ObjectiveGit/GTFilter.m | 2 ++ 1 file changed, 2 insertions(+) diff --git a/ObjectiveGit/GTFilter.m b/ObjectiveGit/GTFilter.m index da769244..9e65fb40 100644 --- a/ObjectiveGit/GTFilter.m +++ b/ObjectiveGit/GTFilter.m @@ -21,6 +21,8 @@ static NSMutableDictionary *GTFiltersNameToRegisteredFilters = nil; static NSMutableDictionary *GTFiltersGitFilterToRegisteredFilters = nil; +static int GTFilterStreamNew(git_writestream **out, git_filter *filter, void **payload, const git_filter_source *src, git_writestream *next); + @interface GTFilter () { git_filter _filter; } From 7cc65e9ba8767e4abfc1121dff5f8832a23a967d Mon Sep 17 00:00:00 2001 From: Hannes Achleitner Date: Mon, 28 Sep 2026 08:48:08 +0200 Subject: [PATCH 4/4] Fix double free of next writestream in GTFilterStreamFree libgit2's own pipeline (filter_streams_free in filter.c) already frees every writestream in the chain exactly once, including next. Its own buffered_stream_free wrapper (used by the built-in crlf/ident filters) deliberately does not free its target/next stream for this reason. Freeing next here as well causes a double free. --- ObjectiveGit/GTFilter.m | 1 - 1 file changed, 1 deletion(-) diff --git a/ObjectiveGit/GTFilter.m b/ObjectiveGit/GTFilter.m index 9e65fb40..3cdd9cfa 100644 --- a/ObjectiveGit/GTFilter.m +++ b/ObjectiveGit/GTFilter.m @@ -150,7 +150,6 @@ static void GTFilterStreamFree(git_writestream *s) { CFBridgingRelease(stream->filterRef); CFBridgingRelease(stream->sourceRef); CFBridgingRelease(stream->bufferRef); - stream->next->free(stream->next); free(stream); }