Coverage Report

Created: 2026-10-28 07:26

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/libheif/libheif/security_limits.cc
Line
Count
Source
1
/*
2
 * HEIF codec.
3
 * Copyright (c) 2024 Dirk Farin <dirk.farin@gmail.com>
4
 *
5
 * This file is part of libheif.
6
 *
7
 * libheif is free software: you can redistribute it and/or modify
8
 * it under the terms of the GNU Lesser General Public License as
9
 * published by the Free Software Foundation, either version 3 of
10
 * the License, or (at your option) any later version.
11
 *
12
 * libheif is distributed in the hope that it will be useful,
13
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
14
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15
 * GNU Lesser General Public License for more details.
16
 *
17
 * You should have received a copy of the GNU Lesser General Public License
18
 * along with libheif.  If not, see <http://www.gnu.org/licenses/>.
19
 */
20
21
#include "security_limits.h"
22
#include <algorithm>
23
#include <limits>
24
#include <map>
25
#include <mutex>
26
27
28
heif_security_limits global_security_limits{
29
    .version = 4,
30
31
    // --- version 1
32
33
    // Artificial limit to avoid allocating too much memory.
34
    // 32768^2 = 1.5 GB as YUV-4:2:0 or 4 GB as RGB32
35
    .max_image_size_pixels = 32768 * 32768,
36
    .max_number_of_tiles = 4096 * 4096,
37
    .max_bayer_pattern_pixels = 16 * 16,
38
    .max_items = 1000,
39
40
    .max_color_profile_size = 100 * 1024 * 1024, // 100 MB
41
    .max_memory_block_size = UINT64_C(4) * 1024 * 1024 * 1024,  // 4 GB
42
43
    .max_components = 256,
44
    .max_iloc_extents_per_item = 32,
45
    .max_size_entity_group = 64,
46
47
    .max_children_per_box = 100,
48
49
    // --- version 2
50
51
    .max_total_memory = UINT64_C(4) * 1024 * 1024 * 1024,  // 4 GB
52
    .max_sample_description_box_entries = 1024,
53
    .max_sample_group_description_box_entries = 1024,
54
55
    // --- version 3
56
57
    .max_sequence_frames = 18'000'000,  // 100 hours at 50 fps
58
    .max_number_of_file_brands = 1000,
59
60
    // --- version 4
61
62
    .max_bad_pixels = 1000,
63
64
    .max_iso23001_17_pixel_size_bytes = 256,
65
66
    .parent = nullptr
67
};
68
69
70
heif_security_limits disabled_security_limits{
71
    .version = 4,
72
    .parent = nullptr
73
};
74
75
76
uint32_t max_coding_unit_size_for_codec(heif_compression_format format)
77
78.8k
{
78
78.8k
  switch (format) {
79
52.6k
    case heif_compression_AV1:      return 128;  // AV1 max superblock
80
16
    case heif_compression_VVC:      return 128;  // VVC max CTU
81
20.4k
    case heif_compression_HEVC:     return 64;   // HEVC max CTU
82
    // The coding units of AVC (macroblock) and JPEG (4:2:0 MCU) are 16 pixels. Their margin
83
    // is larger because the tightened limit is also given to the decoder plugin as its
84
    // maximum picture size, and FFmpeg compares that with the picture width rounded up to
85
    // its stride alignment (up to 64 pixels). With a margin of 16, FFmpeg refused images
86
    // like 720x1280, since 768*1280 is more than (720+16)*(1280+16).
87
487
    case heif_compression_AVC:      return 64;
88
2.84k
    case heif_compression_JPEG:     return 64;
89
2.43k
    case heif_compression_JPEG2000: return 64;
90
0
    case heif_compression_HTJ2K:    return 64;
91
0
    default:                        return 0;
92
78.8k
  }
93
78.8k
}
94
95
96
heif_security_limits tighten_image_size_limit_for_ispe(const heif_security_limits* base,
97
                                                       uint32_t ispe_width,
98
                                                       uint32_t ispe_height,
99
                                                       uint32_t coding_unit_size)
