Skip to content

fix(memory): neutralize every tag spelling a reader accepts in the data boundary - #243

Merged
Zongwei9888 merged 1 commit into
HKUDS:mainfrom
raymondginger2018-sudo:pr/memory-boundary-payload
Sep 27, 2026
Merged

Zongwei9888 merged 1 commit into
HKUDS:mainfrom
raymondginger2018-sudo:pr/memory-boundary-payload

Conversation

@raymondginger2018-sudo

Copy link
Copy Markdown
Contributor

Summary

_escape_data_block in core/harness/memory.py neutralized the untrusted-data boundary by replacing four literal strings, so it only caught the exact lower-case, space-free spellings. A note containing </UNTRUSTED-DATA> or </untrusted-data > therefore stayed in plain text, and the framed block carried two closing tags that a reader accepts as a delimiter — the remainder of the note read as if it sat outside the untrusted-data boundary.

This is the case #216 explicitly invited: "If you find a payload that the current tests do not catch, a PR adding that case to tests/test_memory.py would be very welcome."

Changes

  • core/harness/memory.py — replace the literal-pair replacements with a small _DATA_BLOCK_ESCAPES table of pre-compiled patterns, matched case-insensitively and tolerating whitespace inside the delimiters (</\s*untrusted-data\s*>, </\s*system-reminder\s*>, and their opening forms). _escape_data_block now rewrites every spelling a tag reader accepts into one canonical escaped form, so the framed block keeps exactly one literal closing tag: the boundary's own.
  • tests/test_memory.py — add the regression case next to the existing boundary test.

Scope decisions (why exactly this spelling set)

  • Folded: tag case (</UNTRUSTED-DATA>) and whitespace inside the delimiters (</untrusted-data >, </system-reminder\t>). Both are spellings an HTML-ish tag reader accepts as the same tag.
  • Deliberately not folded: a zero-width space inside the tag name (</untrusted-data​>), and full-width angle brackets (</untrusted-data>). No tag reader accepts either as the delimiter, and rewriting them would mutate the note's own text rather than neutralize a tag. They stay in the note verbatim; that is the intended reading, not a residual hole.
  • Deliberately unchanged: _escape_reminder (the AGENTS.md frame) keeps escaping only the exact spelling. That frame carries user-authorized instruction text; the boundary is the only surface that claims untrusted-ness, so it is the only one widened here. Keeps the diff to the claim being made.

Tests

  • RED before the fix (with the new test only): assert 3 == 1 — len(re.findall(r"</\s*untrusted-data\s*>", text, re.IGNORECASE)) counted the forged </UNTRUSTED-DATA>, the forged </untrusted-data > and the boundary's own closing tag.
  • GREEN after: pytest tests/test_memory.py -k boundary → 2 passed; the three memory test files → 66 passed, 1 skipped (tests/test_memory.py alone: 25 passed).
  • Formatting: uvx ruff@0.15.21 format --check and uvx ruff@0.15.21 check rc=0 on both files (same rev as .pre-commit-config.yaml).
  • Local CI replay (ci-tools/run-ci-jobs.sh, one log dir per leg, all at this revision):
    • lint → precommit_rc=0; windows → group1_rc=0 group2_rc=0; secret-scan → gitleaks_rc=0 (PR-scoped gitleaks git); desktop quality → cargotest_rc=0; desktop bundle → npmci / sidecar / tauribuild / verifybundle = 0 with upload_rc=SKIPPED (locally only the GitHub artifact upload is skipped — skipped, not red).
    • test (py3.12) → no attributable new failures, measured as a differential. Two loop-CLI tests hang under whole-suite load on both revisions, so base and patched were run with the same two nodes deselected — tests/test_loop_cli.py::test_loop_resume_active_running_goal_attaches_without_duplicate_turn, tests/test_loop_cli.py::test_loop_waits_for_the_deciding_turn_to_finish — which lets both sides finish and keeps the failure node sets comparable:
      • repetition 1 — patched 114 failed / 2051 passed / 2 deselected / 4 errors (1488 s) vs base 110 / 2054 / 2 / 4 (1500 s): 3 patched-only nodes, all timing-sensitive tests in unrelated subsystems (app-server status probe, automation-goal ownership, transparency log), and one of them is already registered as flaky by this harness's own baseline (baselines/pytest-win-py3.12.txt:115: "两次实跑一红一绿 ⇒ 机理未定性").
      • repetition 2 — patched 111 / 2054 / 2 / 4 (1417 s) vs base 111 / 2053 / 2 / 4 (1414 s): failure node sets identical (101 nodes each; comm empty in both directions) and none of repetition 1's patched-only nodes recurred.
      • passed differs by exactly +1 (the new test) and wall clock by 3 s; the local failure count is itself nondeterministic (base reads 110/111/112 across runs), which is why the comparison is node-set based.
    • test (py3.13 / py3.14) → no local baseline exists, so the harness declines to classify instead of passing silently; the hang-prone pair and the failure population are the same as base on both versions.
    • package → not revision-related: scripts/verify_python_distribution.py unconditionally requires the built browser client (app_server/web_assets/web-build.json + index.html + the assets they reference), which is a vite product (desktop/vite.config.ts:43) and gitignored. CI builds it before packaging (python-ci.yml:166 npm run build:web → :177). Locally the same revision reads verify_rc=1 before that client is built and verify_rc=0 (Verified deepcode-hku 2.2.0) once it is — the local red was that build state, not this change.

Dependency note

No new dependencies, no new modules: two existing files only (modify/modify), 42 insertions / 8 deletions.

…ta boundary

`_escape_data_block` replaced four literal strings, so only the exact
lower-case, space-free spellings were escaped. A note containing
`</UNTRUSTED-DATA>` or `</untrusted-data >` therefore stayed in plain text,
and the framed block carried a second closing tag: the remainder of the note
read as if it sat outside the untrusted-data boundary (HKUDS#216).

Escape the tags case-insensitively, tolerating whitespace inside the
delimiters, and add a regression test next to the existing boundary test.
@Zongwei9888
Zongwei9888 merged commit e6124ad into HKUDS:main Sep 27, 2026
12 checks passed
@Zongwei9888

Copy link
Copy Markdown
Collaborator

Merged into main as e6124ad. Thank you @raymondginger2018-sudo — exactly the kind of payload the note on #216 asked for, and the scope reasoning (fold case and inner whitespace because a tag reader accepts them; leave zero-width and full-width look-alikes alone because nothing reads them as the delimiter) is the right line to draw. The regression test that counts delimiters with the same tolerant pattern makes the claim precise.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants