Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
f69e78f
security: validate dummy video parameters
CoffeeFlux Aug 26, 2026
28232ce
security: validate YUV4MPEG frame sizes
CoffeeFlux Aug 26, 2026
a5431fe
security: require consent for linked AviSynth scripts
CoffeeFlux Aug 26, 2026
f6b8290
security: authorize linked network paths before access
CoffeeFlux Aug 26, 2026
793f844
security: bound Matroska subtitle decompression
CoffeeFlux Aug 26, 2026
a41f662
security: make SRT tag parsing linear
CoffeeFlux Aug 26, 2026
0378b69
security: linearize ASS attachment parsing
CoffeeFlux Aug 26, 2026
2c782ad
security: skip invalid timecodes iteratively
CoffeeFlux Aug 26, 2026
26c8976
security: contain attachment extraction paths
CoffeeFlux Aug 26, 2026
e1546b3
security: handle empty attachment extraction
CoffeeFlux Aug 26, 2026
56c5f69
security: validate CoreText name tables
CoffeeFlux Aug 26, 2026
cdacb0b
security: sanitize script-info properties
CoffeeFlux Aug 26, 2026
6a74eab
security: harden file mapping bounds checks
CoffeeFlux Aug 26, 2026
09fd341
security: validate linked video UI metadata
CoffeeFlux Aug 26, 2026
0353a8d
security: bound V1 timecode expansion
CoffeeFlux Aug 26, 2026
f2df5dc
security: guard override parameter masks
CoffeeFlux Aug 26, 2026
0ffc303
security: pin Lua support modules to bundled files
CoffeeFlux Aug 26, 2026
87a96ef
security: restrict AviSynth DLL search paths
CoffeeFlux Aug 26, 2026
e417af2
fixup! security: require consent for linked AviSynth scripts
arch1t3cht Sep 27, 2026
df4d618
fixup! security: validate YUV4MPEG frame sizes
arch1t3cht Sep 27, 2026
01ac570
Use signed integer sizes and offset in YUV4MPEG provider
arch1t3cht Sep 27, 2026
3f74629
fixup! security: linearize ASS attachment parsing
arch1t3cht Sep 27, 2026
2a95ecd
fixup! security: handle empty attachment extraction
arch1t3cht Sep 27, 2026
2f7e811
fixup! security: sanitize script-info properties
arch1t3cht Sep 27, 2026
a308cd3
fixup! security: guard override parameter masks
arch1t3cht Sep 27, 2026
db69e62
fixup! security: authorize linked network paths before access
arch1t3cht Sep 27, 2026
4ce6df5
fixup! security: pin Lua support modules to bundled files
arch1t3cht Sep 27, 2026
b97cc0e
fixup! security: bound V1 timecode expansion
arch1t3cht Sep 27, 2026
8d39a3d
fixup! security: bound Matroska subtitle decompression
arch1t3cht Sep 27, 2026
961ab6f
fixup! security: validate dummy video parameters
arch1t3cht Sep 30, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion automation/tests/aegisub.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ int main(int argc, char **argv) {
}

preload_modules(L);
Install(L, {"include"});
Install(L, {"include"}, "include");

