Skip to content

Commit f93f317

Browse files
committed
fix(index): reject stale xlings cache paths
1 parent a5cc31c commit f93f317

3 files changed

Lines changed: 110 additions & 1 deletion

File tree

.agents/docs/2026-05-31-index-refresh-cache-labels-plan.md

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -170,6 +170,36 @@ package's index file, not just the official index directory. If the package
170170
file is absent, mcpp should silently run `xlings update` before calling either
171171
the NDJSON interface install path or the direct CLI fallback.
172172

173+
### 8. xlings index cache can be poisoned by temp-home symlinked indexes
174+
175+
The fourth PR checkpoint still failed in the same Linux musl step. Local
176+
inspection revealed an additional xlings cache layer:
177+
178+
```text
179+
xim-pkgindex/.xlings-index-cache.json
180+
```
181+
182+
That cache stores absolute `path` values for each xpkg file. mcpp e2e tests
183+
inherit the global `xim-pkgindex` into temp `MCPP_HOME` directories, often via
184+
symlink. When xlings rebuilds the cache from that temp home, it can write paths
185+
like:
186+
187+
```text
188+
/tmp/tmp.X.../mcpp-home/registry/data/xim-pkgindex/pkgs/m/musl-gcc.lua
189+
```
190+
191+
into the shared global index directory. Later, the main sandbox loads that
192+
cache and `PackageCatalog::build_match_()` calls `load_package()` on the stale
193+
temp path. The load fails, the match is discarded, and the user-facing error is
194+
still:
195+
196+
```text
197+
package 'xim:musl-gcc@15.1.0' not found
198+
```
199+
200+
So the mcpp guard must also reject a package cache whose target package entry
201+
does not point at the current sandbox's package file.
202+
173203
## Implementation Plan
174204

175205
- [x] Add focused regression coverage for default-index refresh quietness.
@@ -187,6 +217,8 @@ the NDJSON interface install path or the direct CLI fallback.
187217
before auto-installing `xim:` toolchain packages.
188218
- [x] Require the target package file to exist before treating an official
189219
`xim:` index as fresh.
220+
- [x] Reject xlings index caches whose target package path points at a foreign
221+
temp/home directory.
190222
- [x] Validate with the local xlings checkout using the new mcpp binary.
191223
- [x] Push a draft PR and use it as the multi-commit checkpoint.
192224

@@ -273,3 +305,19 @@ the NDJSON interface install path or the direct CLI fallback.
273305
passed.
274306
- `/tmp/mcpp-fresh-codex clean && /tmp/mcpp-fresh-codex build --target
275307
x86_64-linux-musl` passed and resolved `gcc@15.1.0-musl`.
308+
- The next CI run still failed at the same musl step. The remaining issue is
309+
xlings' `.xlings-index-cache.json` using absolute package-file paths that
310+
can be written from e2e temp homes into the shared index directory.
311+
- Added cache-path validation for the target package file in
312+
`is_official_package_index_fresh()`.
313+
- Added two unit tests for foreign/current package paths inside
314+
`.xlings-index-cache.json`.
315+
- Local verification after this cache-path fix:
316+
- `mcpp test -- --gtest_filter=XlingsIndexFreshness.*` passed with 10
317+
matching `test_xlings` cases.
318+
- `mcpp build --no-cache` passed.
319+
- e2e `49_bmi_cache_nested_custom_index.sh`,
320+
`52_local_path_namespaced_index.sh`, and `53_namespaced_cache_label.sh`
321+
passed.
322+
- `/tmp/mcpp-fresh-codex clean && /tmp/mcpp-fresh-codex build --target
323+
x86_64-linux-musl` passed and resolved `gcc@15.1.0-musl`.

src/xlings.cppm

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -317,6 +317,33 @@ std::filesystem::path official_package_file(const Env& env, std::string_view pac
317317
return official_index_dir(env) / "pkgs" / std::string(1, name[0]) / (name + ".lua");
318318
}
319319

