Uzavření descriptorů po forku

Uzavření descriptorů po forku
« kdy: 24. 04. 2011, 23:27:41 »
Zdar. Řeším tu jeden problém se spouštění apikací vfork + execve.  Mám docela problém s automatickým sdílením deskriptorů po této velké dvojici.

Píšu si objekt pro manipulaci se spuštěným procesy, což mimojiné umí třeba různé způsoby vytváření rour předtím, než jsou procesy spuštěny. Rád bych po spuštění procesů, aby jediné, co se nasdílí mezi procesy byly deskriptory 0,1,2 a všechno ostatní se zavřelo. Přitom nemohu spolehat na O_CLOEXEC, jednak nemám jistotu, že to programátoři budou všude používat a jednak některé tyhle techniky jsou zavedeny až od jádra 2.6.23 a mývám problémy s distribucemi, kde jsou jádra starší.

Tak mě napadlo, že bych všechny deskriptory zavřel po forku ručně, pomocí smyčky přes všechny a zavolání close(). Ale jak zjistit jejich seznam? Stačilo by, kdybych věděl číslo největšího deskriptoru. Napadá někoho jiné řešení?

PS použil jsem google, a neuspěl jsem


Gargamel

Re: Uzavření descriptorů po forku
« Odpověď #1 kdy: 24. 04. 2011, 23:55:30 »
nove openssh neco takoveho dela, zkus mrknout do kodu.

Michal Privoznik

Re: Uzavření descriptorů po forku
« Odpověď #2 kdy: 25. 04. 2011, 00:17:03 »
No, zoznam fd mozes ziskat cez /proc/self/fd/, resp. /proc/$(pid)/fd/

Re: Uzavření descriptorů po forku
« Odpověď #3 kdy: 25. 04. 2011, 00:39:10 »
No díky za nasměrování. Ale portable cesta neexistuje, což mě lehce šokuje. Přitom souhlasím s tím, proč to openssh dělá, je to celkem velké bezpečnostní riziko.

Implementačně se komplikuje i to procházení /proc. Nejsem si jist, jestli to lze bezpečně dělat při vforku. Je to přeci jen nějaká alokace uvnitř parent procesu, přestože se po zavření adresáře paměť uvolní... Jedna z možností je připravit si seznam deskriptorů ještě před vforkem.

Jenže /proc není vždy dostupný, například u chrootu. Pak mi nezbývá než zavírat na slepo. Dělám to metodou počítání hustoty čísel otevřených fd. Pokud při hromadném zavírání nenarazím na otevřený deskriptor po 100 cyklech, tak už dál nehledam, protože je málo pravděpodobné, že by tak velká díra vůbec mohla existovat... ale pořád to možné je.

Našel jsem akorát BSD closeform(2), škoda, že to není na linuxu.

Re: Uzavření descriptorů po forku
« Odpověď #4 kdy: 25. 04. 2011, 00:42:13 »
Našel jsem akorát BSD closeform(2), škoda, že to není na linuxu.
closefrom(2)


anonymous

Re: Uzavření descriptorů po forku
« Odpověď #5 kdy: 25. 04. 2011, 02:55:38 »
uf, to sou bludy.

man getrlimit a RLIMIT_NOFILE
melo by to bejt posix prenositelne.

vic jak 16k deskriptoru nebejva caste.

pokud je to mozne, pouzivej FD_CLOEXEC - klidne ho nastav pousalne pro vsechny deskriptory stejnym zpusobem jako bys zaviral pred execve, akorat to udelej pri startu aplikace.

a pak si ohlidej ze kazdej novej deskriptor dostane tento flag taktez. duvod je ten ze fork/exec pak o par ms rychlejsi kdyz se to nemusi zavirat pak.

jo a pouzivej vfork.

martin

Re: Uzavření descriptorů po forku
« Odpověď #6 kdy: 25. 04. 2011, 09:31:20 »
Abych pravdu řekl, tak mi uniká kde je problém.

