Skip to content

Fix memory leak in avifImageSplitGrid() on partial initialization failure#3092

Merged
wantehchang merged 3 commits intoAOMediaCodec:mainfrom
nidu-ninja:avifImageSplitGrid-cleanup-on-failure
Mar 27, 2026
Merged

Fix memory leak in avifImageSplitGrid() on partial initialization failure#3092
wantehchang merged 3 commits intoAOMediaCodec:mainfrom
nidu-ninja:avifImageSplitGrid-cleanup-on-failure

Conversation

@nidu-ninja
Copy link
Copy Markdown
Contributor

This patch fixes a memory leak in avifImageSplitGrid() that occurs when the function exits early due to an error after allocating one or more grid cells.

The function allocates grid cell images incrementally inside nested loops and performs several operations that may fail (such as avifImageSetViewRect(), avifGainMapCreate(), or metadata copy operations). If any of these steps fail, the function previously returned immediately without destroying already allocated cells.

This patch introduces centralized cleanup logic that ensures all previously created grid cells are destroyed before returning on failure.

The change preserves the existing API and behavior while ensuring correct resource cleanup.

gridCells[i] = NULL;
}
}
}
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

NiDU-NINJA: Thank you for the patch. I looked into this issue. I found that the callers of avifImageSplitGrid() are responsible for destroying the images in the gridCells array if avifImageSplitGrid() fails. So this patch is not needed.

The comment for avifImageSplitGrid() says:

// Splits an image into a grid of cells, including its gain map, if any.
// The returned cells must be destroyed with avifImageDestroy().

We can clarify the second sentence to note that it also applies when the function fails.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

NiDU-NINJA: I just realized that having the function destroy the images in the gridCells array before returning AVIF_FALSE is overall a win because we can delete the corresponding code in two callers. The new behavior is also more intuitive because the function doesn't return any cells on failure.

Please edit the commit message because there is no memory leak in the current code.

Please apply the following patch:

diff --git a/apps/avifgainmaputil/imageio.cc b/apps/avifgainmaputil/imageio.cc
index 9a7e4e88..d133e231 100644
--- a/apps/avifgainmaputil/imageio.cc
+++ b/apps/avifgainmaputil/imageio.cc
@@ -105,11 +105,6 @@ avifResult WriteAvifGrid(const avifImage* image, int grid_cols, int grid_rows,
   std::vector<avifImage*> grid_cells_ptrs(grid_cell_count);
   if (!avifImageSplitGrid(image, grid_cols, grid_rows,
                           grid_cells_ptrs.data())) {
-    for (avifImage* img : grid_cells_ptrs) {
-      if (img) {
-        avifImageDestroy(img);
-      }
-    }
     return AVIF_RESULT_UNKNOWN_ERROR;
   }
   // Take ownership of the pointers returned by avifImageSplitGrid.
diff --git a/apps/shared/avifutil.c b/apps/shared/avifutil.c
index 702c95d4..d2828a46 100644
--- a/apps/shared/avifutil.c
+++ b/apps/shared/avifutil.c
@@ -598,11 +598,6 @@ avifBool avifImageSplitGrid(const avifImage * gridSplitImage, uint32_t gridCols,
             goto cleanup;
         }
         if (!avifImageSplitGrid(gridSplitImage->gainMap->image, gridCols, gridRows, gainMapGridCells)) {
-            for (uint32_t i = 0; i < gridCols * gridRows; ++i) {
-                if (gainMapGridCells[i]) {
-                    avifImageDestroy(gainMapGridCells[i]);
-                }
-            }
             free(gainMapGridCells);
             goto cleanup;
         }
@@ -655,10 +650,8 @@ avifBool avifImageSplitGrid(const avifImage * gridSplitImage, uint32_t gridCols,
 cleanup:
     if (!success) {
         for (uint32_t i = 0; i < createdCells; ++i) {
-            if (gridCells && gridCells[i]) {
-                avifImageDestroy(gridCells[i]);
-                gridCells[i] = NULL;
-            }
+            avifImageDestroy(gridCells[i]);
+            gridCells[i] = NULL;
         }
     }
     return success;
@@ -821,4 +814,4 @@ avifResult avifApplyTransforms(avifRGBImage * dstView, avifRGBImage * srcImage,
         }
     }
     return AVIF_RESULT_OK;
-}
\ No newline at end of file
+}

