From 755e2b49bd0247c7b670664681cf541ad5ed96d6 Mon Sep 17 00:00:00 2001 From: Mathieu Benoit Date: Thu, 17 Sep 2026 09:26:56 -0400 Subject: [PATCH] =?UTF-8?q?[FIX]=20cache=20qemu=20:=20r=C3=A9unir=20les=20?= =?UTF-8?q?pr=C3=A9fixes=20de=20miroir=20sous=20une=20seule=20cl=C3=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Un miroir préfixe le chemin à sa guise — « /rocky/10.2/… », « /mirror/rocky-linux/10.2/… », « /pub/archive/fedora/… » — et le chemin entier donnait deux clés pour un même octet : le cache retéléchargeait ce qu'il détenait. Relevé sur un journal de 7099 noms livrés : 1124 vivaient sous plusieurs chemins, et 3,18 Gio repartaient à l'amont pour rien. Seuls les six derniers segments comptent désormais, les segments vides tombant avec eux. Six est le plus petit nombre sans collision : un chemin Debian en porte six, si bien que cinq confondraient son paquet avec celui d'Ubuntu, qui porte le même nom pour d'autres octets — 38 cas mesurés. Vérifié : deux mutations attrapées. --- EN --- A mirror prefixes the path as it pleases, and the whole path gave two keys for one byte: the cache re-downloaded what it held. Measured over a log of 7099 delivered names: 1124 lived under several paths, and 3.18 GiB went back upstream for nothing. Only the last six segments now count, empty ones falling with them. Six is the smallest number without collision: a Debian path carries six, so five would confuse its package with Ubuntu's, which bears the same name for other bytes — 38 measured cases. Checked: two mutations caught. Assisted-by: Claude Opus 5 --- script/qemu_cache/classify_test.go | 40 ++++++++++++++++++++++++++++++ script/qemu_cache/main.go | 2 +- script/qemu_cache/store.go | 29 +++++++++++++++++++++- 3 files changed, 69 insertions(+), 2 deletions(-) diff --git a/script/qemu_cache/classify_test.go b/script/qemu_cache/classify_test.go index 0d7dbbe..c5379f0 100644 --- a/script/qemu_cache/classify_test.go +++ b/script/qemu_cache/classify_test.go @@ -239,6 +239,46 @@ func TestLesMetadonneesRPMParEmpreinteSontImmuables(t *testing.T) { } } +// Un miroir préfixe le chemin à sa guise, et le chemin entier donnait deux +// clés pour un même octet. Relevé sur un journal réel : 1124 noms de paquets +// vivaient sous plusieurs chemins, et 3,18 Gio repartaient à l'amont pour des +// fichiers que le magasin détenait déjà. +func TestLesPrefixesDeMiroirNeFontQuUneCle(t *testing.T) { + for _, cas := range [][2]string{ + { + "https://a.example/rocky/10.2/AppStream/x86_64/os/Packages/r/rust-1.92.0-2.el10_2.x86_64.rpm", + "https://b.example/mirror/rocky-linux/10.2/AppStream/x86_64/os/Packages/r/rust-1.92.0-2.el10_2.x86_64.rpm", + }, + { + "https://a.example/pub/archive/fedora/linux/updates/42/Everything/x86_64/Packages/n/nodejs-libs-22.22.0-2.fc42.x86_64.rpm", + "https://b.example/pub/fedora-archive/fedora/linux/updates/42/Everything/x86_64/Packages/n/nodejs-libs-22.22.0-2.fc42.x86_64.rpm", + }, + { + "https://a.example/pub/rocky//10.2/BaseOS/x86_64/os/Packages/a/avahi-0.9-2.el10.x86_64.rpm", + "https://a.example/pub/rocky/10.2/BaseOS/x86_64/os/Packages/a/avahi-0.9-2.el10.x86_64.rpm", + }, + } { + a, _ := url.Parse(cas[0]) + b, _ := url.Parse(cas[1]) + if KeySansHote("GET", a) != KeySansHote("GET", b) { + t.Errorf("deux chemins du même fichier donnent deux clés :\n %s\n %s", + cas[0], cas[1]) + } + } +} + +// Deux distributions publient le même NOM pour d'autres octets : les confondre +// servirait le paquet de l'une à l'autre. Six segments gardent ce qui les +// sépare, cinq l'effaceraient — 38 noms en collision sur le journal relevé. +func TestDeuxDistributionsNePartagentPasLaCle(t *testing.T) { + deb, _ := url.Parse("https://deb.example/debian/pool/main/p/poppler-data/poppler-data_0.4.12-1_all.deb") + ubu, _ := url.Parse("https://ubu.example/ubuntu/pool/main/p/poppler-data/poppler-data_0.4.12-1_all.deb") + if KeySansHote("GET", deb) == KeySansHote("GET", ubu) { + t.Error("deux distributions partagent une clé : l'une serait servie" + + " avec le paquet de l'autre") + } +} + // Le même index par empreinte, servi par deux miroirs. Sans clé portable, // changer de miroir vide le cache de ses index : une installation hors ligne // échoue alors sur des octets que le magasin détient pourtant, et le message diff --git a/script/qemu_cache/main.go b/script/qemu_cache/main.go index be5503f..f770e11 100644 --- a/script/qemu_cache/main.go +++ b/script/qemu_cache/main.go @@ -25,7 +25,7 @@ import ( "time" ) -const version = "0.2.13" +const version = "0.2.14" func main() { var ( diff --git a/script/qemu_cache/store.go b/script/qemu_cache/store.go index c1e3248..ef7ec4c 100644 --- a/script/qemu_cache/store.go +++ b/script/qemu_cache/store.go @@ -95,8 +95,35 @@ func Key(method, rawURL string) string { // Réservé aux fichiers dont le NOM porte l'identité — paquets, index de // dépôt. L'appliquer à tout ferait entrer en collision les « /index.html » de // deux sites sans rapport. +// +// Le chemin ENTIER ne suffisait pas : un miroir le préfixe à sa guise — +// « /rocky/10.2/… », « /mirror/rocky-linux/10.2/… », « /pub/archive/fedora/… » +// — et le même octet prenait alors deux clés. Seule la FIN du chemin est +// retenue (voir SegmentsDeCle), ce qui réunit ces copies sans jamais +// confondre deux distributions. +// SegmentsDeCle : combien de segments de FIN de chemin identifient un fichier. +// +// Six, et pas moins : un chemin Debian en porte exactement six — +// « debian/pool/main/p//.deb » — si bien que cinq +// effaceraient le segment de distribution et donneraient la même clé au +// paquet d'Ubuntu, qui porte le même nom pour d'autres octets. Relevé sur un +// journal d'accès de 7099 noms livrés : six réunit 1021 des 1124 noms vus +// sous plusieurs chemins, sans confondre aucun contenu ; cinq en confond 38. +const SegmentsDeCle = 6 + func KeySansHote(method string, u *url.URL) string { - chemin := u.Path + segments := make([]string, 0, SegmentsDeCle+2) + for _, s := range strings.Split(u.Path, "/") { + // Les segments vides tombent : un miroir écrit « /pub/rocky//10.2 », + // et deux écritures d'un même chemin feraient sinon deux clés. + if s != "" { + segments = append(segments, s) + } + } + if len(segments) > SegmentsDeCle { + segments = segments[len(segments)-SegmentsDeCle:] + } + chemin := strings.Join(segments, "/") if u.RawQuery != "" { chemin += "?" + u.RawQuery }