Pokud píšeš knihovnu, která zajišťuje spouštění procesů, tak uživatel by měl vědět, že před voláním tvého API má uzavřít _potřebné_ deskriptory resp. uklidit po sobě. Příp. bys měl API rozšířit tak, aby si z tvé knihovny volal uživatelovy "úklidové funkce" (callbacky).

Všechno ostatní je neportabilní a nespolehlivé. Ono totiž "úklid" nemusí znamenat jen uzavřít deskriptor.

Pokud chceš naopak uzavírat deskriptory, které vznikají v tebou použité cizí knihovně, tak pořádná knihovna by měla mít také "uklidové funkce".

Řešit to "automaticky" podle mne nevede ve výsledku k ničemu užitečnému.

Re: Uzavření descriptorů po forku
« Odpověď #7 kdy: 25. 04. 2011, 09:44:42 »
Pokud píšeš knihovnu, která zajišťuje spouštění procesů, tak uživatel by měl vědět, že před voláním tvého API má uzavřít _potřebné_ deskriptory resp. uklidit po sobě. Příp. bys měl API rozšířit tak, aby si z tvé knihovny volal uživatelovy "úklidové funkce" (callbacky).

No já se to snažím napsat portabilní aspoň mezi linuxem a windowsem. No a ten styl práce je trochu jiný, na Windows totiž nemusím uzavírat deskriptory, takže mi přijde divné zavádět pravidla, která platí jen na jedné platformě. Na Windows mohu otevřít deskriptory tak, aby se sdílely, ale musím to explicitně uvést. Opt-in mi tedy přijde lepší, než opt-out.

Opravdu nechci, aby uživatel řešil při spouštění procesu, které kde má pootevírané soubory, k čemu pak taková knihovna je? Ideální stav je přitom tento:

Kód: [Vybrat]
Process p("/bin/bash -c ls");
p.start();
p.join();

případně

Kód: [Vybrat]
Process p1("/bin/bash -c ls"),p2("/usr/bin/sort");
p1 | p2;
p1.start();p2.start();
p1.join();p2.join();

Kde je prostor pro nějaké callbacky?

Spíš mě napadlo lepší řešení. Protože bych chtěl dosáhnout toho, aby uživatel mé knihovny otevíral soubory také pomocí mé knihovny, dám všude tam, kde se otevírají nějaké deskriptory automaticky O_CLOEXEC a zavedu API pro sdílení deskriptorů. Fakt je to lepší, protože tím odpadají i takové zbytečné problémy, jako že někdo v jednom vláknu otevře soubor a než mu stačí udělat opt-out ze sdílení, jiné vlákno udělá fork(). Ale je to takový nejistý... zbytek bude řešeno v guidelines.

martin

Re: Uzavření descriptorů po forku
« Odpověď #8 kdy: 25. 04. 2011, 20:59:54 »
No já se to snažím napsat portabilní aspoň mezi linuxem a windowsem. No a ten styl práce je trochu jiný, na Windows totiž nemusím uzavírat deskriptory, takže mi přijde divné zavádět pravidla, která platí jen na jedné platformě.

:-) No to tak jako tak zavádíte. Buďto zavádíte API z win do lin nebo obráceně.

Citace
Kde je prostor pro nějaké callbacky?

V původním vágním popisu :-)

Nicméně pro váš (poměrně omezující) příklad použití opravdu stačí zavřít ty deskriptory a jednou z možností je zjistit velikost tabulky deskriptorů pomocí sysctl(3). Bude vám to fungovat i v chrootu a nebude to většinou příliš velké číslo a kromě close(2) není většinou potřeba dělat jiné úklidy.

Pak už se můžete jen modlit, že se nezmění implementace deskriptorů v tabulce procesu :-)

Druhou možností je při autokonfiguraci zjistit, zda existuje na dané platformě rfork(2) (např. freebsd). Ten umožňuje fork s vyčištěním tabulky deskriptorů.