// Patch os.exit to close the lua state first since busted calls it when
// it's done
Expand Down
21 changes: 16 additions & 5 deletions libaegisub/common/file_mapping.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -34,19 +34,30 @@ char *map(int64_t s_offset, uint64_t length, boost::interprocess::mode_t mode,
std::unique_ptr<mapped_region>& region, uint64_t& mapping_start)
{
static char dummy = 0;
if (length == 0) return &dummy;
if (s_offset < 0)
throw agi::InternalError("Attempted to map a negative file offset");

auto offset = static_cast<uint64_t>(s_offset);
if (offset + length > file_size)
if (offset > file_size || length > file_size - offset)
throw agi::InternalError("Attempted to map beyond end of file");
if (length == 0) return &dummy;

// Check if we can just use the current mapping
if (region && offset >= mapping_start && offset + length <= mapping_start + region->get_size())
return static_cast<char *>(region->get_address()) + offset - mapping_start;
if (region && offset >= mapping_start) {
auto relative_offset = offset - mapping_start;
auto region_size = region->get_size();
if (relative_offset <= region_size && length <= region_size - relative_offset)
return static_cast<char *>(region->get_address()) + relative_offset;
}

if (sizeof(size_t) == 4) {
mapping_start = offset & ~0xFFFFFULL; // Align to 1 MB boundary
length += static_cast<size_t>(offset - mapping_start);
auto prefix = offset - mapping_start;
if (length > std::numeric_limits<size_t>::max() - prefix)
throw std::bad_alloc();
length += prefix;
if (length > std::numeric_limits<size_t>::max() - 0xFFFFF)
throw std::bad_alloc();
// Map 16 MB or length rounded up to the next MB
length = std::min<uint64_t>(std::max<uint64_t>(0x1000000U, (length + 0xFFFFF) & ~0xFFFFF), file_size - mapping_start);
}
Expand Down
53 changes: 42 additions & 11 deletions libaegisub/common/vfr.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -19,21 +19,39 @@
#include "libaegisub/vfr.h"

#include "libaegisub/charset.h"
#include "libaegisub/format.h"
#include "libaegisub/io.h"
#include "libaegisub/line_iterator.h"
#include "libaegisub/util.h"

#include <algorithm>
#include <boost/interprocess/streams/bufferstream.hpp>
#include <boost/range/algorithm.hpp>
#include <cmath>
#include <functional>
#include <iterator>
#include <limits>

namespace {
static const int64_t default_denominator = 1000000000;
static const int max_timecodes = 10000000;
using agi::line_iterator;
using namespace agi::vfr;

int64_t checked_fps_numerator(double fps) {
if (!std::isfinite(fps) || fps < 0.)
throw InvalidFramerate("FPS must be greater than zero");
if (fps > 1000.)
throw InvalidFramerate("FPS must not be greater than 1000");
return static_cast<int64_t>(fps * default_denominator);
}

void append_v1_timecode(std::vector<int>& timecodes, double time) {
if (!std::isfinite(time) || time < 0 || time > std::numeric_limits<int>::max() - .5)
throw InvalidFramerate("V1 timecode exceeds the supported timestamp range");
timecodes.push_back(static_cast<int>(time + .5));
}

/// @brief Verify that timecodes monotonically increase
/// @param timecodes List of timecodes to check
void validate_timecodes(std::vector<int> const& timecodes) {
Expand Down Expand Up @@ -76,7 +94,9 @@ TimecodeRange v1_parse_line(std::string const& str) {
throw InvalidFramerate("Cannot specify frame rate for negative frames.");
if (range.end < range.start)
throw InvalidFramerate("End frame must be greater than or equal to start frame");
if (range.fps <= 0.)
if (range.end > max_timecodes - 2)
throw InvalidFramerate(agi::format("V1 timecode range exceeds the %d frame limit", max_timecodes));
if (!std::isfinite(range.fps) || range.fps <= 0.)
throw InvalidFramerate("FPS must be greater than zero");
if (range.fps > 1000.)
// This is our limitation, not mkvmerge's
Expand All @@ -92,8 +112,14 @@ TimecodeRange v1_parse_line(std::string const& str) {
/// @param[out] last Unrounded time of the last frame
/// @return Assumed fps times one million
int64_t v1_parse(line_iterator<std::string> file, std::string line, std::vector<int> &timecodes, int64_t &last) {
double fps = atof(line.substr(7).c_str());
if (fps <= 0.) throw InvalidFramerate("Assumed FPS must be greater than zero");
auto fps_string = line.substr(7);
auto first = fps_string.find_first_not_of(" \t\r\n\f\v");
auto last_char = fps_string.find_last_not_of(" \t\r\n\f\v");
if (first != std::string::npos)
fps_string = fps_string.substr(first, last_char - first + 1);
double fps;
if (!agi::util::try_parse(fps_string, &fps) || !std::isfinite(fps) || fps <= 0.)
throw InvalidFramerate("Assumed FPS must be greater than zero");
if (fps > 1000.) throw InvalidFramerate("Assumed FPS must not be greater than 1000");

std::vector<TimecodeRange> ranges;
Expand All @@ -116,27 +142,28 @@ int64_t v1_parse(line_iterator<std::string> file, std::string line, std::vector<
throw InvalidFramerate("Override ranges must not overlap");
}
for (; frame < range.start; ++frame) {
timecodes.push_back(int(time + .5));
append_v1_timecode(timecodes, time);
time += 1000. / fps;
}
for (; frame <= range.end; ++frame) {
timecodes.push_back(int(time + .5));
append_v1_timecode(timecodes, time);
time += 1000. / range.fps;
}
}
timecodes.push_back(int(time + .5));
last = int64_t(time * fps * default_denominator);
append_v1_timecode(timecodes, time);
auto last_time = time * fps * default_denominator;
if (!std::isfinite(last_time) || last_time >= std::ldexp(1.0, 63))
throw InvalidFramerate("V1 timecode exceeds the supported timestamp range");
last = static_cast<int64_t>(last_time);
return int64_t(fps * default_denominator);
}
}

namespace agi::vfr {
Framerate::Framerate(double fps)
: denominator(default_denominator)
, numerator(int64_t(fps * denominator))
, numerator(checked_fps_numerator(fps))
{
if (fps < 0.) throw InvalidFramerate("FPS must be greater than zero");
if (fps > 1000.) throw InvalidFramerate("FPS must not be greater than 1000");
timecodes.push_back(0);
}

Expand Down Expand Up @@ -178,7 +205,11 @@ Framerate::Framerate(agi::fs::path const& filename)
auto encoding = agi::charset::Detect(filename);
auto line = *line_iterator<std::string>(*file, encoding.c_str());
if (line == "# timecode format v2") {
copy(line_iterator<int>(*file, encoding.c_str()), line_iterator<int>(), back_inserter(timecodes));
for (auto timecode : line_iterator<int>(*file, encoding.c_str())) {
if (std::ssize(timecodes) >= max_timecodes)
throw InvalidFramerate("Timecode file exceeds the 10000000 entry limit");
timecodes.push_back(timecode);
}
SetFromTimecodes();
return;
}
Expand Down
9 changes: 5 additions & 4 deletions libaegisub/include/libaegisub/line_iterator.h
Original file line number Diff line number Diff line change
Expand Up @@ -109,10 +109,11 @@ line_iterator<T> end(line_iterator<T>&) { return agi::line_iterator<T>(); }
template<class OutputType>
void line_iterator<OutputType>::next() {
std::string str;
if (!getline(str))
return;
if (!convert(str))
next();
do {
str.clear();
if (!getline(str))
return;
} while (!convert(str));
}

template<>
Expand Down
6 changes: 4 additions & 2 deletions libaegisub/include/libaegisub/lua/script_reader.h
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ namespace agi::lua {
/// Load a Lua or Moonscript file at the given path
bool LoadFile(lua_State *L, agi::fs::path const& filename);
/// Install our module loader and add include_path to the module search
/// path of the given lua state
bool Install(lua_State *L, std::vector<agi::fs::path> const& include_path);
/// path of the given lua state. Mandatory runtime support is loaded only
/// from support_path.
bool Install(lua_State *L, std::vector<agi::fs::path> const& include_path,
agi::fs::path const& support_path);
}
43 changes: 40 additions & 3 deletions libaegisub/lua/script_reader.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,18 @@
#include <lauxlib.h>

namespace agi::lua {
namespace {
bool load_support_file(lua_State *L, fs::path const& filename) {
try {
return LoadFile(L, filename);
}
catch (agi::Exception const& e) {
lua_pushstring(L, e.GetMessage().c_str());
return false;
}
}
}

bool LoadFile(lua_State *L, agi::fs::path const& raw_filename) {
auto filename = raw_filename;
try {
Expand Down Expand Up @@ -119,7 +131,8 @@ namespace agi::lua {
return lua_gettop(L) - pretop;
}

bool Install(lua_State *L, std::vector<fs::path> const& include_path) {
bool Install(lua_State *L, std::vector<fs::path> const& include_path,
fs::path const& support_path) {
// set the module load path to include_path
lua_getglobal(L, "package");
push_value(L, "path");
Expand Down Expand Up @@ -148,18 +161,42 @@ namespace agi::lua {

#ifdef _WIN32
// Replace the default lua IO functions with our unicode compatible ones
luaL_loadstring(L, "require('unicode-monkeypatch')");
if (!load_support_file(L, support_path / "unicode-monkeypatch.lua"))
return false;
if (lua_pcall(L, 0, 0, 0)) {
return false; // leave error message
}
#endif

luaL_loadstring(L, "return require('moonscript').loadstring");
// Load mandatory support by exact path. In particular, do not allow a
// moonscript.lua beside an approved subtitle-local script to run first.
if (!load_support_file(L, support_path / "moonscript.lua"))
return false;
if (lua_pcall(L, 0, 1, 0)) {
return false; // leave error message
}
if (!lua_istable(L, -1)) {
lua_pop(L, 1);
lua_pushliteral(L, "Bundled moonscript.lua did not return a module table");
return false;
}

lua_getfield(L, -1, "loadstring");
if (!lua_isfunction(L, -1)) {
lua_pop(L, 2);
lua_pushliteral(L, "Bundled moonscript.lua has no loadstring function");
return false;
}
lua_setfield(L, LUA_REGISTRYINDEX, "moonscript");

// Direct loading bypasses require(), so record the trusted module as
// loaded before user code gets control.
lua_getglobal(L, "package");
lua_getfield(L, -1, "loaded");
lua_pushvalue(L, -3);
lua_setfield(L, -2, "moonscript");
lua_pop(L, 3); // loaded, package, module

return true;
}
}
23 changes: 15 additions & 8 deletions src/ass_attachment.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -26,11 +26,16 @@
// Out-of-line to anchor vtable
AssEntryGroup AssAttachment::Group() const { return group; }

AssAttachment::AssAttachment(std::string const& header, AssEntryGroup group)
: entry_data(header + "\r\n")
, filename(header.substr(10))
AssAttachment::AssAttachment(std::string const& data, AssEntryGroup group)
: entry_data(data)
, group(group)
{
std::string filename_str = data.substr(10);
const auto pos = filename_str.find("\r\n");
if (pos != std::string::npos)
filename_str.resize(pos);

filename = filename_str;
}

AssAttachment::AssAttachment(agi::fs::path const& name, AssEntryGroup group)
Expand All @@ -49,14 +54,16 @@ AssAttachment::AssAttachment(agi::fs::path const& name, AssEntryGroup group)
}

size_t AssAttachment::GetSize() const {
auto header_end = entry_data.get().find('\n');
return entry_data.get().size() - header_end - 1;
auto const& data = entry_data.get();
auto header_end = data.find('\n');
return data.size() - header_end - 1;
}

void AssAttachment::Extract(agi::fs::path const& filename) const {
auto header_end = entry_data.get().find('\n');
auto decoded = agi::ass::UUDecode(entry_data.get().c_str() + header_end + 1, &entry_data.get().back() + 1);
agi::io::Save(filename, true).Get().write(&decoded[0], decoded.size());
auto const& data = entry_data.get();
auto header_end = data.find('\n');
auto decoded = agi::ass::UUDecode(data.c_str() + header_end + 1, &data.back() + 1);
agi::io::Save(filename, true).Get().write(decoded.data(), decoded.size());
}

std::string AssAttachment::GetFileName(bool raw) const {
Expand Down
5 changes: 1 addition & 4 deletions src/ass_attachment.h
Original file line number Diff line number Diff line change
Expand Up @@ -33,9 +33,6 @@ class AssAttachment final : public AssEntry {
/// Get the size of the attached file in bytes
size_t GetSize() const;

/// Add a line of data (without newline) read from a subtitle file
void AddData(std::string const& data) { entry_data = entry_data.get() + data + "\r\n"; }

/// Extract the contents of this attachment to a file
/// @param filename Path to save the attachment to
void Extract(agi::fs::path const& filename) const;
Expand All @@ -47,6 +44,6 @@ class AssAttachment final : public AssEntry {
std::string const& GetEntryData() const { return entry_data; }
AssEntryGroup Group() const override;

AssAttachment(std::string const& header, AssEntryGroup group);
AssAttachment(std::string const& data, AssEntryGroup group);
AssAttachment(agi::fs::path const& name, AssEntryGroup group);
};
5 changes: 4 additions & 1 deletion src/ass_override.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -362,7 +362,10 @@ void parse_parameters(AssOverrideTag *tag, std::string_view text, AssOverrideTag
std::vector<std::string> paramList = tokenize(text);
size_t totalPars = paramList.size();

int parsFlag = 1 << (totalPars - 1); // Get optional parameters flag
unsigned parsFlag = totalPars > 0 && totalPars <= 8
? 1u << (totalPars - 1)
: 0;

// vector (i)clip is the second clip proto_ittype in the list
if ((tag->Name == "\\clip" || tag->Name == "\\iclip") && totalPars != 4) {
++proto_it;
Expand Down
Loading
Loading