Coverage Report

Created: 2026-06-16 07:20

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 <limits>
23
#include <map>
24
#include <mutex>
25
26
27
heif_security_limits global_security_limits{
28
    .version = 4,
29
30
    // --- version 1
31
32
    // Artificial limit to avoid allocating too much memory.
33
    // 32768^2 = 1.5 GB as YUV-4:2:0 or 4 GB as RGB32
34
    .max_image_size_pixels = 32768 * 32768,
35
    .max_number_of_tiles = 4096 * 4096,
36
    .max_bayer_pattern_pixels = 16 * 16,
37
    .max_items = 1000,
38
39
    .max_color_profile_size = 100 * 1024 * 1024, // 100 MB
40
    .max_memory_block_size = UINT64_C(4) * 1024 * 1024 * 1024,  // 4 GB
41
42
    .max_components = 256,
43
    .max_iloc_extents_per_item = 32,
44
    .max_size_entity_group = 64,
45
46
    .max_children_per_box = 100,
47
48
    // --- version 2
49
50
    .max_total_memory = UINT64_C(4) * 1024 * 1024 * 1024,  // 4 GB
51
    .max_sample_description_box_entries = 1024,
52
    .max_sample_group_description_box_entries = 1024,
53
54
    // --- version 3
55
56
    .max_sequence_frames = 18'000'000,  // 100 hours at 50 fps
57
    .max_number_of_file_brands = 1000,
58
59
    // --- version 4
60
61
    .max_bad_pixels = 1000,
62
63
    .max_iso23001_17_pixel_size_bytes = 256,
64
65
    .parent = nullptr
66
};
67
68
69
heif_security_limits disabled_security_limits{
70
    .version = 4,
71
    .parent = nullptr
72
};
73
74
75
uint32_t max_coding_unit_size_for_codec(heif_compression_format format)
76
59.3k
{
77
59.3k
  switch (format) {
78
36.4k
    case heif_compression_AV1:      return 128;  // AV1 max superblock
79
951
    case heif_compression_VVC:      return 128;  // VVC max CTU
80
18.8k
    case heif_compression_HEVC:     return 64;   // HEVC max CTU
81
785
    case heif_compression_AVC:      return 16;   // H.264 macroblock
82
1.29k
    case heif_compression_JPEG:     return 16;   // JPEG MCU (4:2:0)
83
1.04k
    case heif_compression_JPEG2000: return 64;
84
0
    case heif_compression_HTJ2K:    return 64;
85
0
    default:                        return 0;
86
59.3k
  }
87
59.3k
}
88
89
90
heif_security_limits tighten_image_size_limit_for_ispe(const heif_security_limits* base,
91
                                                       uint32_t ispe_width,
92
                                                       uint32_t ispe_height,
93
                                                       uint32_t coding_unit_size)