Citace
Spíš mě napadlo lepší řešení. Protože bych chtěl dosáhnout toho, aby uživatel mé knihovny otevíral soubory také pomocí mé knihovny dám všude tam, kde se otevírají nějaké deskriptory automaticky O_CLOEXEC a zavedu API pro sdílení deskriptorů.

Tohle bych nedoporučoval. Co s deskriptory, které bude otevírat cizí knihovna? Co s deskriptory, které jsou otevřené již při spuštění procesu? Co s další teoretickou knihovnou, která bych chtěla dělat totéž tj. vynucovat otevírání souborů přes své API? Navíc nemůžete řešit jen soubory ale všechny typy objektů.

Re: Uzavření descriptorů po forku
« Odpověď #9 kdy: 25. 04. 2011, 21:37:25 »
Tohle bych nedoporučoval. Co s deskriptory, které bude otevírat cizí knihovna? Co s deskriptory, které jsou otevřené již při spuštění procesu? Co s další teoretickou knihovnou, která bych chtěla dělat totéž tj. vynucovat otevírání souborů přes své API? Navíc nemůžete řešit jen soubory ale všechny typy objektů.

Jde v zásadě o tyto prostředky. Soubory, roury, sockety, obecně cokoliv, co se dá číst nebo zapisovat jako stream. Různé pseudodeskriptory jako eventfs, signalfd neuvažuju. Princip neutrality říká, že to se knihovny netýká, to by neměla řešit, a zavírání všech deskriptorů při spuštění procesu tak trochu princip neutrality narušuje. Takže po zvážení všech komplikací, včetně výše uvedených problémů se zavíráním deskriptorů naslepo jsem se rozhodl to řešit touto cestou.

Jinak co jde o knihovny třetích stran, není na mě, abych řešil problémy cizích knihoven, spíš abych já sám je nevytvářel. Považuji tedy používání O_CLOEXEC skoro za povinnost. Má knihovna má samozřejmě schopnost "otevřít soubor z FD", takže pokud někdo chce používat mé knihovny s knihovnou třetích stran, může použít tuto funkcionalitu (je tam i možnost převést stream z mé knihovny na FD). Nicméně i tady budu nastavovat FD_CLOEXEC, tak, aby se deskriptory adaptované knihovnou chovaly konsistentně.

Navíc to zjednodušuje opt-in. Mohu určit, které deskriptory se budou sdílet a nechat si vygenerovat fake-filename takového deskriptoru. To pak předat na příkazové řádce child procesu, který... pokud je napsán v mé knihovně... může soubor otevřít standardním postupem. Tohle mi totiž bude fungovat i pod Windows (Pomocí DuplicateHandle se zapnutým sdílením).

Předpokládám, že se to týká i socketů.

Citace
:-) No to tak jako tak zavádíte. Buďto zavádíte API z win do lin nebo obráceně.

opt-in z Windows mi přijde lepší, proto převeznu formu API z win. Ale v jiných místech mám zase API dle linuxu  (například dvojice mutex-con.variable ve Windows citelně chybí, dá se emulovat)

martin

Re: Uzavření descriptorů po forku
« Odpověď #10 kdy: 25. 04. 2011, 22:13:11 »
Princip neutrality říká, že to se knihovny netýká, to by neměla řešit, a zavírání všech deskriptorů při spuštění procesu tak trochu princip neutrality narušuje.

Mohl byste mi přiblížit obsah principu neutrality?

Citace
Považuji tedy používání O_CLOEXEC skoro za povinnost. Má knihovna má samozřejmě schopnost "otevřít soubor z FD", takže pokud někdo chce používat mé knihovny s knihovnou třetích stran, může použít tuto funkcionalitu (je tam i možnost převést stream z mé knihovny na FD). Nicméně i tady budu nastavovat FD_CLOEXEC, tak, aby se deskriptory adaptované knihovnou chovaly konsistentně.

