Files
PlantAI-rag-new/tooling/compile/tools/elf_visibility.py
jerryandClaude Fable 5 b813cff005 P1/M2: tooling/compile/compile_qa.sh(参数化 + normalized_release 补丁)
- 收编工具链遗漏主项目 08-29 新增 src/rag/normalized_release.py(backend_guard 依赖)
- 编译树 45 模块全部 .so,audit 0 错误,7922 健康检查金标过
- 修复:compile 目录相对依赖 tools/(elf_visibility.py)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-31 13:26:18 +08:00

206 lines
6.7 KiB
Python
Executable File

#!/usr/bin/env python3
"""Inspect or hide non-PyInit defined symbols in an ELF64 little-endian .dynsym."""
from __future__ import annotations
import argparse
import json
import os
import struct
import tempfile
from pathlib import Path
from typing import Any
STN_UNDEF = 0
STV_DEFAULT = 0
STV_HIDDEN = 2
def c_string(data: bytes | bytearray, offset: int) -> str:
end = data.find(b"\0", offset)
if end < 0:
end = len(data)
return bytes(data[offset:end]).decode("utf-8", errors="replace")
def parse_sections(data: bytes | bytearray) -> tuple[list[dict[str, Any]], bytes]:
if data[:4] != b"\x7fELF":
raise ValueError("not an ELF file")
if data[4] != 2:
raise ValueError("only ELF64 is supported")
if data[5] != 1:
raise ValueError("only little-endian ELF is supported")
section_offset = struct.unpack_from("<Q", data, 40)[0]
section_entry_size = struct.unpack_from("<H", data, 58)[0]
section_count = struct.unpack_from("<H", data, 60)[0]
section_name_index = struct.unpack_from("<H", data, 62)[0]
if section_entry_size < 64 or not section_count:
raise ValueError("invalid or missing section table")
sections = []
for index in range(section_count):
offset = section_offset + index * section_entry_size
fields = struct.unpack_from("<IIQQQQIIQQ", data, offset)
sections.append(
{
"index": index,
"header_offset": offset,
"name_offset": fields[0],
"type": fields[1],
"flags": fields[2],
"address": fields[3],
"offset": fields[4],
"size": fields[5],
"link": fields[6],
"info": fields[7],
"alignment": fields[8],
"entry_size": fields[9],
}
)
if section_name_index >= len(sections):
raise ValueError("invalid section-name string-table index")
names_section = sections[section_name_index]
names = bytes(
data[
names_section["offset"] : names_section["offset"] + names_section["size"]
]
)
for section in sections:
section["name"] = c_string(names, section["name_offset"])
return sections, names
def inspect_dynsym(data: bytes | bytearray) -> tuple[list[dict[str, Any]], dict[str, Any]]:
sections, _ = parse_sections(data)
dynsym = next((section for section in sections if section["name"] == ".dynsym"), None)
if dynsym is None:
raise ValueError("missing .dynsym")
if dynsym["link"] >= len(sections):
raise ValueError("invalid .dynsym string-table link")
dynstr_section = sections[dynsym["link"]]
dynstr = data[
dynstr_section["offset"] : dynstr_section["offset"] + dynstr_section["size"]
]
entry_size = dynsym["entry_size"] or 24
if entry_size < 24:
raise ValueError("invalid ELF64 symbol entry size")
symbols = []
count = dynsym["size"] // entry_size
for index in range(count):
offset = dynsym["offset"] + index * entry_size
name_offset = struct.unpack_from("<I", data, offset)[0]
info = data[offset + 4]
other = data[offset + 5]
section_index = struct.unpack_from("<H", data, offset + 6)[0]
value, size = struct.unpack_from("<QQ", data, offset + 8)
name = c_string(dynstr, name_offset) if name_offset < len(dynstr) else ""
symbols.append(
{
"index": index,
"entry_offset": offset,
"name": name,
"binding": info >> 4,
"type": info & 0xF,
"visibility": other & 0x3,
"section_index": section_index,
"value": value,
"size": size,
"defined": section_index != STN_UNDEF,
}
)
return symbols, dynsym
def summarize(path: Path, data: bytes | bytearray) -> dict[str, Any]:
sections, _ = parse_sections(data)
symbols, _ = inspect_dynsym(data)
defined = [symbol for symbol in symbols if symbol["defined"] and symbol["name"]]
pyinit = [symbol for symbol in defined if symbol["name"].startswith("PyInit_")]
non_pyinit = [
symbol for symbol in defined if not symbol["name"].startswith("PyInit_")
]
return {
"path": str(path),
"has_symtab": any(section["name"] == ".symtab" for section in sections),
"defined_dynamic_count": len(defined),
"pyinit": [
{"name": symbol["name"], "visibility": symbol["visibility"]}
for symbol in pyinit
],
"non_pyinit": [
{"name": symbol["name"], "visibility": symbol["visibility"]}
for symbol in non_pyinit
],
"all_non_pyinit_hidden": all(
symbol["visibility"] == STV_HIDDEN for symbol in non_pyinit
),
"all_pyinit_default": bool(pyinit)
and all(symbol["visibility"] == STV_DEFAULT for symbol in pyinit),
}
def hide_non_pyinit(path: Path) -> dict[str, Any]:
original = path.read_bytes()
data = bytearray(original)
symbols, _ = inspect_dynsym(data)
changed = []
for symbol in symbols:
if (
symbol["defined"]
and symbol["name"]
and not symbol["name"].startswith("PyInit_")
and symbol["visibility"] != STV_HIDDEN
):
offset = symbol["entry_offset"] + 5
data[offset] = (data[offset] & ~0x3) | STV_HIDDEN
changed.append(symbol["name"])
if changed:
stat = path.stat()
with tempfile.NamedTemporaryFile(
mode="wb", dir=path.parent, prefix=f".{path.name}.", delete=False
) as handle:
temporary = Path(handle.name)
handle.write(data)
handle.flush()
os.fsync(handle.fileno())
os.chmod(temporary, stat.st_mode)
os.replace(temporary, path)
result = summarize(path, data)
result["changed"] = changed
return result
def main() -> int:
parser = argparse.ArgumentParser()
parser.add_argument("elf", type=Path)
parser.add_argument("--hide-non-pyinit", action="store_true")
parser.add_argument("--check", action="store_true")
args = parser.parse_args()
path = args.elf.resolve()
result = (
hide_non_pyinit(path)
if args.hide_non_pyinit
else summarize(path, path.read_bytes())
)
print(json.dumps(result, ensure_ascii=False, sort_keys=True))
if args.check:
valid = (
not result["has_symtab"]
and result["all_pyinit_default"]
and result["all_non_pyinit_hidden"]
and len(result["pyinit"]) == 1
)
return 0 if valid else 1
return 0
if __name__ == "__main__":
raise SystemExit(main())