From e856b2908700a655f078be18a0b2be7a1286187f Mon Sep 17 00:00:00 2001 From: Alexander Clawthorne <55900620+alexander-clawthorne@users.noreply.github.com> Date: Sun, 4 Oct 2026 17:25:57 -0500 Subject: [PATCH 1/3] fix(video): load the H.264 bootstrap cache without statx loadBootstrap sized the cache with File.getEndPos(), which goes through File.stat() and statx. Kernel 4.9 has no statx, so on H700 devices the call failed, the error was swallowed and the saved SPS/PPS were never loaded, although they were written after every session. Read the file into a buffer one byte larger than the limit instead, which also detects oversized files, and log read failures in debug mode. Co-Authored-By: Claude Opus 5.5 --- src/media/video/video_pipeline.zig | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/media/video/video_pipeline.zig b/src/media/video/video_pipeline.zig index 9dded60..5c0c183 100644 --- a/src/media/video/video_pipeline.zig +++ b/src/media/video/video_pipeline.zig @@ -151,10 +151,16 @@ fn loadBootstrap(pipeline: *Pipeline) c_int { return 0; }; defer file.close(); - const size = file.getEndPos() catch return 0; + // Size the cache by reading it: getEndPos() goes through File.stat(), which needs + // statx and fails on the 4.9 kernels H700 devices ship. One spare byte detects an + // oversized file. + var data: [bootstrap_capacity + 1]u8 = undefined; + const size = file.readAll(&data) catch |err| { + if (debugEnabled()) + std.debug.print("Ignoring unreadable H.264 bootstrap cache: {s}\n", .{@errorName(err)}); + return 0; + }; if (size == 0 or size > bootstrap_capacity) return 0; - var data: [bootstrap_capacity]u8 = undefined; - file.reader().readNoEof(data[0..@intCast(size)]) catch return 0; if (c.go_h264_depacketizer_set_bootstrap(pipeline.depacketizer, &data, @intCast(size)) != 0) { if (debugEnabled()) std.debug.print("Ignoring invalid H.264 bootstrap cache\n", .{}); return 0; From c07d5deee1954239524596438f5f664607e1ae97 Mon Sep 17 00:00:00 2001 From: Alexander Clawthorne <55900620+alexander-clawthorne@users.noreply.github.com> Date: Sun, 4 Oct 2026 18:24:29 -0500 Subject: [PATCH 2/3] fix(video): log when the H.264 bootstrap cache is oversized An oversized cache was rejected without a message, so in debug logs it looked the same as a missing one. Log it like the other skipped-cache cases. Co-Authored-By: Claude Opus 5.5 --- src/media/video/video_pipeline.zig | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/media/video/video_pipeline.zig b/src/media/video/video_pipeline.zig index 5c0c183..39a7255 100644 --- a/src/media/video/video_pipeline.zig +++ b/src/media/video/video_pipeline.zig @@ -160,7 +160,12 @@ fn loadBootstrap(pipeline: *Pipeline) c_int { std.debug.print("Ignoring unreadable H.264 bootstrap cache: {s}\n", .{@errorName(err)}); return 0; }; - if (size == 0 or size > bootstrap_capacity) return 0; + if (size == 0) return 0; + if (size > bootstrap_capacity) { + if (debugEnabled()) + std.debug.print("Ignoring oversized H.264 bootstrap cache (over {d} bytes)\n", .{bootstrap_capacity}); + return 0; + } if (c.go_h264_depacketizer_set_bootstrap(pipeline.depacketizer, &data, @intCast(size)) != 0) { if (debugEnabled()) std.debug.print("Ignoring invalid H.264 bootstrap cache\n", .{}); return 0; From 85bacb1dff5cbeecd062545bc21393eba59a00de Mon Sep 17 00:00:00 2001 From: Producdevity Date: Sat, 17 Oct 2026 14:22:33 +0200 Subject: [PATCH 3/3] test(video): cover bootstrap cache loading without statx --- build.zig | 8 ++ src/media/video/bootstrap_cache.zig | 129 ++++++++++++++++++++++++++++ src/media/video/video_pipeline.zig | 32 +++---- 3 files changed, 148 insertions(+), 21 deletions(-) create mode 100644 src/media/video/bootstrap_cache.zig diff --git a/build.zig b/build.zig index d930e64..3163829 100644 --- a/build.zig +++ b/build.zig @@ -576,4 +576,12 @@ pub fn build(b: *std.Build) void { const run_mpp_decoder_test = b.addRunArtifact(mpp_decoder_test); run_mpp_decoder_test.addArtifactArg(fake_mpp_valid); test_step.dependOn(&run_mpp_decoder_test.step); + + const bootstrap_tests = b.addTest(.{ + .root_source_file = b.path("src/media/video/bootstrap_cache.zig"), + .target = b.graph.host, + .optimize = .Debug, + }); + addZigImport(b, bootstrap_tests.root_module, b.graph.host, .Debug, "h264_depacketizer", "src/media/rtp/h264_depacketizer.zig"); + test_step.dependOn(&b.addRunArtifact(bootstrap_tests).step); } diff --git a/src/media/video/bootstrap_cache.zig b/src/media/video/bootstrap_cache.zig new file mode 100644 index 0000000..f6fba5b --- /dev/null +++ b/src/media/video/bootstrap_cache.zig @@ -0,0 +1,129 @@ +const std = @import("std"); +const builtin = @import("builtin"); + +pub const capacity = 4104; + +pub fn read(directory: std.fs.Dir, path: []const u8, buffer: *[capacity + 1]u8) ![]const u8 { + const file = try directory.openFile(path, .{}); + defer file.close(); + // Zig's File.stat() uses statx, which Linux 4.9 does not provide. + const length = try file.readAll(buffer); + if (length > capacity) return error.FileTooBig; + return buffer[0..length]; +} + +const parameter_sets = [_]u8{ 0, 0, 0, 1, 0x67, 0x42, 0, 0, 0, 1, 0x68, 0xce }; + +test "cache reads preserve parameter sets" { + var directory = std.testing.tmpDir(.{}); + defer directory.cleanup(); + try directory.dir.writeFile(.{ .sub_path = "cache.bin", .data = ¶meter_sets }); + var buffer: [capacity + 1]u8 = undefined; + const data = try read(directory.dir, "cache.bin", &buffer); + try std.testing.expectEqualSlices(u8, ¶meter_sets, data); + + const h264 = @import("h264_depacketizer"); + var depacketizer = h264.Depacketizer.init(102); + try std.testing.expect(depacketizer.setBootstrap(data)); + @memset(&buffer, 0); + try std.testing.expectEqualSlices(u8, &.{ 0x67, 0x42 }, depacketizer.sps[0..depacketizer.sps_length]); + try std.testing.expectEqualSlices(u8, &.{ 0x68, 0xce }, depacketizer.pps[0..depacketizer.pps_length]); +} + +test "empty cache returns no parameter sets" { + var directory = std.testing.tmpDir(.{}); + defer directory.cleanup(); + try directory.dir.writeFile(.{ .sub_path = "cache.bin", .data = "" }); + var buffer: [capacity + 1]u8 = undefined; + try std.testing.expectEqual(@as(usize, 0), (try read(directory.dir, "cache.bin", &buffer)).len); +} + +test "malformed cache does not replace active parameter sets" { + var directory = std.testing.tmpDir(.{}); + defer directory.cleanup(); + try directory.dir.writeFile(.{ .sub_path = "cache.bin", .data = "invalid H.264" }); + var buffer: [capacity + 1]u8 = undefined; + const data = try read(directory.dir, "cache.bin", &buffer); + const h264 = @import("h264_depacketizer"); + var depacketizer = h264.Depacketizer.init(102); + try std.testing.expect(depacketizer.setBootstrap(¶meter_sets)); + try std.testing.expect(!depacketizer.setBootstrap(data)); + try std.testing.expectEqualSlices(u8, &.{ 0x67, 0x42 }, depacketizer.sps[0..depacketizer.sps_length]); + try std.testing.expectEqualSlices(u8, &.{ 0x68, 0xce }, depacketizer.pps[0..depacketizer.pps_length]); +} + +test "cache accepts the exact capacity and rejects one extra byte" { + var directory = std.testing.tmpDir(.{}); + defer directory.cleanup(); + var data = [_]u8{0x55} ** (capacity + 1); + @memcpy(data[0..5], &[_]u8{ 0, 0, 0, 1, 0x67 }); + @memcpy(data[2052..2057], &[_]u8{ 0, 0, 0, 1, 0x68 }); + try directory.dir.writeFile(.{ .sub_path = "cache.bin", .data = data[0..capacity] }); + var buffer: [capacity + 1]u8 = undefined; + const loaded = try read(directory.dir, "cache.bin", &buffer); + try std.testing.expectEqualSlices(u8, data[0..capacity], loaded); + const h264 = @import("h264_depacketizer"); + var depacketizer = h264.Depacketizer.init(102); + try std.testing.expect(depacketizer.setBootstrap(loaded)); + + try directory.dir.writeFile(.{ .sub_path = "cache.bin", .data = &data }); + try std.testing.expectError(error.FileTooBig, read(directory.dir, "cache.bin", &buffer)); +} + +test "large cache is rejected rather than parsed as a prefix" { + var directory = std.testing.tmpDir(.{}); + defer directory.cleanup(); + var data = [_]u8{0x55} ** 16384; + @memcpy(data[0..parameter_sets.len], ¶meter_sets); + try directory.dir.writeFile(.{ .sub_path = "cache.bin", .data = &data }); + var buffer: [capacity + 1]u8 = undefined; + try std.testing.expectError(error.FileTooBig, read(directory.dir, "cache.bin", &buffer)); +} + +test "missing cache reports FileNotFound" { + var directory = std.testing.tmpDir(.{}); + defer directory.cleanup(); + var buffer: [capacity + 1]u8 = undefined; + try std.testing.expectError(error.FileNotFound, read(directory.dir, "missing.bin", &buffer)); +} + +test "cache read errors are returned" { + var directory = std.testing.tmpDir(.{}); + defer directory.cleanup(); + try directory.dir.makeDir("not-a-file"); + var buffer: [capacity + 1]u8 = undefined; + try std.testing.expectError(error.IsDir, read(directory.dir, "not-a-file", &buffer)); +} + +test "cache loads when the kernel has no statx syscall" { + if (builtin.os.tag != .linux) return error.SkipZigTest; + var directory = std.testing.tmpDir(.{}); + defer directory.cleanup(); + try directory.dir.writeFile(.{ .sub_path = "cache.bin", .data = ¶meter_sets }); + + const pid = try std.posix.fork(); + if (pid == 0) { + const linux = std.os.linux; + const bpf = linux.BPF; + const Filter = extern struct { code: u16, jt: u8 = 0, jf: u8 = 0, k: u32 }; + const Program = extern struct { len: u16, filter: [*]const Filter }; + const filters = [_]Filter{ + .{ .code = bpf.LD | bpf.W | bpf.ABS, .k = @offsetOf(linux.SECCOMP.data, "nr") }, + .{ .code = bpf.JMP | bpf.JEQ | bpf.K, .jf = 1, .k = @intFromEnum(linux.SYS.statx) }, + .{ .code = bpf.RET | bpf.K, .k = linux.SECCOMP.RET.ERRNO | @as(u32, @intFromEnum(linux.E.NOSYS)) }, + .{ .code = bpf.RET | bpf.K, .k = linux.SECCOMP.RET.ALLOW }, + }; + const program = Program{ .len = filters.len, .filter = &filters }; + // Keep the filter isolated from the test runner. + if (linux.prctl(@intFromEnum(linux.PR.SET_NO_NEW_PRIVS), 1, 0, 0, 0) != 0 or + linux.seccomp(linux.SECCOMP.SET_MODE_FILTER, 0, &program) != 0) linux.exit(1); + var stat: linux.Statx = undefined; + if (linux.E.init(linux.statx(-1, "", 0, 0, &stat)) != .NOSYS) linux.exit(2); + var buffer: [capacity + 1]u8 = undefined; + const data = read(directory.dir, "cache.bin", &buffer) catch linux.exit(3); + linux.exit(if (std.mem.eql(u8, ¶meter_sets, data)) 0 else 4); + } + const result = std.posix.waitpid(pid, 0); + try std.testing.expect(std.posix.W.IFEXITED(result.status)); + try std.testing.expectEqual(@as(u8, 0), std.posix.W.EXITSTATUS(result.status)); +} diff --git a/src/media/video/video_pipeline.zig b/src/media/video/video_pipeline.zig index 39a7255..458bf72 100644 --- a/src/media/video/video_pipeline.zig +++ b/src/media/video/video_pipeline.zig @@ -1,4 +1,5 @@ const std = @import("std"); +const bootstrap_cache = @import("bootstrap_cache.zig"); const c = @cImport({ @cInclude("SDL2/SDL.h"); @@ -12,7 +13,7 @@ const c = @cImport({ @cInclude("video_pipeline.h"); }); -const bootstrap_capacity = 4104; +const bootstrap_capacity = bootstrap_cache.capacity; const packet_capacity = 4096; const queue_capacity = 128; const backpressure_attempts = 64; @@ -145,32 +146,21 @@ fn zSlice(buffer: []const u8) []const u8 { fn loadBootstrap(pipeline: *Pipeline) c_int { const path = zSlice(&pipeline.bootstrap_path); if (path.len == 0) return 0; - const file = std.fs.cwd().openFile(path, .{}) catch |err| { - if (err != error.FileNotFound and debugEnabled()) - std.debug.print("Ignoring unreadable H.264 bootstrap cache: {s}\n", .{@errorName(err)}); - return 0; - }; - defer file.close(); - // Size the cache by reading it: getEndPos() goes through File.stat(), which needs - // statx and fails on the 4.9 kernels H700 devices ship. One spare byte detects an - // oversized file. var data: [bootstrap_capacity + 1]u8 = undefined; - const size = file.readAll(&data) catch |err| { - if (debugEnabled()) - std.debug.print("Ignoring unreadable H.264 bootstrap cache: {s}\n", .{@errorName(err)}); + const cached = bootstrap_cache.read(std.fs.cwd(), path, &data) catch |err| { + if (debugEnabled()) switch (err) { + error.FileNotFound => {}, + error.FileTooBig => std.debug.print("Ignoring oversized H.264 bootstrap cache (over {d} bytes)\n", .{bootstrap_capacity}), + else => std.debug.print("Ignoring unreadable H.264 bootstrap cache: {s}\n", .{@errorName(err)}), + }; return 0; }; - if (size == 0) return 0; - if (size > bootstrap_capacity) { - if (debugEnabled()) - std.debug.print("Ignoring oversized H.264 bootstrap cache (over {d} bytes)\n", .{bootstrap_capacity}); - return 0; - } - if (c.go_h264_depacketizer_set_bootstrap(pipeline.depacketizer, &data, @intCast(size)) != 0) { + if (cached.len == 0) return 0; + if (c.go_h264_depacketizer_set_bootstrap(pipeline.depacketizer, cached.ptr, cached.len) != 0) { if (debugEnabled()) std.debug.print("Ignoring invalid H.264 bootstrap cache\n", .{}); return 0; } - if (debugEnabled()) std.debug.print("H.264 bootstrap loaded ({d} bytes)\n", .{size}); + if (debugEnabled()) std.debug.print("H.264 bootstrap loaded ({d} bytes)\n", .{cached.len}); return 0; }