gridCells[i] = NULL;
}
}
}
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

NiDU-NINJA: I just realized that having the function destroy the images in the gridCells array before returning AVIF_FALSE is overall a win because we can delete the corresponding code in two callers. The new behavior is also more intuitive because the function doesn't return any cells on failure.

Please edit the commit message because there is no memory leak in the current code.

Please apply the following patch:

diff --git a/apps/avifgainmaputil/imageio.cc b/apps/avifgainmaputil/imageio.cc
index 9a7e4e88..d133e231 100644
--- a/apps/avifgainmaputil/imageio.cc
+++ b/apps/avifgainmaputil/imageio.cc
@@ -105,11 +105,6 @@ avifResult WriteAvifGrid(const avifImage* image, int grid_cols, int grid_rows,
   std::vector<avifImage*> grid_cells_ptrs(grid_cell_count);
   if (!avifImageSplitGrid(image, grid_cols, grid_rows,
                           grid_cells_ptrs.data())) {
-    for (avifImage* img : grid_cells_ptrs) {
-      if (img) {
-        avifImageDestroy(img);
-      }
-    }
     return AVIF_RESULT_UNKNOWN_ERROR;
   }
   // Take ownership of the pointers returned by avifImageSplitGrid.
diff --git a/apps/shared/avifutil.c b/apps/shared/avifutil.c
index 702c95d4..d2828a46 100644
--- a/apps/shared/avifutil.c
+++ b/apps/shared/avifutil.c
@@ -598,11 +598,6 @@ avifBool avifImageSplitGrid(const avifImage * gridSplitImage, uint32_t gridCols,
             goto cleanup;
         }
         if (!avifImageSplitGrid(gridSplitImage->gainMap->image, gridCols, gridRows, gainMapGridCells)) {
-            for (uint32_t i = 0; i < gridCols * gridRows; ++i) {
-                if (gainMapGridCells[i]) {
-                    avifImageDestroy(gainMapGridCells[i]);
-                }
-            }
             free(gainMapGridCells);
             goto cleanup;
         }
@@ -655,10 +650,8 @@ avifBool avifImageSplitGrid(const avifImage * gridSplitImage, uint32_t gridCols,
 cleanup:
     if (!success) {
         for (uint32_t i = 0; i < createdCells; ++i) {
-            if (gridCells && gridCells[i]) {
-                avifImageDestroy(gridCells[i]);
-                gridCells[i] = NULL;
-            }
+            avifImageDestroy(gridCells[i]);
+            gridCells[i] = NULL;
         }
     }
     return success;
@@ -821,4 +814,4 @@ avifResult avifApplyTransforms(avifRGBImage * dstView, avifRGBImage * srcImage,
         }
     }
     return AVIF_RESULT_OK;
-}
\ No newline at end of file
+}

cleanup:
if (!success) {
for (uint32_t i = 0; i < createdCells; ++i) {
if (gridCells && gridCells[i]) {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This check is not needed. (See patch below.)

  • This function assumes gridCells is not null. This is the API contract.
  • gridCells[i] cannot possibly be null.

@nidu-ninja
Copy link
Copy Markdown
Contributor Author

@wantehchang i have done the changes as you said

@wantehchang wantehchang merged commit 35f24ce into AOMediaCodec:main Mar 27, 2026
25 checks passed
}
const avifBool hasGainMap = gridSplitImage->gainMap && gridSplitImage->gainMap->image;

uint32_t createdCells = 0;
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I moved the declaration of createdCells right before the nested for loops that increment it.

goto cleanup;
}
gridCells[gridIndex] = cellImage;
assert(gridIndex == createdCells);
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added an assertion here to verify that gridIndex is consecutive. The cleanup code depends on this property.

wantehchang added a commit to wantehchang/libavif that referenced this pull request Mar 27, 2026
After pull request AOMediaCodec#3092, avifImageSplitGrid() doesn't return any grid
cells on failure. So only need to destroy returned cells on success.
wantehchang added a commit that referenced this pull request Mar 27, 2026
After pull request #3092, avifImageSplitGrid() doesn't return any grid
cells on failure. So only need to destroy returned cells on success.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants