Skip to content

emitter/bundle: record executable-intent explicitly, not via stat() - #158

Open
KNambiarDJsc wants to merge 2 commits into
huggingface:mainfrom
KNambiarDJsc:fix/bundle-tracked-executable-files
Open

KNambiarDJsc wants to merge 2 commits into
huggingface:mainfrom
KNambiarDJsc:fix/bundle-tracked-executable-files

Conversation

@KNambiarDJsc

Copy link
Copy Markdown
Contributor

Summary

Implements the design you approved on #130: "recording executable intent explicitly makes sense for a separate PR. Please keep legacy bundle hashes unchanged, reject invalid or duplicate asset paths, and retain the POSIX chmod checks. Tests should cover old and new bundles on both platforms, including tampered manifests."

Root cause, precisely: bundle_hash() (write side) always got a file's mode from the in-memory TaskFile.executable flag — always correct. inspect_bundle() (verify/resume side) instead re-derived it from item.stat(). That round-trips on POSIX (chmodstat), but NTFS has no POSIX execute bit for regular files at all, so stat() reports 0o666 unconditionally on Windows — not sometimes wrong, structurally incapable of carrying this information. Every bundle's integrity check was unconditionally False (or raised) there.

What this does:

  • Records every originally-emitted role-scoped path (tracked_files) and its executable subset (executable_files) in task.toml's existing [metadata.repo2env] extension — itself part of the hashed configuration, so still tamper-evident.
  • inspect_bundle() reads executable-intent from that manifest for tracked files instead of stat(). On POSIX it additionally still cross-checks the real mode against it — unchanged strictness, a real chmod tamper is still caught exactly as today.
  • Absence of tracked_files marks a legacy bundle (emitted before this change): _identity() keeps the exact original hash shape for those, so already-published bundle identities never change — verified against a golden hash captured from the pre-change algorithm (test_legacy_bundle_hash_is_unchanged).
  • Rejects invalid or duplicate asset paths: both tracked_files and executable_files are validated through the same relative_asset_path shape check every other asset uses, duplicates are rejected, and executable_files must be a subset of tracked_files.

A real gap I found mid-implementation, not anticipated in the original proposal: the quality loop (quality/loop/artifacts.py) appends files directly to an already-written bundle directory, then re-stamps bundle_hash by calling inspect_bundle() again. My first pass broke this — it treated any file not in the manifest as "must be non-executable," which raised on real executable files added this way. Fixed by tracking the complete originally-emitted file set (not just the executable subset), so anything added later by other tooling falls back to today's exact stat()-only leniency, unaffected either way. Caught this by running the full suite, not just this module's own tests — see test plan.

Test plan

  • tests/test_task_bundle.py — 12 new tests: golden legacy-hash value, legacy bundle still round-trips, tracked/executable manifest recorded correctly, POSIX chmod-tamper still caught (both for tracked files and for a manifest-tamper-only case), Windows-equivalent proof (real sys.platform forced to win32, every file's real mode forced to 0o666, integrity still holds), content-tamper still caught even under simulated Windows, invalid/duplicate executable_files entries rejected, and the quality-loop append-and-restamp pattern still works
  • WSL/Linux, full suite: 2015 passed, 0 failed (unaffected — was already 0 failed)
  • Windows, controlled before/after on the identical commit (same machine, same env, only this diff applied/reverted): 276 failed / 74 errors on main → 180 failed / 18 errors with this change, full suite
  • Real Windows, not just simulated: ran test_task_bundle.py natively — everything passes except test_symlink_rejected, which fails identically on unmodified main (confirmed via git stash) — the separately-tracked symlink-privilege gap from Track native Windows controller and artifact portability #130, unrelated to this change and out of scope here
  • ruff check . / ruff format --check . — clean

Scope note

This PR is the file-modes item only. Symlinks (real product site: execution/hub_asset_alias.py:100, plus several test-fixture cases) remain the one item from #130's "file modes, symlinks, and process cleanup" list still untouched — happy to pick that up next if useful.

KarthikNambiar04 added 2 commits September 22, 2026 11:20
…-isolation fix

huggingface#145 stopped baking the oracle/verifier/instruction into pr_diff's agent
image, but already-published tasks (e.g. AdithyaSK/repo2rlenv-pr-diff, 181
tasks) keep the pre-fix Dockerfile until re-emitted — huggingface#155.

repo2rlenv migrate pr-diff <dir> [--apply] detects the baked-oracle marker
and rewrites environment/Dockerfile + tests/{test.sh,verifier.py,
oracle.patch,instruction.md} via the SAME builder functions fresh generate
calls today, so a migrated task is byte-identical to one emitted now, not
a hand-maintained parallel implementation. instruction.md and
solution/patch.diff (the oracle itself) are never touched, so a migrated
task keeps its original content_hash. repo_url/base_commit are recovered
from the existing Dockerfile's remote set-url line and task.toml's
metadata.repo2env.ref, not re-derived or guessed. Refuses to touch a task
whose instruction.md/solution/patch.diff don't hash to the content_hash
task.toml already claims, rather than silently rewriting an inconsistent
bundle. Defaults to a dry-run report; --apply writes.
bundle_hash() (write side) always got a file's mode from the in-memory
TaskFile.executable flag; inspect_bundle() (verify/resume side) instead
re-derived it from item.stat(). That round-trips on POSIX (chmod ->
stat), but NTFS has no POSIX execute bit for regular files at all, so
stat() reports 0o666 unconditionally on Windows -- not sometimes
wrong, structurally incapable of carrying this information. Every
bundle's integrity check was unconditionally False (or raised) there.

Record every originally-emitted role-scoped path (tracked_files) and
its executable subset (executable_files) in task.toml's existing
[metadata.repo2env] extension -- itself part of the hashed
configuration, so still tamper-evident. inspect_bundle() reads
executable-intent from that manifest for tracked files instead of
stat(); on POSIX it additionally still cross-checks the real mode
against it (unchanged strictness -- a real chmod tamper is still
caught exactly as today). A file present in the directory but not in
tracked_files (the quality loop appends evidence artifacts to an
already-written bundle and re-stamps its hash -- quality/loop/
artifacts.py) falls back to the untouched legacy stat()-only check,
so that pattern keeps working exactly as it always has.

Absence of tracked_files marks a legacy bundle (emitted before this
change): _identity() keeps the exact original hash shape for those,
so already-published bundle identities never change -- verified
against a golden hash captured from the pre-change algorithm.

Verified on both platforms:
- WSL/Linux: 2015 passed, 0 failed (was already 0 failed; unaffected)
- Windows, controlled before/after on the same commit: 276 failed/74
  errors on main -> 180 failed/18 errors with this change (measured
  with the full suite, not just this module's own tests)
- Windows: bundle_hash is deterministic across platforms (the golden
  legacy hash matches whether computed on Linux or Windows); the one
  remaining local test failure (test_symlink_rejected) is the
  separately-tracked symlink-privilege gap from huggingface#130, confirmed to
  fail identically on unmodified main

This branch has not been deployed

No deployments
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.

1 participant