Asi jsem dost staromódní, ale stále uznávám "be liberal in what you accept, and conservative in what you send". Navíc k deskriptorům knihoven nemusí existovat API (zapouzdření).

Jinak můj názor je, že právě takové použití vaší knihovny ten problém vytváří a nejedná se o problém cizích knihoven. Váš požadavek na otevírání souboru pouze přes vaše API není právě "standardní".

Citace
Navíc to zjednodušuje opt-in. Mohu určit, které deskriptory se budou sdílet a nechat si vygenerovat fake-filename takového deskriptoru. To pak předat na příkazové řádce child procesu, který... pokud je napsán v mé knihovně... může soubor otevřít standardním postupem. Tohle mi totiž bude fungovat i pod Windows (Pomocí DuplicateHandle se zapnutým sdílením).

To, že vám jako autorovi takové knihovny, něco zjednodušuje implementaci knihovny není tak podstatné jako to, že by knihovna měla zjednodušovat práci uživateli takové knihovny.

Já bych volil ten sysctl, sysconf, nebo getrlimit. Ale to API bych pořádně promyslel.

Re: Uzavření descriptorů po forku
« Odpověď #11 kdy: 26. 04. 2011, 02:22:56 »
Princip neutrality říká, že to se knihovny netýká, to by neměla řešit, a zavírání všech deskriptorů při spuštění procesu tak trochu princip neutrality narušuje.
Mohl byste mi přiblížit obsah principu neutrality?


Laicky řečeno, nepletu se do věcí, do kterých mi nic není. Zavřením všech deskriptorů při spuštění jiného procesu bych zasáhl do obvyklých zvyklostí programů třetích stran, které třeba mají důvod, proč mají sdílení zapnuté. Nechci prostě opravovat "průser" unixového API, chovám se k němu neutrálně.

Jinak můj názor je, že právě takové použití vaší knihovny ten problém vytváří a nejedná se o problém cizích knihoven. Váš požadavek na otevírání souboru pouze přes vaše API není právě "standardní".
To jste nepochopil. Už v okamžiku, kdy uživatel místo open použije std::fstream je tedy nestandardní chování? (mimochodem, jaké flagy nastavuje STL na své deskriptory???) Požadavek na použití mého API má samozřejmě několik podmínek. Jednak by mělo zjednodušit práci programátora víc, než přímou práci s open/std::fstream a jednak by mělo být multiplatformní, tj, ovládat se stejně minimálně pod oběma mainstreamovýma platformama (windows/linux). Pouze definuju styčné body, kde je třeba občas přecházet mezi mým API a třeba API knihovny třetí strany, například, pokud knihovna vyžaduje FD pro další práci a já mám soubor otevřen v mém objektu představující soubor.

Nicméně smyslem bylo vysvětlit právě ten princip neutrality. Místa, kde dochází k přístupu do mého API se uplatňují jeho (moje) pravidla tak, aby všechny objekty spravované tím API se chovaly konzistentně, tedy, aby deskriptor, který adaptuje moje API byo nesdílený, dokud si uživatel o sdílení explicitně nepožádá. Deskriptory, které si vytvoří knihovna třetí strany v rámci své práce a nevstupují do mého API mi jsou naprosto volné. Není moje zodpovědnost žehlit chyby v knihovnách třetích stran a pokud knihovna interně vytváří deskriptory, které se by default sdílí, je to problém knihovny a programátora, který tu knihovnu používá. Má knihovna neumí rozhodnout, zda je to záměr, nebo chyba.


Citace
Citace
Navíc to zjednodušuje opt-in. Mohu určit, které deskriptory se budou sdílet a nechat si vygenerovat fake-filename takového deskriptoru. To pak předat na příkazové řádce child procesu, který... pokud je napsán v mé knihovně... může soubor otevřít standardním postupem. Tohle mi totiž bude fungovat i pod Windows (Pomocí DuplicateHandle se zapnutým sdílením).
To, že vám jako autorovi takové knihovny, něco zjednodušuje implementaci knihovny není tak podstatné jako to, že by knihovna měla zjednodušovat práci uživateli takové knihovny.
Myslím si, že to zjednoduší práci i uživatelům. Jak pošlete child aplikaci 5 vstupních a 7 výstupních streamů? Já si je předám na příkazové řádce :-)
Citace
Já bych volil ten sysctl, sysconf, nebo getrlimit. Ale to API bych pořádně promyslel.

