Ctrl K
ComunidadGuías…

utils/check-shareddata-abi.py

Rama master · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
#!/usr/bin/env python3
"""Fail if the SharedData shared-memory layout differs between 32bit and 64bit.

zmc, zma and zms share the Monitor::SharedData struct through /dev/shm, and it
is also read by the Perl (ZoneMinder::Memory) and PHP (web/includes/Monitor.php)
SHM readers. Both of those assume 8-byte alignment for double/uint64/time_t
unconditionally, i.e. the x86-64 layout, on every architecture.

The struct is not packed, so that assumption only holds if the C++ struct is
laid out identically on both word sizes. It is not automatic: the i386 SysV ABI
aligns double and uint64_t to 4 bytes where x86-64 aligns them to 8, which drops
interior padding and shifts every later member. Explicit epadding members in the
struct compensate; this check proves they still do.

The struct body and its static_asserts are lifted verbatim out of zm_monitor.h,
so the expected size lives in exactly one place (the header) and is never
duplicated here. Requires a compiler that can target both -m32 and -m64
(gcc-multilib on Debian/Ubuntu). Run from the repo root:

    python3 utils/check-shareddata-abi.py
"""
import os
import re
import subprocess
import sys
import tempfile

HEADER = os.path.join('src', 'zm_monitor.h')

# Members are declared as `<type> name;` or `<type> name[N];`, where the scalar
# ones are wrapped as `SharedValue<type> name;`. Anonymous unions are handled
# separately since their members are what callers actually name.
SCALAR = r'(?:uint\d+_t|int\d+_t|double|char)'
MEMBER_RE = re.compile(
    r'^\s*(?:{0}|SharedValue<{0}>)\s+(\w+)\s*(?:\[\d+\])?\s*;'.format(SCALAR), re.M)
UNION_MEMBER_RE = re.compile(
    r'^\s*(?:time_t|SharedValue<time_t>)\s+(\w+)\s*;', re.M)
# The wrapper the scalar members are declared with, which the probe needs too.
SHARED_VALUE_START = 'template <typename T>\n  class SharedValue {'


def fail(msg):
    print('ERROR: {}'.format(msg))
    print('\nThis check compiles Monitor::SharedData from {} at -m32 and -m64\n'
          'and requires the layouts to be identical.'.format(HEADER))
    return 1


def extract(src):
    """Pull the SharedData struct body, the SharedValue template it is declared
    with (empty if the header has none) and their static_asserts out of the
    header."""
    if 'typedef struct {' not in src or '} SharedData;' not in src:
        return None, None, None
    body = src.split('typedef struct {', 1)[1].split('} SharedData;', 1)[0]
    shared_value = ''
    if SHARED_VALUE_START in src:
        start = src.index(SHARED_VALUE_START)
        shared_value = src[start:src.index('\n  };', start) + len('\n  };')]
    # Only the asserts that constrain SharedData or SharedValue.
    asserts = [ln.strip() for ln in src.split('\n')
               if ln.strip().startswith('static_assert(')
               and ('SharedData' in ln or 'SharedValue' in ln)]
    return body, shared_value, asserts


def build_probe(body, shared_value, asserts):
    names = list(dict.fromkeys(
        MEMBER_RE.findall(body) + UNION_MEMBER_RE.findall(body)))
    if not names:
        return None, None
    out = ['#include <cstdio>', '#include <cstdint>', '#include <ctime>',
           '#include <cstddef>', '#include <type_traits>', shared_value,
           'typedef struct {' + body + '} SharedData;']
    out.extend(asserts)
    out.append('int main() {')
    out.append('  printf("sizeof %zu\\n", sizeof(SharedData));')
    for n in names:
        out.append('  printf("%-32s %zu\\n", "{0}", offsetof(SharedData, {0}));'
                   .format(n))
    out.append('  return 0;\n}')
    return '\n'.join(out), names


def run_for_bits(cxx, source, bits, workdir):
    exe = os.path.join(workdir, 'probe{}'.format(bits))
    compile_proc = subprocess.run(
        [cxx, '-m{}'.format(bits), '-std=c++17', '-w', source, '-o', exe],
        capture_output=True, text=True)
    if compile_proc.returncode != 0:
        return None, compile_proc.stderr
    run_proc = subprocess.run([exe], capture_output=True, text=True)
    if run_proc.returncode != 0:
        return None, run_proc.stderr
    return run_proc.stdout, None


def main():
    if not os.path.exists(HEADER):
        return fail('{} not found; run from the repository root.'.format(HEADER))

    body, shared_value, asserts = extract(open(HEADER, encoding='utf-8').read())
    if body is None:
        return fail('could not locate the SharedData struct in {}. If it was '
                    'renamed or reformatted, update this script.'.format(HEADER))
    if not asserts:
        return fail('no static_assert on SharedData found in {}. The ABI guard '
                    'must not be removed.'.format(HEADER))

    source_text, names = build_probe(body, shared_value, asserts)
    if source_text is None:
        return fail('parsed the SharedData struct but found no members; the '
                    'declaration style likely changed.')

    cxx = os.environ.get('CXX', 'g++')
    with tempfile.TemporaryDirectory() as workdir:
        source = os.path.join(workdir, 'probe.cpp')
        with open(source, 'w', encoding='utf-8') as handle:
            handle.write(source_text)

        results = {}
        for bits in (64, 32):
            stdout, err = run_for_bits(cxx, source, bits, workdir)
            if stdout is None:
                if 'static assertion' in (err or ''):
                    print('SharedData static_assert failed at -m{}:\n'.format(bits))
                    for line in (err or '').splitlines():
                        # Every error, not only the asserts: a member the probe
                        # cannot compile drops out and shows up as a size error.
                        if 'error:' in line or 'reduces to' in line:
                            print('  ' + line.strip())
                    return fail('the {}bit layout violates the header\'s own '
                                'assertions.'.format(bits))
                print(err or '')
                return fail('could not build the {}bit probe with {}. Install a '
                            'multilib toolchain (gcc-multilib).'.format(bits, cxx))
            results[bits] = stdout

    if results[32] != results[64]:
        print('SharedData layout differs between 32bit and 64bit:\n')
        left = dict(l.rsplit(None, 1) for l in results[64].strip().split('\n'))
        right = dict(l.rsplit(None, 1) for l in results[32].strip().split('\n'))
        print('  {:<32} {:>8} {:>8}'.format('member', '64bit', '32bit'))
        for key in left:
            if left[key] != right.get(key):
                print('  {:<32} {:>8} {:>8}'.format(
                    key.strip(), left[key], right.get(key, '?')))
        print('\nAdd or adjust the epadding members in {} so both agree.'
              .format(HEADER))
        return 1

    size = results[64].split('\n', 1)[0].split()[1]
    print('OK: SharedData is {} bytes with identical offsets at -m32 and -m64 '
          '({} members checked).'.format(size, len(names)))
    return 0


if __name__ == '__main__':
    sys.exit(main())