100
78.8k
{
101
78.8k
  heif_security_limits result = *base;
102
103
  // The returned struct is a stack-local derived copy. Point parent at the
104
  // registered context so MemoryHandle::alloc() can still find the entry in
105
  // sMemoryUsage for total-memory accounting. If base is itself derived, walk
106
  // to the root so we keep the parent chain at one hop.
107
78.8k
  result.parent = (base->version >= 4 && base->parent) ? base->parent : base;
108
78.8k
  result.version = 4;
109
110
78.8k
  if (ispe_width == 0 || ispe_height == 0) {
111
30.7k
    return result;
112
30.7k
  }
113
114
48.1k
  uint64_t padded_w = static_cast<uint64_t>(ispe_width)  + coding_unit_size;
115
48.1k
  uint64_t padded_h = static_cast<uint64_t>(ispe_height) + coding_unit_size;
116
117
  // Skip tightening if the padded dimensions would overflow uint64_t when multiplied.
118
  // The image is already absurdly large; check_for_valid_image_size will reject it.
119
48.1k
  if (padded_w != 0 && padded_h > std::numeric_limits<uint64_t>::max() / padded_w) {
120
0
    return result;
121
0
  }
122
123
48.1k
  uint64_t allowed = std::max(padded_w * padded_h, MIN_TIGHTENED_CODED_IMAGE_PIXELS);
124
125
48.1k
  if (result.max_image_size_pixels == 0 || allowed < result.max_image_size_pixels) {
126
47.3k
    result.max_image_size_pixels = allowed;
127
47.3k
  }
128
48.1k
  return result;
129
48.1k
}
130
131
132
DecodePixelBudget::DecodePixelBudget(uint64_t max_image_size_pixels)
133
76.9k
{
134
  // Clamp a very high configured limit so that the sums below cannot overflow.
135
76.9k
  const uint64_t max_pixels = uint64_t{1} << 40;
136
137
76.9k
  m_max_distinct_item_pixels = std::min(max_image_size_pixels, max_pixels);
138
76.9k
  m_max_item_pixels = m_max_distinct_item_pixels;
139
76.9k
  m_min_distinct_item_pixels = MIN_DECODED_PIXELS_BASE;
140
76.9k
}
141
142
143
DecodePixelBudget::DecodePixelBudget(uint64_t max_image_size_pixels, uint64_t tile_pixels, uint64_t num_tiles)
144
0
    : DecodePixelBudget(max_image_size_pixels)
