Skip to content

feat(libSceLibreSsl3): implement ASN.1 integer decoding - #10948

Merged
oneandonlydean merged 6 commits into
boykopovar:mainfrom
AZLisme:feat/libressl3-asn1-accessors
Oct 26, 2026
Merged

oneandonlydean merged 6 commits into
boykopovar:mainfrom
AZLisme:feat/libressl3-asn1-accessors

Conversation

@AZLisme

@AZLisme AZLisme commented Oct 25, 2026 •

Copy link
Copy Markdown
Contributor

What

Implement d2i_ASN1_INTEGER for arbitrary-width signed integers, BER forms, cursor advancement, replacement and guest-heap ownership. The canonical public declaration uses the existing 24-byte ASN.1 string layout. Replacing a prior value uses private cleanup, so no additional public accessor/free export is required.

Contract: LibreSSL 4.0.0 integer decoding and string lifetime. Decoder error-queue paths throw and remain documented in debt; the existing legacy queue is not populated by this decoder. Public ASN1_INTEGER_free and ASN1_STRING_get0_data remain unchanged throwing exports; their implementation is deferred while #10452 is reviewed. This decoder does not depend on #10452.

Tested

Maintainer run 40382921477, attempt 1, completed successfully for unchanged head 81013f41bf09c3d543bb110d4954abb767df0229. Linux and Windows used the exact main base afc780e681fc379a6ed1b19a2a247cae17f0fc07 with this complete ordered five-PR composition:

Windows: 1,689 passed, 815 expected skips, zero failures out of 2,504 registered CTests. Linux: 2,305 passed, 93 expected skips, zero failures out of 2,398. Both skip validators executed and accepted every skip. The normal builds and production libs targets succeeded; both logs show IntegerDecoder.cpp compiled and the actual LibreSSL3 module linked and NID-patched.

All three ASN.1 fixtures passed on each platform: guest_libressl_asn1, guest_libressl_asn1_allocator and guest_libressl3_asn1. Each platform also passed all 65 public-header cases (declaration audit plus 64 compile batches). Registration audits found 1,404 executable targets on Windows and 1,409 on Linux, zero unregistered; these are registration counts, not executed-test counts. All 12 workflow jobs completed successfully. Supplemental matrix results do not establish macOS ASN.1 product coverage or library-ASan validation.

This qualifies the recorded five-PR union, not the standalone PR, a later main, physical-console parity, or fixes for the decoder’s remaining NULL/error-queue/old-output lifetime differences. This update changes the description only. The existing source, dependencies and debt remain unchanged. Statements below about no new runtime results or absent Windows/full builds describe their original historical follow-ups and local qualification.

Contributor evidence: BunnyFl compared the unchanged 77ba69041c decoder with decrypted firmware 13.42 running natively on a PC, expressly not a physical console. For the 147,982 nonthrowing cases in a 200,000-input run, returned values, decoded fields, cursor advancement and output replacement matched; the report describes three identical runs and a further 500,000-input seed with no comparison mismatches. For 52,018 throwing cases, the native module returned NULL and queued errors. The earlier category table listed distinct first error codes, not complete sequences.

