Repository navigation
v4.9.0
Added
-
smda.export: the engine that turns a disassembler frontend's analysis into a report, and
Disassembler.setExporter(exporter)to pin one, whichida_domain_export.pyand downstream
callers used to do through a private flag. (#360) -
(binja) Export Binary Ninja's analysis as a SMDA report, the way
smda.idadoes for IDA.
smda.binja.BinjaInterfacereads functions, blocks, edges, symbols and imports from a
BinaryViewand feedssmda.export.Exporter,smda.binja.BinjaExporter.exportBinaryView(bv)
wraps the two, and the scripts match the IDA ones:binja_export.pyexports the open view from
Run Script... or a binary or.bndbheadlessly, andbinja_analyze.pyadds SMDA's recovered
functions and names to the open view as one undo step. Ported from the MCRIT plugin, where the
exporter was versioned by the plugin's releases instead of the report format it produces. No new
dependency: Binary Ninja's API comes with the application, and the module imports without it.
Measured with Binary Ninja 6.0 on an x64 PE DLL, two x86_64 Mach-O binaries and/bin/ls:
reports identical to the plugin's exporter exceptoep, which ELF and Mach-O reports now record
relative to the base address as native SMDA reports and IDA exports do (the plugin recorded an
absolute address; PE entry points were already RVAs). Not reproducible from the repository:
none of the binaries is bundled and the comparison needs a licensed Binary Ninja. No cost to the
disassembly backends, which it does not touch. (#357)
Changed
-
(cli)
analyze.pydetects container formats on its own, so-p/--parse_headeris no longer
needed to get a PE/ELF/Mach-O/Delphi-KB/DEX file mapped and its symbols parsed. Routing is decided
byshouldParseHeader(), which asks every loader inFileLoader.file_loaderswhether it claims
the bytes -- the same listFileLoaderdispatches on, not a second copy of the magic. An explicit
-a/--base_addror-i/--oepstill selects raw buffer mode, because a dump of a mapped image
begins with the header it was mapped from and the caller naming its base address is describing
exactly that -- and so does a base address in the file name, the_0x<addr>form
parseBaseAddrFromArgs()already read, since a memory-dumped PE still starts withMZand would
otherwise be mapped a second time;-pkeeps working and overrides all of them. DEX is unchanged
in output: routed throughdisassembleFiletheblockblastfixture yields the same 2,219
functions / 2,527 blocks / 9,824 instructions at base 0 as the buffer path. Detection needs the bytes, sodisassembleFile()and
FileLoadertake an optionalbufferthe caller has already read and skip reading the file a
second time; omitting it loads from the path as before. -
(build) Correct the package summary and describe the package to PyPI properly. The summary
shippeddisassmblerin every release's metadata, which is also the string PyPI's search index
matches on. The classifier list gainedTyping :: Typed--py.typedhas shipped since the
package-data entry was added, with nothing advertising it -- plus the twoIntended Audience
entries,Programming Language :: Python :: 3 :: Only, and akeywordslist, none of which
PyPI could infer. No code, no runtime dependency and no report field moves. -
(build) Stop shipping
tests/in the sdist. setuptools' default rules picked up every
.pyfile under the root but not the extension-less xored corpora they read, so the published
sdist carried 1.6 MB of a test suite that could not run from it -- and the corpora are live
malware, which is not something to publish to an index either. AMANIFEST.inprunes it; the
sdist goes 3.12 MB -> 1.49 MB and the wheel is unaffected. Run the suite from a clone. -
The export engine moved out of
smda.idaintosmda.export.Exporter(config, interface)builds
the report from anyBackendInterface, andBackendInterfacenow declares exactly the methods
the engine reads:getApiMapandisExternalFunctionadded, the never-calledgetApiOffsets
dropped. The engine was IDA-only in name: the interface it reads has had two IDA implementations
since the IDA Domain backend, and nothing in it touches IDA, so a second frontend should not
import it from the first.smda.ida.IdaExporterandsmda.ida.BackendInterfaceremain as
before, so no caller changes; the engine's code moved verbatim. (#360) -
(ci) Pull requests only start the jobs their diff can move. A
Changed pathsjob reads
the PR file list and gates the rest: the MCRIT install matrix needs a packaging change, lint
and tests skip a documentation-only PR, the two security audits need a dependency or.github/
change, the benchmark workflow triggers only on files that can move output or timing and skips
Malpedia for changes confined to the AArch64, CIL or Dalvik backends, and each fuzz target runs
only when its region of the tree was touched.push,scheduleandworkflow_dispatchstill
run every job. (#359)
Fixed
-
(report) Refuse an ELF or Mach-O synthesis whose section span exceeds
SmdaConfig.MAX_IMAGE_SIZE, instead
of allocating it._resolveFunctionOffsetsbounded the span from the first function offset to the last
function's extent end, but_syntheticSpanalso stretches the section up to the highest function offset. A
report whose function offset lies far above its own blocks therefore passed the check and asked_nopFillfor
gigabytes.fuzz_synthesisfound it onmasterafter #371 merged, though synthesis has not changed since 4.8.0.
_syntheticSpannow raisesValueErrorover the limit, the same operational error the offset check raises. PE
synthesis, which does not use it, still builds such a report. Reproduced intests/testSynthesis.py: a report
with one function 0x90000000 above its blocks raisesMemoryErrorunder a 4 GiB cap before this change, and
ValueErrorwithout allocating after it. (#376) -
(ci) Classify a large pull request's file list correctly. Every path filter piped the list into
grep -qunderset -o pipefail;grep -qexits on its first match,printfthen dies of SIGPIPE once the
list outgrows the 64 KB pipe buffer, and the pipeline's exit status reads as "no match". A PR of about 1,500
paths or more could therefore skip lint, tests, the audits, the benchmark and fuzzing and still show green, and
pass the changelog check without an entry. The filters now read the list from a here-string. On the same
change,CHANGELOG.mdno longer counts as documentation for the lint/test gate (the release guard tests parse
it),data/counts as benchmark input (run_perf_check.pyloads the ApiScout database from it), and the MCRIT
install matrix runs whenci.ymlitself changes. Measured by running each filter's step script locally against
a stubbed file list: at 3,000 paths the old filters gave the wrong verdict in every workflow and the new ones
the right one; on small lists both agree except for the three filter changes above. (#370) -
(binja) Cap the image
BinjaInterface.getBinary()assembles atSmdaConfig.MAX_IMAGE_SIZE, as the IDA
frontends already do. It allocated from the lowest segment to the highest segment end in one piece, so a view
whose segments lie far apart raisedMemoryErrorinstead of exporting. Both frontends now share
assembleSegmentBuffer, which moved fromsmda.ida.segment_mappingtosmda.export.segment_mapping(the old
module re-exports it); its truncation warning no longer names IDA. No other change to Binary Ninja output: a
segment's unbacked tail pastdata_lengthis still zero-filled and still counts toward the image size. (#371) -
(report) Apply
SmdaConfig.MAX_IMAGE_SIZEwhen a report is written, not only when it is read. Since #350
fromDict()refuses to inflate a stored buffer larger than the limit, buttoDict()still packed any buffer, so
a dump over 100 MiB analysed withSTORE_BUFFER=Truewrote a buffer that its own reload dropped.toDict()now
leaves out a buffer over the limit and logs a warning, so a written report loads back with everything it
contains; the in-memory report keeps its buffer. No cost for buffers within the limit, which serialize as
before. Nothing in the MCRIT family reads a stored buffer (searched 2026-09-28). (#372) -
(cli) Keep shellcode that starts with
MZin raw buffer mode.analyze.py's container detection took any
loader'sisCompatible()as its answer, andPeFileLoader's checks only the twoMZbytes, soMZ-prefixed
shellcode went down the PE header path and could come back as an empty report. For PE, detection now also needs
thePE\0\0signature ate_lfanew;isCompatible()itself, whichFileLoaderdispatches on, is unchanged, and
-pstill forces header parsing. No cost on well-formed PE files, which carry the signature (cutwailfixture). (#373) -
Disassembler(backend="IDA").disassembleFile()no longer fails on the unconditional
addPdbFilecall; the export engine now carries the same no-op the CIL and Dalvik backends have.
(#360)
Security
- (report) Bound stored report-buffer inflation by
SmdaConfig.MAX_IMAGE_SIZEand reject non-native ZIP
layouts before materializing their contents, so a crafted serialized report cannot trigger unbounded
decompression while the remainder of a malformed report stays loadable. (#350)
What's Changed
Changes
- ci: scope the release concurrency group to the trigger by @danielplohmann in #353
- build: prefer the repository venv when resolving PYTHON by @danielplohmann in #354
- refactor: move the export engine out of smda.ida by @r0ny123 in #360
- ci: accept binja as a PR title scope by @r0ny123 in #358
- feat(binja): add Binary Ninja exporter as smda.binja by @r0ny123 in #357
- fix(report): bound stored buffer inflation by MAX_IMAGE_SIZE by @r0ny123 in #350
- feat(cli): auto-detect container formats and modernize the repository furniture by @r0ny123 in #349
- ci: start only the jobs a pull request can move by @r0ny123 in #359
- ci: read the PR file list from a here-string in the path filters by @danielplohmann in #370
- fix(binja): cap the exported image at MAX_IMAGE_SIZE by @danielplohmann in #371
- fix(report): apply MAX_IMAGE_SIZE when writing a stored buffer by @danielplohmann in #372
- fix(cli): keep MZ-prefixed shellcode in raw buffer mode by @danielplohmann in #373
- ci: keep dependency floors and pair the ruff bumps in Dependabot by @danielplohmann in #374
- build: bump ruff to 0.16.9 in both pins by @danielplohmann in #375
- fix(report): refuse a synthesis span beyond MAX_IMAGE_SIZE instead of allocating it by @danielplohmann in #376
- build: release 4.9.0 by @danielplohmann in #377
Dependency and tooling updates
- build(deps-dev): bump ruff from 0.16.6 to 0.16.8 by @dependabot[bot] in #362
- build(deps): bump https://github.com/astral-sh/ruff-pre-commit from v0.16.6 to 0.16.8 by @dependabot[bot] in #361
Full Changelog: v4.8.0...v4.9.0