145
0
{
146
  // Each tile gets its share of the budget of the whole image.
147
0
  m_max_item_pixels = std::min(tile_pixels, m_max_distinct_item_pixels);
148
149
0
  if (num_tiles > 1) {
150
0
    m_min_distinct_item_pixels = (MIN_DECODED_PIXELS_BASE + num_tiles - 1) / num_tiles;
151
0
  }
152
0
}
153
154
155
bool DecodePixelBudget::count_item_decode(heif_item_id id, uint64_t num_pixels)
156
100k
{
157
100k
  std::lock_guard<std::mutex> lock(m_mutex);
158
159
  // A larger image is going to be refused by the image size check.
160
100k
  num_pixels = std::min(num_pixels, m_max_distinct_item_pixels);
161
162
  // Each distinct item adds its size to m_distinct_item_pixels only once. The sum saturates at the
163
  // maximum image size: many large items, each cropped to a small image, must not
164
  // raise the allowance as fast as they use it up. For a tile decode, an item adds at most
165
  // the size of a tile.
166
100k
  const uint64_t item_contribution = std::min(num_pixels, m_max_item_pixels);
167
100k
  uint64_t& item_pixels = m_item_pixels_counted[id];
168
100k
  if (item_contribution > item_pixels) {
169
70.0k
    m_distinct_item_pixels = std::min(m_distinct_item_pixels + (item_contribution - item_pixels), m_max_distinct_item_pixels);
170
70.0k
    item_pixels = item_contribution;
171
70.0k
  }
172
173
  // Every decode is consumed, also repeated decodes of the same item.
174
  // Compared by subtraction, so that the check itself cannot overflow.
175
100k
  uint64_t allowance = std::max(m_distinct_item_pixels, m_min_distinct_item_pixels) * MAX_DECODED_PIXELS_FACTOR;
176
100k
  if (m_consumed_pixels > allowance || num_pixels > allowance - m_consumed_pixels) {
177
4
    return false;
178
4
  }
179
180
100k
  m_consumed_pixels += num_pixels;
181
100k
  return true;
182
100k
}
183
184
185
Error check_for_valid_image_size(const heif_security_limits* limits, uint32_t width, uint32_t height)
186
132k
{
187
132k
  uint64_t maximum_image_size_limit = limits->max_image_size_pixels;
188
189
  // --- check whether the image size is "too large"
190
191
132k
  if (maximum_image_size_limit > 0) {
192
132k
    auto max_width_height = static_cast<uint32_t>(std::numeric_limits<int>::max());
193
132k
    if ((width > max_width_height || height > max_width_height) ||
194
132k
        (height != 0 && width > maximum_image_size_limit / height)) {
195
574
      std::stringstream sstr;
196
574
      sstr << "Image size " << width << "x" << height << " exceeds the maximum image size "
197
574
           << maximum_image_size_limit << "\n";
198
199
574
      return {heif_error_Memory_allocation_error,
200
574
              heif_suberror_Security_limit_exceeded,
201
574
              sstr.str()};
202
574
    }
203
132k
  }
204
205
132k
  if (width == 0 || height == 0) {
206
37
    return {heif_error_Memory_allocation_error,
207
37
            heif_suberror_Invalid_image_size,
208
37
            "zero width or height"};
209
37
  }
210
211
132k
  return Error::Ok;
212
132k
}
213
214
215
struct memory_stats {
216
  size_t total_memory_usage = 0;
217
  size_t max_memory_usage = 0;
218
};
219
220
std::mutex& get_memory_usage_mutex()
221
715k
{
222
715k
  static std::mutex sMutex;
223
715k
  return sMutex;
224
715k
}
225
226
static std::map<const heif_security_limits*, memory_stats> sMemoryUsage;
227
228
TotalMemoryTracker::TotalMemoryTracker(const heif_security_limits* limits)
229
126k
{
230
126k
  std::lock_guard<std::mutex> lock(get_memory_usage_mutex());
231
232
126k
  sMemoryUsage[limits] = {};
233
126k
  m_limits_context = limits;
234
126k
}
235
236
TotalMemoryTracker::~TotalMemoryTracker()
237
126k
{
238
126k
  std::lock_guard<std::mutex> lock(get_memory_usage_mutex());
239
126k
  sMemoryUsage.erase(m_limits_context);
240
126k
}
241
242
243
size_t TotalMemoryTracker::get_max_total_memory_used() const
244
0
{
245
0
  std::lock_guard<std::mutex> lock(get_memory_usage_mutex());
246
247
0
  auto it = sMemoryUsage.find(m_limits_context);
248
0
  if (it != sMemoryUsage.end()) {
249
0
    return it->second.max_memory_usage;
250
0
  }
251
0
  else {
252
0
    assert(false);
253
0
    return 0;
254
0
  }
255
0
}
256
257
258
Error MemoryHandle::alloc(size_t count, size_t element_size,
259
                          const heif_security_limits* limits_context,
260
                          const char* reason_description)
261
5.30k
{
262
5.30k
  if (element_size != 0 && count > SIZE_MAX / element_size) {
263
0
    std::stringstream sstr;
264
0
    if (reason_description) {
265
0
      sstr << "Allocation size overflow computing " << count << " * " << element_size
266
0
           << " for " << reason_description;
267
0
    }
268
0
    else {
269
0
      sstr << "Allocation size overflow computing " << count << " * " << element_size;
270
0
    }
271
0
    return {heif_error_Memory_allocation_error,
272
0
            heif_suberror_Security_limit_exceeded,
273
0
            sstr.str()};
274
0
  }
275
5.30k
  return alloc(count * element_size, limits_context, reason_description);
276
5.30k
}
277
278
279
Error MemoryHandle::alloc(size_t memory_amount, const heif_security_limits* limits_context,
280
                          const char* reason_description)