320+
std::string json_escaped_path_probe(std::filesystem::path path) {
321+
auto value = path.string();
322+
std::string escaped;
323+
escaped.reserve(value.size() * 2);
324+
for (char c : value) {
325+
if (c == '\\') escaped += "\\\\";
326+
else escaped.push_back(c);
327+
}
328+
return escaped;
329+
}
330+
331+
bool official_index_cache_matches_package_file(const Env& env,
332+
std::string_view packageName) {
333+
auto cache = official_index_dir(env) / ".xlings-index-cache.json";
334+
if (!std::filesystem::exists(cache)) return true;
335+
336+
auto pkg = official_package_file(env, packageName);
337+
if (pkg.empty()) return false;
338+
339+
std::ifstream is(cache);
340+
if (!is) return false;
341+
std::string body((std::istreambuf_iterator<char>(is)), {});
342+
auto rawPath = pkg.string();
343+
return body.find(rawPath) != std::string::npos
344+
|| body.find(json_escaped_path_probe(pkg)) != std::string::npos;
345+
}
346+
320347
void mark_index_refreshed(const std::filesystem::path& indexDir) {
321348
if (!std::filesystem::exists(index_pkgs_dir(indexDir))) return;
322349
std::error_code ec;
@@ -1040,7 +1067,9 @@ bool is_official_package_index_fresh(const Env& env,
10401067
std::int64_t ttlSeconds) {
10411068
if (!is_official_index_fresh(env, ttlSeconds)) return false;
10421069
auto pkg = official_package_file(env, packageName);
1043-
return !pkg.empty() && std::filesystem::exists(pkg);
1070+
return !pkg.empty()
1071+
&& std::filesystem::exists(pkg)
1072+
&& official_index_cache_matches_package_file(env, packageName);
10441073
}
10451074

10461075
int update_index(const Env& env, bool quiet) {

tests/unit/test_xlings.cpp

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,3 +111,35 @@ TEST(XlingsIndexFreshness, AcceptsFreshOfficialPackageFile) {
111111

112112
std::filesystem::remove_all(home);
113113
}
114+
115+
TEST(XlingsIndexFreshness, RejectsOfficialPackageCacheWithForeignPath) {
116+
auto home = make_tempdir("mcpp-xlings-index-freshness");
117+
auto pkg = home / "data" / "xim-pkgindex" / "pkgs" / "m" / "musl-gcc.lua";
118+
std::filesystem::create_directories(pkg.parent_path());
119+
std::ofstream(home / "data" / "xim-pkgindex" / ".mcpp-index-updated") << "ok\n";
120+
std::ofstream(pkg) << "package = {}\n";
121+
std::ofstream(home / "data" / "xim-pkgindex" / ".xlings-index-cache.json")
122+
<< R"({"entries":{"musl-gcc":{"path":"/tmp/foreign/xim-pkgindex/pkgs/m/musl-gcc.lua"}}})";
123+
124+
mcpp::xlings::Env env{.home = home};
125+
126+
EXPECT_FALSE(mcpp::xlings::is_official_package_index_fresh(env, "musl-gcc", 3600));
127+
128+
std::filesystem::remove_all(home);
129+
}
130+
131+
TEST(XlingsIndexFreshness, AcceptsOfficialPackageCacheWithCurrentPath) {
132+
auto home = make_tempdir("mcpp-xlings-index-freshness");
133+
auto pkg = home / "data" / "xim-pkgindex" / "pkgs" / "m" / "musl-gcc.lua";
134+
std::filesystem::create_directories(pkg.parent_path());
135+
std::ofstream(home / "data" / "xim-pkgindex" / ".mcpp-index-updated") << "ok\n";
136+
std::ofstream(pkg) << "package = {}\n";
137+
std::ofstream(home / "data" / "xim-pkgindex" / ".xlings-index-cache.json")
138+
<< std::format(R"({{"entries":{{"musl-gcc":{{"path":"{}"}}}}}})", pkg.string());
139+
140+
mcpp::xlings::Env env{.home = home};
141+
142+
EXPECT_TRUE(mcpp::xlings::is_official_package_index_fresh(env, "musl-gcc", 3600));
143+
144+
std::filesystem::remove_all(home);
145+
}

0 commit comments

Comments
 (0)