Linux neumí vytvořit a otevřít adresář jedním voláním. Návrh na změnu dělí vývojáře jádra
Kdo v Linuxu zakládá adresář a chce mu hned nastavit práva, musí to udělat na dva kroky. V mezeře mezi nimi mu ho může někdo vyměnit. Série záplat Joriho Koolstry chce tu mezeru zavřít tím, že vzkřísí kombinaci příznaků O_CREAT a O_DIRECTORY, kterou jádro tři roky odmítá s chybou. Správce vrstvy VFS je pro, dva vývojáři varují, že program nemá jak poznat, jestli mu novinku jádro nabízí.

Mezi vytvořením a otevřením je okno
Volání mkdir() adresář založí, ale nevrátí nic, čím by ho program mohl dál držet. Volání open() a openat() naopak adresář otevřou, jenže jen takový, který už existuje. Kdo tedy zakládá strom adresářů a chce jim nastavit práva, vlastníka, časy nebo rozšířené atributy, musí to udělat na dva kroky: nejdřív vytvořit, pak otevřít podle jména.
V té mezeře je závod. Jori Koolstra to v popisu klíčové záplaty píše natvrdo: než se program dostane k otevření, může adresář někdo vyměnit za něco jiného. Obejít se to dá jen napůl. Program si smí hned po otevření zavolat fstat() a ověřit, že drží adresář s očekávaným vlastníkem a právy, ale podle Koolstry se to snadno naprogramuje špatně a hlavně to neřekne to podstatné: kdo adresář vytvořil. Adresář založený jiným procesem se stejnými oprávněními je od toho vlastního nerozeznatelný, takže se program nedozví, jestli je jeho a má ho na starost.
Nápad není nový. Stojí v seznamu přání skupiny UAPI, který totéž žádá i pro zařízení a pojmenované roury a jako řešení navrhuje samostatné volání mkdirat_fd().
Příznak, který tři roky vrací jen chybu
Koolstra sáhl po jiné cestě: po kombinaci příznaků, která dnes nedělá nic. O_CREAT říká „vytvoř, když to neexistuje“, O_DIRECTORY říká „uspěj jen tehdy, když je to adresář“. Dohromady si protiřečí a jádro se s nimi nikdy nevypořádalo rozumně. Když cesta neexistovala, vytvořilo obyčejný soubor. Když na ní ležel obyčejný soubor, vrátilo ENOTDIR. Když adresář, vrátilo EISDIR.
V roce 2020 to commit Ala Vira, který se dostal do jádra 5.7, změnilo ještě k horšímu: jádro začalo v prvním případě vracet ENOTDIR a soubor přitom stejně vytvořilo. Tři roky si toho nikdo nevšiml, až v roce 2023 na to narazil Pedro Falcato. Christian Brauner tehdy navrhl, ať se místo návratu ke starému chování udělá to, co dělá NetBSD, tedy vracet EINVAL vždycky. Linus Torvalds podle tehdejšího rozboru na LWN.net souhlasil; v překladu řekl: „Myslím, že můžeme docela dobře předpokládat, že to nikdo doopravdy nepoužívá, a klidně to můžeme pořádně vyčistit.“ Falcato u té příležitosti zjistil, že POSIX nezakazuje ani jedno z těch chování a že FreeBSD kombinaci propustí, pokud adresář existuje.
Brauner změnu zapsal v březnu 2023 do commitu 43b450632676 a do jeho zprávy dal i důvod: kromě úklidu staré chyby to podle něj otevírá možnost dát té kombinaci příznaků v budoucnu něco intuitivnějšího. Právě o tu možnost teď jde.
Co série mění
Nová sémantika je prostá. O_CREAT|O_DIRECTORY provede mkdir a otevře, co vzniklo, a vrátí přitom deskriptor, který adresář drží. Poslední verze série z 12. července má deset záplat, mění 224 řádků v souboru fs/namei.c a přidává test o 201 řádcích. Jedna ze záplat musela předsunout kontrolu, jestli smí program adresář otevřít pro zápis: u obyčejných souborů se dělá až na konci, tady by to znamenalo adresář vytvořit a hned potom volání odmítnout.
Ne všude to bude fungovat. Souborové systémy s vlastním atomickým otevíráním, tedy 9p, Ceph, FUSE, GFS2, NFS, klient SMB a vboxsf, by na to podle Koolstry potřebovaly rozšířit své protokoly. Zatím u nich jádro bit O_CREAT odstraní: existující adresář se otevře, ale u toho, který se měl vytvořit, přijde ENOENT.
Druhé omezení se týká adresářů s příznakem sticky, tedy třeba /tmp, kde smí soubor smazat jen jeho vlastník. Vlastní přepínač pro adresáře série nezavádí a dosavadní pojistka pro obyčejné soubory na ně nedosáhne: podle Koolstry by to zaskočilo správce, kteří s takovou možností nepočítají.
Spor je o to, jak program pozná, že to jádro umí
Spor se nevede o samotnou vlastnost, ale o to, že se zapíná kombinací příznaků, a ne novým voláním. Christoph Hellwig namítá, že jádro neznámé a neověřené kombinace příznaků u open() odjakživa ignoruje, takže se jim nesmí potichu dát význam, pokud to není zpětně slučitelné. Na Braunerovu obranu, že se dosud nic nerozbilo, odpověděl v překladu takto: „Samozřejmě se dosud nic nerozbilo, protože to nikdo nepoužíval. Jakmile tomu dáme význam, začne se to používat a rozbije se to.“
Falcato ten problém rozepsal na příklady. Program by se musel vyrovnat se čtyřmi chováními téže kombinace. Zjistit podle verze jádra, na kterém běží, které z nich platí, přitom nemusí být možné: podniková jádra si vlastnosti backportují na přání, jak se stalo u atomických zápisů v souborovém systému XFS, které vyšly v jádře 6.16 a v SUSE Linux Enterprise Serveru i v Oracle UEK 8 se objevily už v 6.12. K tomu se přidává filtr seccomp, který může neznámé volání odmítnout hláškou o tom, že volání vůbec není.
Brauner, správce vrstvy VFS, to označil za konstrukci, která neobstojí: oprava, po které kombinace vrací tvrdou chybu, je podle něj v dlouhodobě podporovaných jádrech roky a nepřišlo na ni ani jedno hlášení o regresi. Připomněl i tři případy, kdy jádro už jednou obsazený příznak přeznačilo: CLONE_STOPPED na CLONE_NEWCGROUP, CLONE_PID na CLONE_PIDFD a O_TMPFILE na kombinaci __O_TMPFILE|O_DIRECTORY. Sám přitom rozlišuje, co je a co není v pořádku: dát význam kombinaci, která se dřív ignorovala, se podle něj nemá, kdežto použít znovu kombinaci, která byla odmítána, ano.
Do jednoho bodu se ti dva nesejdou. Brauner tvrdí, že se změna z roku 2023 dostala do všech dlouhodobě podporovaných jader, Falcato uvádí, že vyšla v jádře 6.4, dostala se zpětně do 6.1, ale ne do 6.2 a 6.3 a ne do 5.15 ani 5.10. Kdo z nich má pravdu, se z diskuse nedozvíme; ani jeden pro své tvrzení odkaz neuvedl.
Koolstra odpovídá, že komu záleží na starých jádrech, ať si po vytvoření ověří, že opravdu drží adresář, nebo ať si prostě vyžádá rozumnou minimální verzi jádra. NeilBrown navrhl kompromis, který by hádání zrušil: přidat do volání openat2() příznak ve smyslu „selži, pokud tuhle kombinaci výslovně neumíš“. Ptá se, proč to neudělat rovnou, když by se tím psaní bezpečné aplikace i výběr jádra staly snadné.
Koolstra proti tomu má dvě námitky, ani jednu ale nepovažuje za zabijáka nápadu. Volání openat2() podle něj blokovala a možná dosud blokuje řada profilů seccomp, mimo jiné v systemd, takže by se k tomu zjišťování nedostaly zrovna ty velké aplikace, jejichž potřeby změny v rozhraní jádra obvykle pohánějí. A za druhé je to podle něj věc slohu: openat2() má otevírat soubory, ne zjišťovat vlastnosti jádra. Sám nadhodil samostatné volání has_feature(2), kterému by se předalo číslo volání a popis vlastnosti.
Kde to stojí
Sloučeno není nic. Brauner 24. července požádal o novou verzi s podporou v openat() i openat2(); Koolstra odpověděl, že to už v posílání z 12. července je a jen zapomněl sérii označit jako čtvrtou. Dřív téhož měsíce ho Brauner požádal, ať ze série vyndá nesouvisející opravy, protože zásah do auditu by si vyžádal zapojení lidí od auditu a slučování by zdržel. To se stalo: nová verze má deset záplat místo čtrnácti a audit řeší zvlášť.
Kdo o tom rozhodne, napsal Koolstra sám: je to na Christianovi.
Zdroje: série záplat a diskuse k ní v archivu konference linux-kernel na marc.info, rozbor LWN.net z 27. března 2023, commit 43b450632676 v repozitáři jádra a seznam přání skupiny UAPI.