The follow-up report supplies full sequences and recording-allocator observations: 60,000 error inputs in three identical runs and 200,000 with a second seed produced the same sequence set. Tested successful replacement frees owned data after clearing it, then the old object, before allocating new data/object; flag 0x10 preserves borrowed data. The module and decoder match that order and tested allocation-failure cleanup. Native first-allocation failure returns NULL without errors; second-allocation failure returns NULL with 0x0DFFF041 twice, while this decoder throws. Redundant-sign/zero-content rejection queues 0x0DFFF03E and frees/clears old output natively, while this decoder throws and retains it. Other tested parse failures preserve old output but return NULL with ordered errors natively. Negative lengths return NULL with no queued error and preserve input/output on both. Nonnegative NULL in/*in faults in the module and throws here. The successful-replacement comparison covered 20,000 inputs in three identical runs plus 79,934 second-seed decodes; allocation and NULL/negative tests also repeated three times, with mutation controls for zeroing/free order.

These are contributor-reported native-PC results with an allocator stand-in, separate from our tests below. They confirm the tested ownership/unwind order and specific remaining differences; they do not establish physical-console/application-heap interoperability or exhaust all malformed/overflow error precedence. The decoder still does not populate the existing legacy queue, and the remaining LibreSSL3 ERR exports still throw. This follow-up updates the debt entry only; decoder/library/test sources and historical runtime qualification remain unchanged. No new runtime result is claimed for this documentation update.

Documentation follow-up 81013f41bf09 updates only the ASN.1 debt entry for the follow-up native-PC report, stating the remaining error-return and old-output lifetime differences first. Every non-debt file, including the entire core tree, is byte-identical to 816ca46c66e9; no decoder, API, allocator, error-queue or test behavior changes. Conventions and whitespace checks pass. A private static merge with main 4e43321bd6ceccdc1ae28568bbbc35d9a6446464 is clean; this is a source-union check, not merged-main runtime qualification. Existing runtime evidence below retains its original scope, without a documentation-only rerun.

Documentation follow-up 816ca46c66e9 qualifies the new native-PC evidence and decoder/error-queue gap in debt. The entire core tree is identical to 77ba69041c5e; only the debt row changes. Existing runtime evidence is retained without a documentation-only rerun. Conventions, whitespace and independent documentation/source review pass, and this revision merges cleanly with main 504ea3b53b23 in a static check.

Review follow-up 77ba69041c5e rebases onto 401731067856 and fixes the debt-entry grammar only. The full LibreSSL3 library/test tree and external test sources are byte-identical to approved 8b87391b3143; conventions and whitespace checks pass. The focused runtime results below are retained from that unchanged source, without a documentation-only rerun.

Linux x86_64, GCC 16.2.1, Release, based on 63a863d3b07f2457ee1210f1abba1c11409cdff5. The complete changed runtime library was built through its normal module CMake, using freshly rebuilt libc/libkernel/libSceLibcInternal from that base. Public-header uniqueness/umbrella audit and duplicate-inclusion syntax compilation passed; conventions and whitespace checks passed. Windows/macOS, PS5 execution and a monolithic repository build were not run. All 3/3 focused tests passed: crypto comparison, existing LibreSSL3 digest, and INTEGER decoding. Decoder coverage includes all 65,536 signed 16-bit values, larger values, successful BER forms, input advancement and preservation, replacement of owned/borrowed values, and secure clearing before release. A fresh original-source check also passed three null-data replacement cases with old lengths 0/-1/INT_MIN. The original pinned library previously matched 65,562 valid decoding cases.

Checklist

  • Based on current main; no other open PR implements the same functions
  • One topic per PR; follow-ups go in a new PR
  • No comments in code except technical debt
  • Unimplemented paths throw (NotImplemented_nid_no_patch); silent stubs are listed under Silent stubs in their library's technical debt file
  • No notes, investigation .md files or images added to the repository (attach them to this PR)
  • New third-party code is a submodule built from source
  • Generic behaviour, not specific to one title
  • Depends on: none
  • Fixes: none
  • Unblocks: none
  • GPUs tested: n/a
  • AI-assisted: yes

@github-actions

github-actions Bot commented Oct 25, 2026 •

Copy link
Copy Markdown

Other open pull requests touch the same code. Merging one will break the other, or both implement the same thing. If this one builds on them, list them in Depends on.

Pull request Conflicting files (lines on main) Same exports Same new files
PR 10452 core/libs/prx/libSceLibreSsl3/CMakeLists.txt L1
docs/dev/debt/libSceLibreSsl3.md L5
PR 10490 core/libs/prx/libSceLibreSsl3/CMakeLists.txt L1
docs/dev/debt/libSceLibreSsl3.md L5
PR 10491 core/libs/prx/libSceLibreSsl3/CMakeLists.txt L1
docs/dev/debt/libSceLibreSsl3.md L5
PR 10493 core/libs/prx/libSceLibreSsl3/CMakeLists.txt L1
docs/dev/debt/libSceLibreSsl3.md L5
PR 10631 core/libs/prx/libSceLibreSsl3/CMakeLists.txt L1
docs/dev/debt/libSceLibreSsl3.md L5, L8

Some comparisons could not be completed because a pull request conflicts with the base branch. Review these shared files manually before concluding that there is no overlap.

Pull request Shared files not compared
PR 9536 core/libs/prx/libSceLibreSsl3/Export.cpp
docs/dev/debt/libSceLibreSsl3.md
PR 9571 core/libs/prx/libSceLibreSsl3/Export.cpp
core/libs/prx/libSceLibreSsl3/include/Api.hpp
docs/dev/debt/libSceLibreSsl3.md
PR 9970 core/libs/prx/libSceLibreSsl3/CMakeLists.txt
core/libs/prx/libSceLibreSsl3/Export.cpp
docs/dev/debt/libSceLibreSsl3.md
PR 10461 core/libs/prx/libSceLibreSsl3/CMakeLists.txt
core/libs/prx/libSceLibreSsl3/Export.cpp
docs/dev/debt/libSceLibreSsl3.md
PR 10557 core/libs/prx/libSceLibreSsl3/CMakeLists.txt
core/libs/prx/libSceLibreSsl3/Export.cpp
docs/dev/debt/libSceLibreSsl3.md

@AZLisme

AZLisme commented Oct 25, 2026

Copy link
Copy Markdown
Contributor Author

Ready for review at 8b87391b3143fa5568191659bbd03c7ac5ca2554. The focused module suite passes 3/3, with canonical public-header and conventions checks passing. Successful BER decoding, integer boundaries, ownership/clearing and replacement coverage remain; the tests do not require unfinished error paths to keep throwing.

Please run maintainer-triggered full Linux and Windows CI for this head. Once review and CI are satisfactory, please merge. #10960 declares this PR first for shared-file integration, and I will rebase it after this merges.

@ahmadmuj ahmadmuj left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed head 8b87391b3143fa5568191659bbd03c7ac5ca2554, merged locally onto main 29ce6d87eb5b3050b4f08f60ed9ee52195979d14 (clean; still clean on 4017310678562c63594d99b8f45db2d68b9aaa6c, which touches nothing in libSceLibreSsl/libSceLibreSsl3).

What I ran, Linux x86-64, GCC 13.3:

  • Merged tree, full configure with BUILD_TESTING=ON, built only the three libSceLibreSsl3 test targets: guest_crypto_memcmp, guest_libressl3_digest, guest_libressl3_asn1 pass 3/3. check_conventions.py --base origin/main passes.
  • A differential against real LibreSSL 4.0.0: portable tarball (SHA256 checked against the OpenBSD list), static libcrypto built from source, d2i_ASN1_INTEGER vs your IntegerDecoder.cpp unmodified (only malloc/free/NotImplemented shimmed), ASan+UBSan on. It compares success vs NULL/throw, type, length, flags, data bytes and the *in advance, and on failure that *in didn't move.
    • Exhaustive: every 1-3 byte content under 02 0n, plus every identifier byte x length byte x a few first content bytes at every truncated inlen.
    • Sign padding at sizes 2-40 (00 80.., ff 00.., ff 80.., 00 00 01, ff ff 01, ...), each with long-form lengths 81-84, 88, 89 and fe with leading zero length bytes, trailing bytes and every prefix.
    • Headers: 1f 02, 1f 80 02, 1f 82 00, tag overflow, constructed and non-universal classes, 80, ff, huge lengths, length > buffer, inlen 0 and negative.
    • 2.5M random cases.
    • 21.8M cases total, 0 mismatches, 0 sanitizer reports. A scratch mutant without the redundant-padding check gives 541k mismatches (first one 02 02 00 05), so the harness does see those paths.
  • Replacement with a non-null *a (an earlier decode of 02 02 12 34): on success both free the old struct and its data and set *a to the new value. Negative inlen: both return NULL and leave *a alone, so that return nullptr matches ASN1_item_ex_d2i (tasn_dec.c L1250) and isn't a silent stub.
  • Walked it against the pinned LibreSSL sources too: asn1_get_identifier_cbs/asn1_get_length_cbs (asn1_lib.c L24-137), c2i_ASN1_INTEGER_cbs and the twos/padding helpers (a_int.c L528-737), and ASN1_STRING_free (a_string.c, NDEF = 0x10, freezero). Everything lines up. The one ordering difference: LibreSSL frees *a before the content checks, so 02 02 00 01 with an existing *a returns NULL with *a freed and set to NULL. Your debt line already covers that, and you throw there anyway.

Approving. Two small notes, not blocking:

  • Since 8b87391 there's no test for the content rejections (empty content, 02 02 00 7f-style redundant padding), so dropping that check would still pass the suite. I get why you don't want to freeze the throw paths. Just flagging that the differential above is the only thing covering them right now.
  • docs/dev/debt/libSceLibreSsl3.md: "ASN.1 integer decoding ... have no PS5 verification" should be "has".

Limits: no dump or PS5, so the 24-byte layout and app-heap allocation are only checked against LibreSSL's ASN1_STRING and the existing Asn1String in libSceLibreSsl.

@AZLisme
AZLisme force-pushed the feat/libressl3-asn1-accessors branch from 8b87391 to 77ba690 Compare October 25, 2026 13:22
@AZLisme

AZLisme commented Oct 25, 2026

Copy link
Copy Markdown
Contributor Author

Changed “have” to “has” in the ASN.1 decoding debt entry. The unfinished-error assertions remain removed. Rebased onto main 4017310; the entire libSceLibreSsl3 tree and both external library tests are byte-for-byte identical to reviewed 8b87391. Conventions and diff checks pass. No runtime rerun was needed for this documentation-only correction.

Updated head: 77ba69041c5ec098041c598c9a23cbcac15f0d73. Ready for merge when maintainers are satisfied.

@MarcZX10 MarcZX10 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed exact head 77ba69041c5ec098041c598c9a23cbcac15f0d73. The only change since the reviewed implementation is the grammar correction in the ASN.1 debt entry; the implementation and tests are unchanged. I ran python3 tools/check_conventions.py --base upstream/main --head HEAD and git diff --check upstream/main...HEAD; both pass. The author reports no runtime rerun for this documentation-only change. The earlier exact-head Linux focused suite and independent LibreSSL differential review remain applicable. #10960 declares this PR first for the shared-file ordering.

@BunnyFl

BunnyFl commented Oct 26, 2026

Copy link
Copy Markdown
Contributor

@AZLisme firmware 13.42 data for d2i_ASN1_INTEGER. I ran the decrypted libSceLibreSsl3.sprx's export (0x131d0) natively on a PC (not a console), against IntegerDecoder.cpp from 77ba69041c compiled unchanged with Asn1String. The module's errors were read back with its own ERR_get_error.

  • 200,000 inputs:
    • about two thirds minimal DER integers: positive, negative, zero, −256^k and the most negative value of each width, with up to 200 content bytes so short and long length forms both occur;
    • the rest BER-style or malformed variants: a redundant sign byte, a long-form length for short content, the high-tag-number form of tag 2, a constructed or wrong tag, zero-length content, indefinite length, an 0x82 length with a leading zero, class bits on tag 2, and five length bytes.
  • The length argument is exact, larger (with trailing garbage), smaller, or negative. The output pointer is NULL, points to NULL, or points to an object from an earlier decode on the same side.
  • Where the PR returns (147,982 inputs), everything compared matches: NULL or not, length, type (2 or 0x102), the content bytes (magnitude of negatives), flags, how far *in moves, and what *out receives. That includes the BER forms the PR accepts (long-form and leading-zero lengths, the high tag form, five length bytes). The module never returned the object passed in, the same as the PR. Three identical runs, plus 500,000 inputs with a second seed, 0 mismatches.
  • Where the PR throws (52,018 inputs), the module returns NULL and queues one of these errors:
input category (only the inputs where the PR throws) errors the module queued
minimal DER cut short by the length argument 0x0dfff09b, 0x0dfff066
redundant sign byte 0x0dfff03e, 0x0dfff09b, 0x0dfff066
long-form length, high tag form, 0x82 with a leading zero, five length bytes 0x0dfff09b, 0x0dfff066
constructed integer or wrong tag 0x0dfff0a8, 0x0dfff0e7, 0x0dfff09b, 0x0dfff066
class bits on tag 2 0x0dfff0a8, 0x0dfff066
zero-length content 0x0dfff03e, 0x0dfff066
indefinite length 0x0dfff066
  • Controls: copies of IntegerDecoder.cpp with one change each are caught. These are always type 2, no two's complement for negatives, advancing *in by the whole length argument, and dropping the keep-padding rule for FF 00….
  • Not checked: whether the module frees an object passed in through *out (frees weren't recorded), allocation failure, and NULL in/*in.

@AZLisme

AZLisme commented Oct 26, 2026

Copy link
Copy Markdown
Contributor Author

@BunnyFl I’ve credited the native-PC comparison in Tested and clarified the debt in 816ca46c66e9. Matching return values, decoded fields, input advancement and replacement outputs are recorded for the cases you compared.

The rejected-input difference remains: this decoder throws, while the native module returns NULL with ASN.1 errors. The legacy queue exists, but the decoder does not populate it. Exact general error sequences, prior-output frees and allocation/NULL/failure ownership remain unverified, so the category table alone does not justify a behavior change. This commit changes documentation only; the complete core tree is unchanged. Conventions, whitespace, independent peer review and the static current-main merge check pass; earlier runtime evidence retains its original scope. Please re-review the qualified update and merge once the remaining review/CI requirements are satisfied.

@oneandonlydean oneandonlydean self-assigned this Oct 26, 2026
@BunnyFl

BunnyFl commented Oct 26, 2026

Copy link
Copy Markdown
Contributor

@AZLisme thanks for adding it. Merging isn't something I can do: I'm not a maintainer. Here is firmware 13.42 data for the three points the debt entry still lists, from the decrypted libSceLibreSsl3.sprx's d2i_ASN1_INTEGER run natively on a PC (not a console). The comparison is against IntegerDecoder.cpp, which is unchanged since 77ba69041c. The allocator was a recording stand-in.

One correction to my earlier table: its code lists were the distinct first queued codes per category, not sequences. Full sequences are below.

Error sequences: the same input categories as before, every queued code read back. That was 60,000 inputs in three identical runs, plus 200,000 with a second seed, which gave the same set. A successful decode queues nothing. Every NULL return queues one of these exact sequences:

Inputs Sequence
redundant sign byte or zero-length content, length argument covering the encoding 0x0DFFF03E
constructed integer, length argument covering the encoding 0x0DFFF0E7
wrong tag (0x03) or class bits on tag 2 0x0DFFF0A8, 0x0DFFF03A
indefinite length 0x0DFFF066, 0x0DFFF03A
length argument shorter than the encoding 0x0DFFF066, 0x0DFFF03A or 0x0DFFF09B, 0x0DFFF03A (wrong tag and class bits can also give the 0x0DFFF0A8 sequence)
negative length argument none
  • *in never moved on a NULL return.

An earlier object in *out: owned data, or flags 0x10.

  • On success, the module and the PR match exactly. That was 20,000 inputs in three identical runs, plus 79,934 compared decodes with a second seed:
    • with owned data, both free the old data first, zeroed, then the old object, then allocate the new data and object;
    • with flags 0x10, only the old object is freed;
    • *out ends up as the returned object on both.
  • On failure, it depends on the failure:
    • the 0x0DFFF03E row: the old object is freed the same way, and *out is set to NULL;
    • every other failure, including a negative length: *out and the old object are left untouched.
    • The PR throws for all of these except a negative length, where it returns NULL. Either way it leaves *out untouched.

Allocation failures: a valid positive and a valid negative encoding, with and without an old object, failing each allocation in turn. The module makes two allocations, the data first and then the object.

Allocation that fails Module PR
first NULL, nothing queued throws
second NULL with 0x0DFFF041 queued twice throws
  • When the second allocation fails, both sides free the new data zeroed.
  • With an old object, both free it before the first allocation and leave *out NULL.
  • *in doesn't move, and no blocks are left allocated on either side.
  • The order of frees and allocations matches the PR's in all 8 cases; only the end differs (NULL vs throw).

NULL pointers and negative lengths:

  • in NULL and *in NULL both make the module read address 0 and fault (+0x1f08b and +0xd8c2a). The PR throws.

  • Lengths -1, -2, -1000, INT32_MIN and -2^40 return NULL on both, with nothing queued and *out/*in untouched.

  • The allocation and NULL/negative parts also gave three identical runs.

  • Controls: copies of IntegerDecoder.cpp with one change each are caught. These are:

    • old data not zeroed;
    • the old object freed before its data;
    • the data freed for flags 0x10;
    • allocating before freeing the old object;
    • the new data not zeroed when the second allocation fails.

@AZLisme

AZLisme commented Oct 26, 2026

Copy link
Copy Markdown
Contributor Author

@BunnyFl Thank you for the full sequences, allocator recordings and controls, and for clarifying that you are not a maintainer. In 81013f41bf09, the debt entry now leads with the remaining differences: rejected inputs and allocation failures still throw here rather than match native NULL/error handling, and 0x0DFFF03E content rejection retains an old output that the module frees and clears. The Tested section credits your confirmed replacement/free order, allocation cleanup and matching negative-length behavior, and corrects the earlier table to distinct first codes rather than full sequences.

The attribution remains firmware 13.42 running natively on a PC with a recording allocator, not physical-console/application-heap validation. Nonnegative NULL input remains a guarded throw rather than reproducing the module fault. This commit changes documentation only: every non-debt file, including the full core tree, matches 816ca46c66e9; conventions, whitespace and the static main merge check pass. Earlier runtime results retain their original scope. The existing legacy queue can represent the observed ordered and duplicate codes, so a future bounded decoder correction does not require a new error subsystem.

@AZLisme

AZLisme commented Oct 26, 2026

Copy link
Copy Markdown
Contributor Author

Maintainer run 40382921477, attempt 1, passed for unchanged 81013f41bf09 on afc780e681fc, with ordered inputs #10948, #11166, #10871, #11223 and #11275. Windows: 1,689 passed, 815 expected skips; Linux: 2,305 passed, 93 expected skips; zero failures. Both skip validators, all three ASN.1 fixtures and all 65 header cases passed on each platform.

The description records this exact composite result while retaining the native-PC attribution and remaining NULL/error-queue/old-output lifetime gaps. No source changes or console-parity claim. Please re-review this head and merge when the usual requirements are satisfied.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants