[GH-ISSUE #7400] [Bug] TLS template ranges in section zero padding abort PE load-config inspection #4407

Open
opened 2026-10-31 00:49:33 +00:00 by bsakaguchi · 0 comments
Owner

Originally created by @coygeek on GitHub (Oct 27, 2026).
Original GitHub issue: https://github.com/morluto/rea/issues/7400

Before submitting

  • I searched the open and closed issues for a related report.
  • I removed credentials, tokens, private paths, and sensitive target data.

Area

Native analysis or decompilation

Observed behavior

The public inspect-pe-load-config CLI returns invalid_request / invalid_input instead of Evidence when a declared 256-byte TLS template range extends into a section's virtual zero padding. The TLS directory and callback-array storage are fully file-backed. Moving only the template's start/end pointers to an equal-sized file-backed range in the same section makes inspection succeed.

I observed this through the compiled checkout CLI, once for each case in the script below. A range wholly inside the section's virtual zero padding also fails. A separate control with an index address in the same padding succeeds and reports its file offset as null.

Reproduction

Use a clean checkout at the source revision below with its locked dependencies and compiled runtime already prepared, and Node.js v24.22.1. Run the following from the checkout root. It creates every input, isolates CLI state, saves the full outputs under $WORK, and prints the relevant results. No target executable is launched.

WORK="$(mktemp -d "${TMPDIR:-.}/rea-tls-check.XXXXXX")"
cat > "$WORK/probe.mjs" <<'JS'
import fs from 'node:fs';
import path from 'node:path';
import os from 'node:os';
import {spawnSync} from 'node:child_process';
const repo = process.argv[2] ?? process.cwd();
const work = path.dirname(new URL(import.meta.url).pathname);
const base = 0x180000000n;
for (const name of ['home','config','data','cache','state','tmp','run']) fs.mkdirSync(path.join(work,name), {recursive:true});
const env = Object.fromEntries(Object.entries(process.env).filter(([k]) => !k.startsWith('REA_') && !k.startsWith('XDG_')));
Object.assign(env,{HOME:path.join(work,'home'),XDG_CONFIG_HOME:path.join(work,'config'),XDG_DATA_HOME:path.join(work,'data'),XDG_CACHE_HOME:path.join(work,'cache'),XDG_STATE_HOME:path.join(work,'state'),XDG_RUNTIME_DIR:path.join(work,'run'),TMPDIR:path.join(work,'tmp')});
function image(start,end,index=0x1300) {
  const b=Buffer.alloc(0x1800);
  const u16=(at,x)=>b.writeUInt16LE(x,at),u32=(at,x)=>b.writeUInt32LE(x,at),ptr=(at,rva)=>b.writeBigUInt64LE(base+BigInt(rva),at);
  b.write('MZ'); u32(0x3c,0x80); b.write('PE\0\0',0x80);
  u16(0x84,0x8664); u16(0x86,2); u16(0x94,0xf0); u16(0x96,3);
  const o=0x98; u16(o,0x20b); u32(o+4,0x200); u32(o+8,0x1000); u32(o+16,0x2000); u32(o+20,0x2000);
  b.writeBigUInt64LE(base,o+24); u32(o+32,0x1000); u32(o+36,0x200);u16(o+40,6);u16(o+48,6);u32(o+56,0x3000);u32(o+60,0x600);u16(o+68,3);
  b.writeBigUInt64LE(0x100000n,o+72);b.writeBigUInt64LE(0x1000n,o+80);b.writeBigUInt64LE(0x100000n,o+88);b.writeBigUInt64LE(0x1000n,o+96);u32(o+108,16);
  u32(o+112+9*8,0x1000);u32(o+112+9*8+4,40);
  function section(at,name,vs,rva,rs,off,flags){b.write(name,at);u32(at+8,vs);u32(at+12,rva);u32(at+16,rs);u32(at+20,off);u32(at+36,flags);}
  section(o+0xf0,'.data',0x1000,0x1000,0x1000,0x600,0xc0000040);
  section(o+0xf0+40,'.text',0x400,0x2000,0x200,0x1600,0x60000020);
  ptr(0x600,start);ptr(0x608,end);ptr(0x610,index);ptr(0x618,0x1380);
  // The callback-array storage at RVA 0x1380 is file-backed and null-terminated.
  u32(0x624,4<<20);
  return b;
}
const cases=[['file-backed',0x2100,0x2200,0x1300],['straddling',0x2180,0x2280,0x1300],['zero-tail',0x2200,0x2300,0x1300],['index-in-zero-tail',0x2100,0x2200,0x2250]];
console.log(JSON.stringify({node:process.version,os:os.type(),release:os.release(),arch:os.arch()}));
for (const args of [['--version'],['inspect-pe-load-config','--help']]) {
 const r=spawnSync(process.execPath,[path.join(repo,'scripts/rea.mjs'),...args],{cwd:repo,env,encoding:'utf8'});
 fs.writeFileSync(path.join(work,args[0]==='--version'?'version.txt':'help.txt'),r.stdout+r.stderr);
 if(args[0]==='--version')console.log(JSON.stringify({version:r.stdout.trim(),exit:r.status}));
}
for(const [name,start,end,index] of cases) {
 const file=path.join(work,name+'.exe');fs.writeFileSync(file,image(start,end,index));
 const r=spawnSync(process.execPath,[path.join(repo,'scripts/rea.mjs'),'inspect-pe-load-config',file,'--json'],{cwd:repo,env,encoding:'utf8'});
 fs.writeFileSync(path.join(work,name+'.stdout.json'),r.stdout);fs.writeFileSync(path.join(work,name+'.stderr.txt'),r.stderr);
 const o=JSON.parse(r.stdout);const tls=o.normalized_result?.tls;
 console.log(JSON.stringify({case:name,exit:r.status,code:o.code,category:o.category,issue:o.details?.issues?.[0]?.message.replace(file,'<artifact>'),tls,stderr:r.stderr}));
}
JS
node "$WORK/probe.mjs" "$PWD"

The straddling and zero-tail cases exit 1. The file-backed and index-in-zero-tail controls exit 0. The image layout and all template lengths remain fixed; the cases change only directory pointers.

Expected behavior

Preserve the declared template start/end VAs and raw_data_size: 256, with raw_data_file_offset: null and raw_data_section: ".text" when the range has no complete file translation. Return the rest of the TLS/load-config Evidence normally. Directory and callback-array bytes remain subject to the documented storage checks.

Expected source: docs/pe-load-config.md:15 and docs/pe-load-config.md:42 describe raw TLS ranges, nullable derived mappings, and fail-closed directory/callback-array storage. Expected source: src/domain/native/peLoadConfig.ts:333 defines the nullable raw-data file offset. Expected source: src/native/pe/PeLoadConfigParser.ts:194 already preserves unknown file offsets for other located addresses.

The Microsoft PE format reference defines zero padding when a section's virtual size exceeds its raw size. Here the template's entire virtual range remains within that section; the TLS SizeOfZeroFill field is independently zero. This expectation concerns REA's static metadata contract; I did not test Windows loading.

Impact and scope

P2: an input-dependent compatibility failure prevents the caller from obtaining any TLS/load-config result, even though missing derived file mapping information has a documented nullable representation. The exact observed scope is the two synthetic PE32+ template placements below. Real-producer frequency, PE32 behavior, other hosts, and MCP transport were not tested. No target data was changed and no security boundary was crossed.

No unchanged-artifact workaround was verified. Changing the fixture's pointers produces a control, not a workaround for preserving the original evidence. The bounded observations do not establish severe or product-wide failure.

REA version

Exact node scripts/rea.mjs --version stdout:

6.8.0

Source commit: f8f7ac96de8a66a4ab9a23d87e04779bfcd11045.

Runtime and platform

Node.js v24.22.1; macOS 27.0.1, Darwin 27.0.0, arm64. No disassembler or Windows host was used.

Target and artifact context

Authored 6144-byte PE32+ AMD64 image. Image base 0x180000000; section alignment 0x1000; file alignment 0x200. The 40-byte TLS directory is at RVA 0x1000, file offset 0x600, in .data. Its null-terminated callback array is at RVA 0x1380, file offset 0x980.

The .text section has RVA 0x2000, virtual size 0x400, raw size 0x200, and file offset 0x1600. Its declared virtual interval is [0x2000, 0x2400); file backing ends at 0x2200. The file-backed template control is [0x2100, 0x2200); the straddling template is [0x2180, 0x2280); the wholly padded template is [0x2200, 0x2300). These inputs test offline format interpretation, not executable runtime validity.

Interface

CLI

Earliest failure stage

Native analysis or reconstruction

Relevant output or evidence

Observed results from node scripts/rea.mjs inspect-pe-load-config <synthetic-image> --json:

Case Exit Relevant result
file-backed 0 raw_data_size 256; raw_data_file_offset 5888; raw_data_section .text
straddling 1 invalid_request; invalid_input; issue reason invalid_format
zero-tail 1 invalid_request; invalid_input; issue reason invalid_format
index-in-zero-tail 0 index VA 0x180002250; RVA 8784; file_offset null; section .text

Both failures have this issue-message suffix, with no normalized_result:

Virtual address or string range is not file mapped

All four commands had empty stderr. The successful controls report callbacks: [] and callback-array storage {offset: 2432, bytes: 8}.

Failure source: src/native/pe/PeLoadConfigParser.ts:328 requires a complete file mapping for the nonempty raw-data extent before returning any facts. Failure source: src/native/metadata/BinaryContainer.ts:471 throws the observed unmapped-range error. These are source-derived explanations of the observed CLI failure.

The existing focused parser suite also passes all 75 tests with npm run test:local -- src/native/pe/PeLoadConfigParser.test.ts; that suite does not establish the missing raw-range behavior is correct.

Typed outcome or recovery expected

A complete Evidence result containing the declared TLS template range, a null derived file offset, and the known section, while preserving validation of the bytes actually decoded.

Analysis or suspected cause

Source-derived: the raw-data extent is used to derive a location but is not read as template bytes. Its file-offset lookup propagates an unmapped failure. The helper used for index/callback targets instead catches that mapping outcome and preserves the address and section. This difference explains why the index control succeeds in the same virtual padding.

Additional context

Open and closed issue searches covered TLS, TLS raw data, inspect_pe_load_config, load configuration, file mapped, and raw_data_file_offset. Related closed reports #6871, #6688, #6830, and #6831 concern IL2CPP registration, PE export strings, or Go metadata rather than nullable TLS template locations. Broad Windows/PE feature reports #362 and #38 do not describe this refusal. No same-contract duplicate was found.

No last-known-good revision or real-producer occurrence was established. No fix was implemented or published.

Originally created by @coygeek on GitHub (Oct 27, 2026). Original GitHub issue: https://github.com/morluto/rea/issues/7400 ## Before submitting - [x] I searched the open and closed issues for a related report. - [x] I removed credentials, tokens, private paths, and sensitive target data. ## Area Native analysis or decompilation ## Observed behavior The public `inspect-pe-load-config` CLI returns `invalid_request` / `invalid_input` instead of Evidence when a declared 256-byte TLS template range extends into a section's virtual zero padding. The TLS directory and callback-array storage are fully file-backed. Moving only the template's start/end pointers to an equal-sized file-backed range in the same section makes inspection succeed. I observed this through the compiled checkout CLI, once for each case in the script below. A range wholly inside the section's virtual zero padding also fails. A separate control with an index address in the same padding succeeds and reports its file offset as `null`. ## Reproduction Use a clean checkout at the source revision below with its locked dependencies and compiled runtime already prepared, and Node.js v24.22.1. Run the following from the checkout root. It creates every input, isolates CLI state, saves the full outputs under `$WORK`, and prints the relevant results. No target executable is launched. ```sh WORK="$(mktemp -d "${TMPDIR:-.}/rea-tls-check.XXXXXX")" cat > "$WORK/probe.mjs" <<'JS' import fs from 'node:fs'; import path from 'node:path'; import os from 'node:os'; import {spawnSync} from 'node:child_process'; const repo = process.argv[2] ?? process.cwd(); const work = path.dirname(new URL(import.meta.url).pathname); const base = 0x180000000n; for (const name of ['home','config','data','cache','state','tmp','run']) fs.mkdirSync(path.join(work,name), {recursive:true}); const env = Object.fromEntries(Object.entries(process.env).filter(([k]) => !k.startsWith('REA_') && !k.startsWith('XDG_'))); Object.assign(env,{HOME:path.join(work,'home'),XDG_CONFIG_HOME:path.join(work,'config'),XDG_DATA_HOME:path.join(work,'data'),XDG_CACHE_HOME:path.join(work,'cache'),XDG_STATE_HOME:path.join(work,'state'),XDG_RUNTIME_DIR:path.join(work,'run'),TMPDIR:path.join(work,'tmp')}); function image(start,end,index=0x1300) { const b=Buffer.alloc(0x1800); const u16=(at,x)=>b.writeUInt16LE(x,at),u32=(at,x)=>b.writeUInt32LE(x,at),ptr=(at,rva)=>b.writeBigUInt64LE(base+BigInt(rva),at); b.write('MZ'); u32(0x3c,0x80); b.write('PE\0\0',0x80); u16(0x84,0x8664); u16(0x86,2); u16(0x94,0xf0); u16(0x96,3); const o=0x98; u16(o,0x20b); u32(o+4,0x200); u32(o+8,0x1000); u32(o+16,0x2000); u32(o+20,0x2000); b.writeBigUInt64LE(base,o+24); u32(o+32,0x1000); u32(o+36,0x200);u16(o+40,6);u16(o+48,6);u32(o+56,0x3000);u32(o+60,0x600);u16(o+68,3); b.writeBigUInt64LE(0x100000n,o+72);b.writeBigUInt64LE(0x1000n,o+80);b.writeBigUInt64LE(0x100000n,o+88);b.writeBigUInt64LE(0x1000n,o+96);u32(o+108,16); u32(o+112+9*8,0x1000);u32(o+112+9*8+4,40); function section(at,name,vs,rva,rs,off,flags){b.write(name,at);u32(at+8,vs);u32(at+12,rva);u32(at+16,rs);u32(at+20,off);u32(at+36,flags);} section(o+0xf0,'.data',0x1000,0x1000,0x1000,0x600,0xc0000040); section(o+0xf0+40,'.text',0x400,0x2000,0x200,0x1600,0x60000020); ptr(0x600,start);ptr(0x608,end);ptr(0x610,index);ptr(0x618,0x1380); // The callback-array storage at RVA 0x1380 is file-backed and null-terminated. u32(0x624,4<<20); return b; } const cases=[['file-backed',0x2100,0x2200,0x1300],['straddling',0x2180,0x2280,0x1300],['zero-tail',0x2200,0x2300,0x1300],['index-in-zero-tail',0x2100,0x2200,0x2250]]; console.log(JSON.stringify({node:process.version,os:os.type(),release:os.release(),arch:os.arch()})); for (const args of [['--version'],['inspect-pe-load-config','--help']]) { const r=spawnSync(process.execPath,[path.join(repo,'scripts/rea.mjs'),...args],{cwd:repo,env,encoding:'utf8'}); fs.writeFileSync(path.join(work,args[0]==='--version'?'version.txt':'help.txt'),r.stdout+r.stderr); if(args[0]==='--version')console.log(JSON.stringify({version:r.stdout.trim(),exit:r.status})); } for(const [name,start,end,index] of cases) { const file=path.join(work,name+'.exe');fs.writeFileSync(file,image(start,end,index)); const r=spawnSync(process.execPath,[path.join(repo,'scripts/rea.mjs'),'inspect-pe-load-config',file,'--json'],{cwd:repo,env,encoding:'utf8'}); fs.writeFileSync(path.join(work,name+'.stdout.json'),r.stdout);fs.writeFileSync(path.join(work,name+'.stderr.txt'),r.stderr); const o=JSON.parse(r.stdout);const tls=o.normalized_result?.tls; console.log(JSON.stringify({case:name,exit:r.status,code:o.code,category:o.category,issue:o.details?.issues?.[0]?.message.replace(file,'<artifact>'),tls,stderr:r.stderr})); } JS node "$WORK/probe.mjs" "$PWD" ``` The `straddling` and `zero-tail` cases exit 1. The `file-backed` and `index-in-zero-tail` controls exit 0. The image layout and all template lengths remain fixed; the cases change only directory pointers. ## Expected behavior Preserve the declared template start/end VAs and `raw_data_size: 256`, with `raw_data_file_offset: null` and `raw_data_section: ".text"` when the range has no complete file translation. Return the rest of the TLS/load-config Evidence normally. Directory and callback-array bytes remain subject to the documented storage checks. Expected source: docs/pe-load-config.md:15 and docs/pe-load-config.md:42 describe raw TLS ranges, nullable derived mappings, and fail-closed directory/callback-array storage. Expected source: src/domain/native/peLoadConfig.ts:333 defines the nullable raw-data file offset. Expected source: src/native/pe/PeLoadConfigParser.ts:194 already preserves unknown file offsets for other located addresses. The [Microsoft PE format reference](https://learn.microsoft.com/en-us/windows/win32/debug/pe-format#section-table-section-headers) defines zero padding when a section's virtual size exceeds its raw size. Here the template's entire virtual range remains within that section; the TLS `SizeOfZeroFill` field is independently zero. This expectation concerns REA's static metadata contract; I did not test Windows loading. ## Impact and scope P2: an input-dependent compatibility failure prevents the caller from obtaining any TLS/load-config result, even though missing derived file mapping information has a documented nullable representation. The exact observed scope is the two synthetic PE32+ template placements below. Real-producer frequency, PE32 behavior, other hosts, and MCP transport were not tested. No target data was changed and no security boundary was crossed. No unchanged-artifact workaround was verified. Changing the fixture's pointers produces a control, not a workaround for preserving the original evidence. The bounded observations do not establish severe or product-wide failure. ## REA version Exact `node scripts/rea.mjs --version` stdout: ```text 6.8.0 ``` Source commit: `f8f7ac96de8a66a4ab9a23d87e04779bfcd11045`. ## Runtime and platform Node.js v24.22.1; macOS 27.0.1, Darwin 27.0.0, arm64. No disassembler or Windows host was used. ## Target and artifact context Authored 6144-byte PE32+ AMD64 image. Image base `0x180000000`; section alignment `0x1000`; file alignment `0x200`. The 40-byte TLS directory is at RVA `0x1000`, file offset `0x600`, in `.data`. Its null-terminated callback array is at RVA `0x1380`, file offset `0x980`. The `.text` section has RVA `0x2000`, virtual size `0x400`, raw size `0x200`, and file offset `0x1600`. Its declared virtual interval is `[0x2000, 0x2400)`; file backing ends at `0x2200`. The file-backed template control is `[0x2100, 0x2200)`; the straddling template is `[0x2180, 0x2280)`; the wholly padded template is `[0x2200, 0x2300)`. These inputs test offline format interpretation, not executable runtime validity. ## Interface CLI ## Earliest failure stage Native analysis or reconstruction ## Relevant output or evidence Observed results from `node scripts/rea.mjs inspect-pe-load-config <synthetic-image> --json`: | Case | Exit | Relevant result | | --- | --- | --- | | file-backed | 0 | raw_data_size 256; raw_data_file_offset 5888; raw_data_section .text | | straddling | 1 | invalid_request; invalid_input; issue reason invalid_format | | zero-tail | 1 | invalid_request; invalid_input; issue reason invalid_format | | index-in-zero-tail | 0 | index VA 0x180002250; RVA 8784; file_offset null; section .text | Both failures have this issue-message suffix, with no `normalized_result`: ```text Virtual address or string range is not file mapped ``` All four commands had empty stderr. The successful controls report `callbacks: []` and callback-array storage `{offset: 2432, bytes: 8}`. Failure source: src/native/pe/PeLoadConfigParser.ts:328 requires a complete file mapping for the nonempty raw-data extent before returning any facts. Failure source: src/native/metadata/BinaryContainer.ts:471 throws the observed unmapped-range error. These are source-derived explanations of the observed CLI failure. The existing focused parser suite also passes all 75 tests with `npm run test:local -- src/native/pe/PeLoadConfigParser.test.ts`; that suite does not establish the missing raw-range behavior is correct. ## Typed outcome or recovery expected A complete Evidence result containing the declared TLS template range, a null derived file offset, and the known section, while preserving validation of the bytes actually decoded. ## Analysis or suspected cause Source-derived: the raw-data extent is used to derive a location but is not read as template bytes. Its file-offset lookup propagates an unmapped failure. The helper used for index/callback targets instead catches that mapping outcome and preserves the address and section. This difference explains why the index control succeeds in the same virtual padding. ## Additional context Open and closed issue searches covered TLS, TLS raw data, inspect_pe_load_config, load configuration, file mapped, and raw_data_file_offset. Related closed reports [#6871](https://github.com/morluto/rea/issues/6871), [#6688](https://github.com/morluto/rea/issues/6688), [#6830](https://github.com/morluto/rea/issues/6830), and [#6831](https://github.com/morluto/rea/issues/6831) concern IL2CPP registration, PE export strings, or Go metadata rather than nullable TLS template locations. Broad Windows/PE feature reports [#362](https://github.com/morluto/rea/issues/362) and [#38](https://github.com/morluto/rea/issues/38) do not describe this refusal. No same-contract duplicate was found. No last-known-good revision or real-producer occurrence was established. No fix was implemented or published.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
morluto/rea#4407
No description provided.