Symptom
add_file_node accepts a parent_dir it cannot resolve. It files the node under the source root instead of refusing it, while the parent's own index still decides which dir_children bucket the name goes into.
Mechanism
In add_file_node (src/graph/dag.cpp), the path comes from the parent node's path_id. When get_file_node(parent_dir) returns null, it falls back to the source root:
auto parent_path = PathId::SourceRoot;
if (node.parent_dir != 0) {
auto const* parent = get_file_node(std::as_const(graph), node.parent_dir);
if (parent) {
parent_path = parent->path_id;
}
}
get_file_node returns null for a command-, condition- or phi-tagged id, an index past the end, or an id whose slot holds a different node. In each of those cases:
- The node is registered in
path_to_node at <source root>/<name>, a path the caller never asked for.
- The
dir_children registration is keyed on node_id::index(parent_dir), with the tag stripped, so it goes into another node's bucket or past the end.
graph.dir_children is resized only to the new node's index, and Vec::operator[] is unchecked (include/pup/core/vec.hpp). So a parent_dir whose index exceeds the new node's writes out of bounds. I read this from the source and did not exercise it.
Observed with a probe linked against build/libputup.a from the PR #490 branch. The probe adds root foo.c, then foo.c under unminted parent 999:
P2a root foo.c: ok id=2 next_file_id=3
P2b foo.c under invalid parent 999: ERR code=21 msg=[Unable to create 'foo.c' because a file already occupies that path] next_file_id=3
With #490 the call fails, but the message names a path the caller did not request. On main without #490, the same call silently repoints path_to_node["foo.c"] at the new node.
Reachability
None from production that I could find. The five add_file_node call sites take their parent either from ensure_file_node's result or from NodeId{0}. The fallback is a latent hazard with no known trigger, so it's filed separately from #487 instead of being folded into its fix.
Decision to make
Whether a non-zero parent_dir that does not resolve should be an error (ErrorCode::InvalidNodeId, which add_edge already uses for an unresolvable endpoint) or an assertion. Either removes the silent re-rooting and the out-of-bounds index together.
Symptom
add_file_nodeaccepts aparent_dirit cannot resolve. It files the node under the source root instead of refusing it, while the parent's own index still decides whichdir_childrenbucket the name goes into.Mechanism
In
add_file_node(src/graph/dag.cpp), the path comes from the parent node'spath_id. Whenget_file_node(parent_dir)returns null, it falls back to the source root:get_file_nodereturns null for a command-, condition- or phi-tagged id, an index past the end, or an id whose slot holds a different node. In each of those cases:path_to_nodeat<source root>/<name>, a path the caller never asked for.dir_childrenregistration is keyed onnode_id::index(parent_dir), with the tag stripped, so it goes into another node's bucket or past the end.graph.dir_childrenis resized only to the new node's index, andVec::operator[]is unchecked (include/pup/core/vec.hpp). So aparent_dirwhose index exceeds the new node's writes out of bounds. I read this from the source and did not exercise it.Observed with a probe linked against
build/libputup.afrom the PR #490 branch. The probe adds rootfoo.c, thenfoo.cunder unminted parent999:With #490 the call fails, but the message names a path the caller did not request. On
mainwithout #490, the same call silently repointspath_to_node["foo.c"]at the new node.Reachability
None from production that I could find. The five
add_file_nodecall sites take their parent either fromensure_file_node's result or fromNodeId{0}. The fallback is a latent hazard with no known trigger, so it's filed separately from #487 instead of being folded into its fix.Decision to make
Whether a non-zero
parent_dirthat does not resolve should be an error (ErrorCode::InvalidNodeId, whichadd_edgealready uses for an unresolvable endpoint) or an assertion. Either removes the silent re-rooting and the out-of-bounds index together.