Skip to content

Exoneration: parent_ndx layers 2-3 do not apply to oc's architecture (no code change) #7628

Description

@oferchen

Exoneration record. This issue has been wrong twice; both corrections are
below, because the corrections are the content. NO CODE CHANGE IS REQUIRED.

Layer 1 - recv_file_entry() refusing a non-directory transfer-root entry
(flist.c:1127-1134) - was ported in #7625
(crates/protocol/src/flist/read/mod.rs, read_entry()). That is real and
necessary: it is a policy check on untrusted peer input.

Correction 1: layers 2 and 3 are not unported work

Originally filed as "layers 2 and 3 outstanding". Wrong - asserted from
upstream's C without checking oc's architecture.

Upstream's failure mode is C-specific. dir_flist is a separate array
holding only S_ISDIR entries, grown with realloc() not calloc(). A .
entry with a non-directory mode never lands in it, so dir_flist->used stays
0 while the basename test still passes; parent_ndx keeps its default 0 and
consumers dereference dir_flist->files[0] - uninitialised heap. Layer 2
(flist.c:3072-3083) clears parent_ndx to -1 when dir_flist is empty;
layer 3 bounds-checks the consumers.

oc has no such structure:

  • No dir_flist. crates/transfer/src/generator/ndx_map.rs states it:
    "oc has no separate dir_flist, so the flat index is recorded directly and
    needs no second translation." The equivalent is NdxSegment::parent_flat: i32, a flat index into the single list.
  • Layer 2's semantics already present.
    crates/transfer/src/generator/file_list/inc_recurse.rs sets parent_flat
    to 0 only when file_list.get(0) exists and is named ., else -1.
    Slot 0 is checked present before use.
  • Layer 3's semantics already present.
    DirectoryTree::try_add_directory
    (crates/protocol/src/flist/dir_tree.rs) rejects out-of-range parents; the
    unchecked path uses safe Vec indexing, so the worst case is a bounds-check
    panic, never an uninitialised read. Already covered by SEC-4 /
    CVE-2026-43620 regression tests in that file.

Layers 2 and 3 defend a C memory-layout hazard that safe-Rust flat-Vec
indexing does not express.

Correction 2: the "layer-1 coupling" residual was a FALSE ALARM

The previous revision claimed the name-only test at inc_recurse.rs
(file_list.get(0).is_some_and(|e| e.name() == ".")) was safe only because
the #7625 decode guard guarantees any . entry is a directory - i.e. a
load-bearing cross-module dependency - and proposed making the test
type-aware.

That is wrong. It was reasoned from role names instead of traced. Tracing the
assignment to GeneratorContext::file_list:

  • generator/file_list/mod.rs build_file_list() is a local filesystem
    walk
    (oc's send_file_list(), citing flist.c:2227). It is the only
    producer.
  • inc_recurse.rs merely mem::takes that same locally-built list and
    reorders it; iconv.rs re-encodes it. Neither decodes anything.
  • The synthetic . there is built from std::fs::symlink_metadata(".") and
    pushed only inside an explicit if meta.is_dir().

So the generator's . is a directory by construction, from oc's own walk,
gated by its own is_dir() check. The decode guard never touches this list -
consistent with the fact that no role under crates/transfer/src/generator/
constructs a FileListReader at all. The name-only test is safe for a reason
entirely unrelated to #7625, and there is no cross-module coupling.

The proposed type test is therefore not shipped: a hardening added for a
reason that does not hold reads as protection and provides none.

Status

Nothing to port, nothing to harden. Recorded so this is not re-filed a third
time.

Activity

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions