Coverage Report

Created: 2026-10-17 06:50

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
0
{
78
0
  switch (format) {
79
0
    case heif_compression_AV1:      return 128;  // AV1 max superblock
80
0
    case heif_compression_VVC:      return 128;  // VVC max CTU
81
0
    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
0
    case heif_compression_AVC:      return 64;
88
0
    case heif_compression_JPEG:     return 64;
89
0
    case heif_compression_JPEG2000: return 64;
90
0
    case heif_compression_HTJ2K:    return 64;
91
0
    default:                        return 0;
92
0
  }
93
0
}
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
0
{
101
0
  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
0
  result.parent = (base->version >= 4 && base->parent) ? base->parent : base;
108
0
  result.version = 4;
109
110
0
  if (ispe_width == 0 || ispe_height == 0) {
111
0
    return result;
112
0
  }
113
114
0
  uint64_t padded_w = static_cast<uint64_t>(ispe_width)  + coding_unit_size;
115
0
  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
0
  if (padded_w != 0 && padded_h > std::numeric_limits<uint64_t>::max() / padded_w) {
120
0
    return result;
121
0
  }
122
123
0
  uint64_t allowed = std::max(padded_w * padded_h, MIN_TIGHTENED_CODED_IMAGE_PIXELS);
124
125
0
  if (result.max_image_size_pixels == 0 || allowed < result.max_image_size_pixels) {
126
0
    result.max_image_size_pixels = allowed;
127
0
  }
128
0
  return result;
129
0
}
130
131
132
Error check_for_valid_image_size(const heif_security_limits* limits, uint32_t width, uint32_t height)
133
16.5k
{
134
16.5k
  uint64_t maximum_image_size_limit = limits->max_image_size_pixels;
135
136
  // --- check whether the image size is "too large"
137
138
16.5k
  if (maximum_image_size_limit > 0) {
139
16.5k
    auto max_width_height = static_cast<uint32_t>(std::numeric_limits<int>::max());
140
16.5k
    if ((width > max_width_height || height > max_width_height) ||
141
16.5k
        (height != 0 && width > maximum_image_size_limit / height)) {
142
49
      std::stringstream sstr;
143
49
      sstr << "Image size " << width << "x" << height << " exceeds the maximum image size "
144
49
           << maximum_image_size_limit << "\n";
145
146
49
      return {heif_error_Memory_allocation_error,
147
49
              heif_suberror_Security_limit_exceeded,
148
49
              sstr.str()};
149
49
    }
150
16.5k
  }
151
152
16.5k
  if (width == 0 || height == 0) {
153
4
    return {heif_error_Memory_allocation_error,
154
4
            heif_suberror_Invalid_image_size,
155
4
            "zero width or height"};
156
4
  }
157
158
16.5k
  return Error::Ok;
159
16.5k
}
160
161
162
struct memory_stats {
163
  size_t total_memory_usage = 0;
164
  size_t max_memory_usage = 0;
165
};
166
167
std::mutex& get_memory_usage_mutex()
168
688k
{
169
688k
  static std::mutex sMutex;
170
688k
  return sMutex;
171
688k
}
172
173
static std::map<const heif_security_limits*, memory_stats> sMemoryUsage;
174
175
TotalMemoryTracker::TotalMemoryTracker(const heif_security_limits* limits)
176
26.2k
{
177
26.2k
  std::lock_guard<std::mutex> lock(get_memory_usage_mutex());
178
179
26.2k
  sMemoryUsage[limits] = {};
180
26.2k
  m_limits_context = limits;
181
26.2k
}
182
183
TotalMemoryTracker::~TotalMemoryTracker()
184
26.2k
{
185
26.2k
  std::lock_guard<std::mutex> lock(get_memory_usage_mutex());
186
26.2k
  sMemoryUsage.erase(m_limits_context);
187
26.2k
}
188
189
190
size_t TotalMemoryTracker::get_max_total_memory_used() const
191
0
{
192
0
  std::lock_guard<std::mutex> lock(get_memory_usage_mutex());
193
194
0
  auto it = sMemoryUsage.find(m_limits_context);
195
0
  if (it != sMemoryUsage.end()) {
196
0
    return it->second.max_memory_usage;
197
0
  }
198
0
  else {
199
0
    assert(false);
200
0
    return 0;
201
0
  }
202
0
}
203
204
205
Error MemoryHandle::alloc(size_t count, size_t element_size,
206
                          const heif_security_limits* limits_context,
207
                          const char* reason_description)
208
62.7k
{
209
62.7k
  if (element_size != 0 && count > SIZE_MAX / element_size) {
210
0
    std::stringstream sstr;
211
0
    if (reason_description) {
212
0
      sstr << "Allocation size overflow computing " << count << " * " << element_size
213
0
           << " for " << reason_description;
214
0
    }
215
0
    else {
216
0
      sstr << "Allocation size overflow computing " << count << " * " << element_size;
217
0
    }
218
0
    return {heif_error_Memory_allocation_error,
219
0
            heif_suberror_Security_limit_exceeded,
220
0
            sstr.str()};
221
0
  }
222
62.7k
  return alloc(count * element_size, limits_context, reason_description);
223
62.7k
}
224
225
226
Error MemoryHandle::alloc(size_t memory_amount, const heif_security_limits* limits_context,
227
                          const char* reason_description)