281
288k
{
282
  // --- check whether limits are exceeded
283
284
288k
  if (!limits_context) {
285
0
    return Error::Ok;
286
0
  }
287
288
  // check against maximum memory block size
289
290
288k
  if (limits_context->max_memory_block_size != 0 &&
291
288k
      memory_amount > limits_context->max_memory_block_size) {
292
52
    std::stringstream sstr;
293
294
52
    if (reason_description) {
295
52
      sstr << "Allocating " << memory_amount << " bytes for " << reason_description <<" exceeds the security limit of "
296
52
           << limits_context->max_memory_block_size << " bytes";
297
52
    }
298
0
    else {
299
0
      sstr << "Allocating " << memory_amount << " bytes exceeds the security limit of "
300
0
           << limits_context->max_memory_block_size << " bytes";
301
0
    }
302
303
52
    return {heif_error_Memory_allocation_error,
304
52
            heif_suberror_Security_limit_exceeded,
305
52
            sstr.str()};
306
52
  }
307
308
  // Resolve to the registered (root) context for total-memory accounting.
309
  // The passed-in limits may be a stack-local derived copy (e.g. tightened for
310
  // ispe) whose `parent` points back to the registered context.
311
288k
  const heif_security_limits* root_limits = limits_context;
312
412k
  while (root_limits->version >= 4 && root_limits->parent) {
313
123k
    root_limits = root_limits->parent;
314
123k
  }
315
316
  // we allow several allocations on the same handle, but they have to be for the same registered context
317
288k
  if (m_limits_context) {
318
113k
    assert(m_limits_context == root_limits);
319
113k
  }
320
321
288k
  if (root_limits == &global_security_limits ||
322
288k
      root_limits == &disabled_security_limits) {
323
0
    return Error::Ok;
324
0
  }
325
326
288k
  std::lock_guard<std::mutex> lock(get_memory_usage_mutex());
327
288k
  auto it = sMemoryUsage.find(root_limits);
328
288k
  if (it == sMemoryUsage.end()) {
329
    // Unregistered limits context with no resolvable parent — total-memory
330
    // tracking is not available, but the per-block check above still applies.
331
0
    return Error::Ok;
332
0
  }
333
334
  // check against maximum total memory usage
335
336
288k
  if (limits_context->max_total_memory != 0 &&
337
288k
      it->second.total_memory_usage + memory_amount > limits_context->max_total_memory) {
338
4
    std::stringstream sstr;
339
340
4
    if (reason_description) {
341
4
      sstr << "Memory usage of " << it->second.total_memory_usage + memory_amount
342
4
           << " bytes for " << reason_description << " exceeds the security limit of "
343
4
           << limits_context->max_total_memory << " bytes of total memory usage";
344
4
    }
345
0
    else {
346
0
      sstr << "Memory usage of " << it->second.total_memory_usage + memory_amount
347
0
           << " bytes exceeds the security limit of "
348
0
           << limits_context->max_total_memory << " bytes of total memory usage";
349
0
    }
350
351
4
    return {heif_error_Memory_allocation_error,
352
4
            heif_suberror_Security_limit_exceeded,
353
4
            sstr.str()};
354
4
  }
355
356
357
  // --- register memory usage
358
359
288k
  m_limits_context = root_limits;
360
288k
  m_memory_amount += memory_amount;
361
362
288k
  it->second.total_memory_usage += memory_amount;
363
364
  // remember maximum memory usage (for informational purpose)
365
288k
  if (it->second.total_memory_usage > it->second.max_memory_usage) {
366
262k
    it->second.max_memory_usage = it->second.total_memory_usage;
367
262k
  }
368
369
288k
  return Error::Ok;
370
288k
}
371
372
373
void MemoryHandle::free()
374
750k
{
375
750k
  if (m_limits_context) {
376
174k
    std::lock_guard<std::mutex> lock(get_memory_usage_mutex());
377
378
174k
    auto it = sMemoryUsage.find(m_limits_context);
379
174k
    if (it != sMemoryUsage.end()) {
380
89.9k
      it->second.total_memory_usage -= m_memory_amount;
381
89.9k
    }
382
383
174k
    m_limits_context = nullptr;
384
174k
    m_memory_amount = 0;
385
174k
  }
386
750k
}
387
388
389
void MemoryHandle::free(size_t memory_amount)
390
137
{
391
137
  if (m_limits_context) {
392
137
    std::lock_guard<std::mutex> lock(get_memory_usage_mutex());
393
394
137
    auto it = sMemoryUsage.find(m_limits_context);
395
137
    if (it != sMemoryUsage.end()) {
396
137
      it->second.total_memory_usage -= memory_amount;
397
137
    }
398
399
137
    m_memory_amount -= memory_amount;
400
137
  }
401
137
}
402