94
59.3k
{
95
59.3k
  heif_security_limits result = *base;
96
97
  // The returned struct is a stack-local derived copy. Point parent at the
98
  // registered context so MemoryHandle::alloc() can still find the entry in
99
  // sMemoryUsage for total-memory accounting. If base is itself derived, walk
100
  // to the root so we keep the parent chain at one hop.
101
59.3k
  result.parent = (base->version >= 4 && base->parent) ? base->parent : base;
102
59.3k
  result.version = 4;
103
104
59.3k
  if (ispe_width == 0 || ispe_height == 0) {
105
17.0k
    return result;
106
17.0k
  }
107
108
42.3k
  uint64_t padded_w = static_cast<uint64_t>(ispe_width)  + coding_unit_size;
109
42.3k
  uint64_t padded_h = static_cast<uint64_t>(ispe_height) + coding_unit_size;
110
111
  // Skip tightening if the padded dimensions would overflow uint64_t when multiplied.
112
  // The image is already absurdly large; check_for_valid_image_size will reject it.
113
42.3k
  if (padded_w != 0 && padded_h > std::numeric_limits<uint64_t>::max() / padded_w) {
114
0
    return result;
115
0
  }
116
117
42.3k
  uint64_t allowed = padded_w * padded_h;
118
119
42.3k
  if (result.max_image_size_pixels == 0 || allowed < result.max_image_size_pixels) {
120
41.5k
    result.max_image_size_pixels = allowed;
121
41.5k
  }
122
42.3k
  return result;
123
42.3k
}
124
125
126
Error check_for_valid_image_size(const heif_security_limits* limits, uint32_t width, uint32_t height)
127
72.1k
{
128
72.1k
  uint64_t maximum_image_size_limit = limits->max_image_size_pixels;
129
130
  // --- check whether the image size is "too large"
131
132
72.1k
  if (maximum_image_size_limit > 0) {
133
72.0k
    auto max_width_height = static_cast<uint32_t>(std::numeric_limits<int>::max());
134
72.0k
    if ((width > max_width_height || height > max_width_height) ||
135
71.9k
        (height != 0 && width > maximum_image_size_limit / height)) {
136
487
      std::stringstream sstr;
137
487
      sstr << "Image size " << width << "x" << height << " exceeds the maximum image size "
138
487
           << maximum_image_size_limit << "\n";
139
140
487
      return {heif_error_Memory_allocation_error,
141
487
              heif_suberror_Security_limit_exceeded,
142
487
              sstr.str()};
143
487
    }
144
72.0k
  }
145
146
71.6k
  if (width == 0 || height == 0) {
147
130
    return {heif_error_Memory_allocation_error,
148
130
            heif_suberror_Invalid_image_size,
149
130
            "zero width or height"};
150
130
  }
151
152
71.5k
  return Error::Ok;
153
71.6k
}
154
155
156
struct memory_stats {
157
  size_t total_memory_usage = 0;
158
  size_t max_memory_usage = 0;
159
};
160
161
std::mutex& get_memory_usage_mutex()
162
583k
{
163
583k
  static std::mutex sMutex;
164
583k
  return sMutex;
165
583k
}
166
167
static std::map<const heif_security_limits*, memory_stats> sMemoryUsage;
168
169
TotalMemoryTracker::TotalMemoryTracker(const heif_security_limits* limits)
170
113k
{
171
113k
  std::lock_guard<std::mutex> lock(get_memory_usage_mutex());
172
173
113k
  sMemoryUsage[limits] = {};
174
113k
  m_limits_context = limits;
175
113k
}
176
177
TotalMemoryTracker::~TotalMemoryTracker()
178
113k
{
179
113k
  std::lock_guard<std::mutex> lock(get_memory_usage_mutex());
180
113k
  sMemoryUsage.erase(m_limits_context);
181
113k
}
182
183
184
size_t TotalMemoryTracker::get_max_total_memory_used() const
185
0
{
186
0
  std::lock_guard<std::mutex> lock(get_memory_usage_mutex());
187
188
0
  auto it = sMemoryUsage.find(m_limits_context);
189
0
  if (it != sMemoryUsage.end()) {
190
0
    return it->second.max_memory_usage;
191
0
  }
192
0
  else {
193
0
    assert(false);
194
0
    return 0;
195
0
  }
196
0
}
197
198
199
Error MemoryHandle::alloc(size_t count, size_t element_size,
200
                          const heif_security_limits* limits_context,
201
                          const char* reason_description)
202
4.45k
{
203
4.45k
  if (element_size != 0 && count > SIZE_MAX / element_size) {
204
0
    std::stringstream sstr;
205
0
    if (reason_description) {
206
0
      sstr << "Allocation size overflow computing " << count << " * " << element_size
207
0
           << " for " << reason_description;
208
0
    }
209
0
    else {
210
0
      sstr << "Allocation size overflow computing " << count << " * " << element_size;
211
0
    }
212
0
    return {heif_error_Memory_allocation_error,
213
0
            heif_suberror_Security_limit_exceeded,
214
0
            sstr.str()};
215
0
  }
216
4.45k
  return alloc(count * element_size, limits_context, reason_description);
217
4.45k
}
218
219
220
Error MemoryHandle::alloc(size_t memory_amount, const heif_security_limits* limits_context,
221
                          const char* reason_description)