Mě to přijde jako hackoidní řešení. Chápu to u sshčka, i když tam se taky bere jen prvních  64 deskriptorů, pak se člověk ptá, zda to vůbec má smysl.

Průser unixu je zde o tom, že deskriptory se duplikují do cizích procesů a ještě větší průser je, že se duplikují dál do jejich child procesů bez kontroly, protože process, který dál forkuje a provádí exec se vůbec o existenci těchto deskriptorů nemusí dozvědět. Protunelovat se takto do zabezpečené seance tak není žádný problém.

Neutrální postoj je ten, že si to zabezpečím u sebe a nebudu ten průser řešit nějakým hackem a zadělávat si na další průser.

martin

Re: Uzavření descriptorů po forku
« Odpověď #12 kdy: 26. 04. 2011, 05:14:33 »
Laicky řečeno, nepletu se do věcí, do kterých mi nic není. Zavřením všech deskriptorů při spuštění jiného procesu bych zasáhl do obvyklých zvyklostí programů třetích stran, které třeba mají důvod, proč mají sdílení zapnuté. Nechci prostě opravovat "průser" unixového API, chovám se k němu neutrálně.

To není pravda. Jednak se o průser nejedná a za další do ničeho nezasahujete, pokud chcete provádět věci dle předchozího příkladu. Naopak neřešíte běžné situace.

Spíš mi přijde, že jste odkojen win32 (pletu se?) a tak vám některé věci z linuxu nesednou. Já ale žádný problém nevidím, protože jsem zvyklý na fork a jeho vlastnosti.

Citace
Už v okamžiku, kdy uživatel místo open použije std::fstream je tedy nestandardní chování?

Nestandardní je podmiňovat použití knihovny změnou přístupu k API OS. Jistě existují vyjimky (paměťový management apod.), ale běžné to není. Navíc to stále neřeší případy, kdy proces obsahuje deskriptor(y), které uživatel explicitně neotevřel. Takže stejně se o jejich uzavření (pokud je chce uzavřít) musí postarat sám. Tak kde je ta výhoda std::fstream?

Citace
(mimochodem, jaké flagy nastavuje STL na své deskriptory???)

Nevím, ale nečekal bych nějakou spásu. Mrkněte se do zdrojáků.

Citace
Deskriptory, které si vytvoří knihovna třetí strany v rámci své práce a nevstupují do mého API mi jsou naprosto volné. Není moje zodpovědnost žehlit chyby v knihovnách třetích stran a pokud knihovna interně vytváří deskriptory, které se by default sdílí, je to problém knihovny a programátora, který tu knihovnu používá. Má knihovna neumí rozhodnout, zda je to záměr, nebo chyba.

Opakuji. Nejde o chybu knihoven třetích stran. Knihovna nemá a nemůže mít nejmenší tušení, že vy děláte poměrně radikální manipulaci s procesem (fork, execve).

Jde o chybu vaší knihovny, že se nechová ke zdrojům procesu konzistentně a tudíž korektně (část deskriptorů zavíráte a část ne). Vy vytváříte proces, vy jste zodpovědný za vyřešení problémů, kterou jsou s tím spojené. A to navíc tak, aby to práci uživateli ušetřilo.

V tuto chvíli není jediný důvod, aby používal vaši fstream. Ani jeden důvod. O obsluhu ostatních deskriptorů se musí postarat tak, jako tak, takže k čemu by si měl komplikovat život?

Citace
Jak pošlete child aplikaci 5 vstupních a 7 výstupních streamů? Já si je předám na příkazové řádce :-)