228
347k
{
229
  // --- check whether limits are exceeded
230
231
347k
  if (!limits_context) {
232
0
    return Error::Ok;
233
0
  }
234
235
  // check against maximum memory block size
236
237
347k
  if (limits_context->max_memory_block_size != 0 &&
238
347k
      memory_amount > limits_context->max_memory_block_size) {
239
32
    std::stringstream sstr;
240
241
32
    if (reason_description) {
242
32
      sstr << "Allocating " << memory_amount << " bytes for " << reason_description <<" exceeds the security limit of "
243
32
           << limits_context->max_memory_block_size << " bytes";
244
32
    }
245
0
    else {
246
0
      sstr << "Allocating " << memory_amount << " bytes exceeds the security limit of "
247
0
           << limits_context->max_memory_block_size << " bytes";
248
0
    }
249
250
32
    return {heif_error_Memory_allocation_error,
251
32
            heif_suberror_Security_limit_exceeded,
252
32
            sstr.str()};
253
32
  }
254
255
  // Resolve to the registered (root) context for total-memory accounting.
256
  // The passed-in limits may be a stack-local derived copy (e.g. tightened for
257
  // ispe) whose `parent` points back to the registered context.
258
347k
  const heif_security_limits* root_limits = limits_context;
259
347k
  while (root_limits->version >= 4 && root_limits->parent) {
260
0
    root_limits = root_limits->parent;
261
0
  }
262
263
  // we allow several allocations on the same handle, but they have to be for the same registered context
264
347k
  if (m_limits_context) {
265
59.1k
    assert(m_limits_context == root_limits);
266
59.1k
  }
267
268
347k
  if (root_limits == &global_security_limits ||
269
347k
      root_limits == &disabled_security_limits) {
270
0
    return Error::Ok;
271
0
  }
272
273
347k
  std::lock_guard<std::mutex> lock(get_memory_usage_mutex());
274
347k
  auto it = sMemoryUsage.find(root_limits);
275
347k
  if (it == sMemoryUsage.end()) {
276
    // Unregistered limits context with no resolvable parent — total-memory
277
    // tracking is not available, but the per-block check above still applies.
278
0
    return Error::Ok;
279
0
  }
280
281
  // check against maximum total memory usage
282
283
347k
  if (limits_context->max_total_memory != 0 &&
284
347k
      it->second.total_memory_usage + memory_amount > limits_context->max_total_memory) {
285
0
    std::stringstream sstr;
286
287
0
    if (reason_description) {
288
0
      sstr << "Memory usage of " << it->second.total_memory_usage + memory_amount
289
0
           << " bytes for " << reason_description << " exceeds the security limit of "
290
0
           << limits_context->max_total_memory << " bytes of total memory usage";
291
0
    }
292
0
    else {
293
0
      sstr << "Memory usage of " << it->second.total_memory_usage + memory_amount
294
0
           << " bytes exceeds the security limit of "
295
0
           << limits_context->max_total_memory << " bytes of total memory usage";
296
0
    }
297
298
0
    return {heif_error_Memory_allocation_error,
299
0
            heif_suberror_Security_limit_exceeded,
300
0
            sstr.str()};
301
0
  }
302
303
304
  // --- register memory usage
305
306
347k
  m_limits_context = root_limits;
307
347k
  m_memory_amount += memory_amount;
308
309
347k
  it->second.total_memory_usage += memory_amount;
310
311
  // remember maximum memory usage (for informational purpose)
312
347k
  if (it->second.total_memory_usage > it->second.max_memory_usage) {
313
121k
    it->second.max_memory_usage = it->second.total_memory_usage;
314
121k
  }
315
316
347k
  return Error::Ok;
317
347k
}
318
319
320
void MemoryHandle::free()
321
601k
{
322
601k
  if (m_limits_context) {
323
288k
    std::lock_guard<std::mutex> lock(get_memory_usage_mutex());
324
325
288k
    auto it = sMemoryUsage.find(m_limits_context);
326
288k
    if (it != sMemoryUsage.end()) {
327
241k
      it->second.total_memory_usage -= m_memory_amount;
328
241k
    }
329
330
288k
    m_limits_context = nullptr;
331
288k
    m_memory_amount = 0;
332
288k
  }
333
601k
}
334
335
336
void MemoryHandle::free(size_t memory_amount)
337
0
{
338
0
  if (m_limits_context) {
339
0
    std::lock_guard<std::mutex> lock(get_memory_usage_mutex());
340
341
0
    auto it = sMemoryUsage.find(m_limits_context);
342
0
    if (it != sMemoryUsage.end()) {
343
0
      it->second.total_memory_usage -= memory_amount;
344
0
    }
345
346
0
    m_memory_amount -= memory_amount;
347
0
  }
348
0
}
349