222
232k
{
223
  // --- check whether limits are exceeded
224
225
232k
  if (!limits_context) {
226
0
    return Error::Ok;
227
0
  }
228
229
  // check against maximum memory block size
230
231
232k
  if (limits_context->max_memory_block_size != 0 &&
232
232k
      memory_amount > limits_context->max_memory_block_size) {
233
48
    std::stringstream sstr;
234
235
48
    if (reason_description) {
236
48
      sstr << "Allocating " << memory_amount << " bytes for " << reason_description <<" exceeds the security limit of "
237
48
           << limits_context->max_memory_block_size << " bytes";
238
48
    }
239
0
    else {
240
0
      sstr << "Allocating " << memory_amount << " bytes exceeds the security limit of "
241
0
           << limits_context->max_memory_block_size << " bytes";
242
0
    }
243
244
48
    return {heif_error_Memory_allocation_error,
245
48
            heif_suberror_Security_limit_exceeded,
246
48
            sstr.str()};
247
48
  }
248
249
  // Resolve to the registered (root) context for total-memory accounting.
250
  // The passed-in limits may be a stack-local derived copy (e.g. tightened for
251
  // ispe) whose `parent` points back to the registered context.
252
232k
  const heif_security_limits* root_limits = limits_context;
253
328k
  while (root_limits->version >= 4 && root_limits->parent) {
254
96.4k
    root_limits = root_limits->parent;
255
96.4k
  }
256
257
  // we allow several allocations on the same handle, but they have to be for the same registered context
258
232k
  if (m_limits_context) {
259
105k
    assert(m_limits_context == root_limits);
260
105k
  }
261
262
232k
  if (root_limits == &global_security_limits ||
263
230k
      root_limits == &disabled_security_limits) {
264
1.28k
    return Error::Ok;
265
1.28k
  }
266
267
230k
  std::lock_guard<std::mutex> lock(get_memory_usage_mutex());
268
230k
  auto it = sMemoryUsage.find(root_limits);
269
230k
  if (it == sMemoryUsage.end()) {
270
    // Unregistered limits context with no resolvable parent — total-memory
271
    // tracking is not available, but the per-block check above still applies.
272
0
    return Error::Ok;
273
0
  }
274
275
  // check against maximum total memory usage
276
277
230k
  if (limits_context->max_total_memory != 0 &&
278
230k
      it->second.total_memory_usage + memory_amount > limits_context->max_total_memory) {
279
1
    std::stringstream sstr;
280
281
1
    if (reason_description) {
282
1
      sstr << "Memory usage of " << it->second.total_memory_usage + memory_amount
283
1
           << " bytes for " << reason_description << " exceeds the security limit of "
284
1
           << limits_context->max_total_memory << " bytes of total memory usage";
285
1
    }
286
0
    else {
287
0
      sstr << "Memory usage of " << it->second.total_memory_usage + memory_amount
288
0
           << " bytes exceeds the security limit of "
289
0
           << limits_context->max_total_memory << " bytes of total memory usage";
290
0
    }
291
292
1
    return {heif_error_Memory_allocation_error,
293
1
            heif_suberror_Security_limit_exceeded,
294
1
            sstr.str()};
295
1
  }
296
297
298
  // --- register memory usage
299
300
230k
  m_limits_context = root_limits;
301
230k
  m_memory_amount += memory_amount;
302
303
230k
  it->second.total_memory_usage += memory_amount;
304
305
  // remember maximum memory usage (for informational purpose)
306
230k
  if (it->second.total_memory_usage > it->second.max_memory_usage) {
307
220k
    it->second.max_memory_usage = it->second.total_memory_usage;
308
220k
  }
309
310
230k
  return Error::Ok;
311
230k
}
312
313
314
void MemoryHandle::free()
315
570k
{
316
570k
  if (m_limits_context) {
317
125k
    std::lock_guard<std::mutex> lock(get_memory_usage_mutex());
318
319
125k
    auto it = sMemoryUsage.find(m_limits_context);
320
125k
    if (it != sMemoryUsage.end()) {
321
66.3k
      it->second.total_memory_usage -= m_memory_amount;
322
66.3k
    }
323
324
125k
    m_limits_context = nullptr;
325
125k
    m_memory_amount = 0;
326
125k
  }
327
570k
}
328
329
330
void MemoryHandle::free(size_t memory_amount)
331
48
{
332
48
  if (m_limits_context) {
333
48
    std::lock_guard<std::mutex> lock(get_memory_usage_mutex());
334
335
48
    auto it = sMemoryUsage.find(m_limits_context);
336
48
    if (it != sMemoryUsage.end()) {
337
48
      it->second.total_memory_usage -= memory_amount;
338
48
    }
339
340
48
    m_memory_amount -= memory_amount;
341
48
  }
342
48
}
343