Když pominu, že můžete předat (v unixu) jen čísla deskriptorů a nikoli streamy, tak to záleží na té aplikaci, jaké si k tomu definuje API. Předat pár čísel lze různými způsoby (příkazová řádka, fixní čísla deskriptorů, přes reálný, přes stdin/out, příp. další IPC prostředky)

Citace
Citace
Já bych volil ten sysctl, sysconf, nebo getrlimit.

Mě to přijde jako hackoidní řešení. Chápu to u sshčka, i když tam se taky bere jen prvních  64 deskriptorů, pak se člověk ptá, zda to vůbec má smysl.

Ať už se vám to zdá jakékoli, projití té tabulky je v současné době jediné řešení. Ve fbsd by šlo ještě použít kombinaci duplikace deskriptorů a closefrom.

Citace
Průser unixu je zde o tom, že deskriptory se duplikují do cizích procesů a ještě větší průser

Má Anglie průser v tom, že jezdí na levé straně? To je jako tvrdit, že průser windows je v tom, že nový proces nelze udělat bez souboru na FS. Jedná se prostě o 2 různé přístupy a oba mají + a -.

Tohle (v)fork API tu je přes 30 let. Ukažte mi ten průser v praxi.

Re: Uzavření descriptorů po forku
« Odpověď #13 kdy: 26. 04. 2011, 16:44:32 »
Navíc to stále neřeší případy, kdy proces obsahuje deskriptor(y), které uživatel explicitně neotevřel. Takže stejně se o jejich uzavření (pokud je chce uzavřít) musí postarat sám. Tak kde je ta výhoda std::fstream?

Ano, to je ten průser o kterém mluvím. Programátor se musí starat o věci, nad kterými nemá absolutní (nebo spíš žádnou) kontrolu. Musí být připraven na situace, o kterých nemá ani potuchy.

Opakuji. Nejde o chybu knihoven třetích stran. Knihovna nemá a nemůže mít nejmenší tušení, že vy děláte poměrně radikální manipulaci s procesem (fork, execve).
Netušil jsem, že spuštění cizího procesu je radikální manipulace s procesem. Existuje nějaká neradikální cesta? Něco jako CreateProcess ve Windows?

Jde o chybu vaší knihovny, že se nechová ke zdrojům procesu konzistentně a tudíž korektně (část deskriptorů zavíráte a část ne).
Nezavírá žádné, jen ty svoje označuje flagem O_CLOEXEC. Chci budoucímu uživateli mé knihovny dát jistotu, že si knihovna nevytvoří žádný prostředek, který by přežil exec aniž by o tom programátor věděl.

Vy vytváříte proces, vy jste zodpovědný za vyřešení problémů, kterou jsou s tím spojené. A to navíc tak, aby to práci uživateli ušetřilo.

Ale houby, za problémy spojené s vytvářením nového procesu je zodpovědný operačný systém. Ten ale doposud problémy spíš vytváří.

V tuto chvíli není jediný důvod, aby používal vaši fstream. Ani jeden důvod. O obsluhu ostatních deskriptorů se musí postarat tak, jako tak, takže k čemu by si měl komplikovat život?

Asi proto, že mé streamy nejsou určeny k vytváření procesů, ale k něčemu úplně jinému a tohle jen tak bokem. Ano, s jednou z možností je vytvořit process a k němu můj stream, který se dá plnit daty stejně jako varianta pipe, fork, exec, ale bez zbytečných close (pipe je také O_CLOEXEC, pouze po forku se descriptory zduplikují do descriptorů 0,1,2 které jediný přežijí.

Tohle (v)fork API tu je přes 30 let. Ukažte mi ten průser v praxi.
Hurá, po třiceti letech jsme přišli na průser v Unixu. Budeme dalších 30 let dělat, že neexistuje? Tenhle váš závěr je likvidační.

A příklad toho průseru? OpenSSH

V diskuzi je už zbytečné dál pokračovat. Díky za ujasnění problému.