PDF Library for Delphi julkaisee RepairQDFFile-kutsun tulosteen sisäisen kirjoittajan, TPDFQDFFileWriter-luokan, kautta, eikä se koskaan avaa kohdetta kirjoittamista varten: korjatut tavut menevät yksinomaisesti luotuun väliaikaistiedostoon samaan hakemistoon, tiedosto huuhdellaan ja suljetaan, ja vasta sitten se nimetään kohteen päälle MoveFileExW-kutsulla Windowsissa tai rename(2)-kutsulla POSIXissa. Jos jokin epäonnistuu ennen uudelleennimeämistä, kohde säilyttää jokaisen tavunsa ja kutsuja näkee LastErrorCode-arvon 305. Asiakirjan korjaaminen muistissa on korjausominaisuuden helppo puolisko. Tuloksen saaminen levylle niin, ettei käyttäjälle koskaan jää nollapituista tai puoliksi kirjoitettua tiedostoa, on se puolisko, josta tämä artikkeli kertoo
Miksi epäonnistuva korjaus voi silti tuhota kohdetiedoston?
Koska toimintojen järjestys oli väärä. Ennen versiota v3.539.13 RepairQDFFile avasi tulosteen PLCreateFileStream(OutputFileName, fmCreate) -kutsulla ja antoi sitten tuon streamin jäsentimelle. fmCreate katkaisee tiedoston avattaessa, joten siihen mennessä kun QDF-läpikäynti päätti, ettei syöte ollut korjattavissa, kohde oli jo tyhjennetty. Paikan päällä tehtävä korjaus, jossa InputFileName ja OutputFileName ovat sama polku, muutti hylätyn syötteen menetetyksi tiedostoksi. Jäsennin itse käyttäytyi hyvin: matalan tason PDFQDFRepair-funktio jättää kohdestreamin koskemattomaksi, kun se hylkää monitulkintaiset merkit. Tuo suoja oli yksinkertaisesti merkityksetön, koska julkinen rajapinta oli katkaissut tiedoston yhtä kutsua aiemmin
Version v3.539.13 korjaus siirsi korjauksen TMemoryStream-streamiin ja avasi tulosteen vasta kun PDFQDFRepair oli onnistunut. Se sulkee jäsennysvirheen reiän eikä mitään muuta. Kirjoitusvaihe oli edelleen fmCreate ja sen perään CopyFrom, joten levyn täyttyminen, kesken kaiken tullut jakorikkomus tai poikkeus katkaisun ja viimeisen WriteBuffer-kutsun välissä jätti kohteen silti vahingoittuneeksi. Muisti edellä tehty korjaus suojaa huonolta syötteeltä. Levylle julkaiseminen tarvitsee oman rajansa, ja versiot v3.539.14 ja v3.539.15 rakensivat sellaisen
// v3.539.12: kohde katkaistaan ennen kuin syöte validoidaan
Output := PLCreateFileStream(OutputFileName, fmCreate);
try
if PDFQDFRepair(Source, Output, QDFError) then // liian myöhäistä sanoa ei
Result := 1;
finally
Output.Free;
end;
// v3.539.15: korjaus muistissa, sitten tavut julkaisukirjoittajalle
Repaired := TMemoryStream.Create;
try
if not PDFQDFRepair(Source, Repaired, QDFError) then
Exit; // kohdetta ei koskaan avattu
Writer := TPDFQDFFileWriter.Create;
try
Writer.Save(Repaired, OutputFileName);
Result := 1;
finally
Writer.Free;
end;
finally
Repaired.Free;
end;
Mitä atominen julkaisu oikeastaan takaa?
TPDFQDFFileWriter.Save takaa, että kohdepolku on joko kokonaan vanha tiedosto tai kokonaan uusi tiedosto, ei koskaan sekoitus, jokaisen sellaisen vian osalta, jonka kirjasto itse pystyy havaitsemaan. Kirjoittaja tekee tämän neljässä vaiheessa, joista jokainen kieltäytyy jatkamasta, ellei edellinen päättynyt. Ensimmäiseksi se ratkaisee kohteen GetFullPathNameW-kutsulla, kutsuen sitä kahdesti ja varaamalla puskurin palautetun pituuden mukaan sen sijaan että olettaisi MAX_PATH-arvon, joten pitkät polut eivät tule hiljaa katkaistuiksi. Toiseksi se luo kohdehakemistoon väliaikaistiedoston, jonka nimi on .pdflib-qdf- plus GUID plus .tmp, käyttäen CreateFileW-kutsua CREATE_NEW-lipulla Windowsissa ja open(2)-kutsua O_CREAT or O_EXCL -lipuilla sekä tilalla 0600 POSIXissa. Molemmat liput saavat luonnin epäonnistumaan, jos nimi on jo olemassa, joten kaksi samasta GUIDista kilpailevaa prosessia ei voi jakaa kahvaa. Kolmanneksi se kopioi korjatun streamin 64 kilotavun paloina WriteBuffer-kutsun kautta, joka nostaa virheen vajaasta kirjoituksesta sen sijaan että palauttaisi luvun, jota kukaan ei tarkista, ja kutsuu sitten FlushFileBuffers- tai fsync(2)-funktiota ja sulkee kahvan. Neljänneksi se nimeää tiedoston uudelleen
procedure TPDFQDFFileWriter.Flush(Target: TStream);
begin
if not FlushFileBuffers(THandleStream(Target).Handle) then
raise EWriteError.Create('Unable to flush QDF output');
end;
procedure TPDFQDFFileWriter.Publish(const TempFileName, FileName: WideString);
begin
// Älä salli levyjen välistä kopiota äläkä poista kohdetta ensin
if not MoveFileExW(PWideChar(TempFileName), PWideChar(FileName),
MOVEFILE_REPLACE_EXISTING or MOVEFILE_WRITE_THROUGH) then
raise EWriteError.Create('Unable to publish QDF output');
end;
Uudelleennimeämisvaihe on se kohta, jossa useimmat itse kyhätyt ”turvallinen tallennus” -rutiinit hajoavat hiljaa. MoveFileExW MOVEFILE_REPLACE_EXISTING-lipulla korvaa kohteen yhdellä tiedostojärjestelmäoperaatiolla samalla levyllä. Kirjoittaja jättää tarkoituksella pois MOVEFILE_COPY_ALLOWED-lipun, koska levyjen välinen siirto rappeutuu kopioi ja poista -sekvenssiksi, joka on täsmälleen se ei-atominen jono, jonka välttämiseksi koko suunnittelu on olemassa. Koska väliaikaistiedosto on kohdehakemistossa, se on rakenteellisesti kohdelevyllä. Kirjoittaja ei myöskään koskaan poista vanhaa tiedostoa ensin; poista ja nimeä uudelleen -parilla on ikkuna, jossa polkua ei ole lainkaan olemassa, ja siihen ikkunaan osuva kaatuminen hävittää asiakirjan. MOVEFILE_WRITE_THROUGH pyytää kutsua olemaan palaamatta ennen kuin uudelleennimeäminen on tavoittanut levyn, mikä sopii yhteen datan nimenomaisen huuhtelun kanssa. POSIXissa rename(2) takaa jo, että uusi nimi korvaa minkä tahansa olemassa olevan tiedoston atomisesti, ja sama hakemistosijoittelu estää sen epäonnistumisen EXDEV-virheellä. Siivous on symmetrinen. Väliaikainen nimi poistetaan finally-lohkossa jokaisella polulla, mikä onnistuessa on no-op, koska uudelleennimeäminen on jo kuluttanut sen, ja epäonnistuessa poistaa osittaisen tiedoston, jottei hakemistoon kerry .tmp-jäänteitä. Tests\QDFFileRegression.inc-tiedoston regressio tarkistaa täsmälleen tämän: jokaisen injektoidun vian jälkeen kohteen tavut vastaavat alkuperäistä, lähteen tavut vastaavat alkuperäistä ja hakemistossa ei ole mitään muuta kuin ne kaksi fixturea
Miksi väliaikaistiedosto löysentää käyttöoikeuksia Windowsissa?
Nil-turvakuvauksella luotu tiedosto perii DACL:n ylähakemistosta eikä siitä tiedostosta, jonka se on korvaamassa. Se on oikea oletus aivan uudelle asiakirjalle ja väärä paikan päällä tehtävälle korjaukselle. Oletetaan, että ylläpitäjä on lukinnut contract.pdf-tiedoston yhdelle tilille suojatulla, ei-perityllä DACL:llä. Sen viereen syntyvä väliaikaistiedosto perii hakemiston laajemmat käyttöoikeudet, ja kun se nimetään contract.pdf-tiedoston päälle, uudelleennimetty tiedosto kantaa laajaa DACL:ää, koska NTFS:n tietoturva kulkee tiedosto-objektin mukana eikä nimen mukana. Korjaus onnistuu, tavut ovat oikein, ja ylläpitäjän konfiguroima käyttöoikeushallinta on hiljaa poissa. Mikään paluuarvossa ei vihjaa siihen
PDF Library for Delphi lukee siksi kohteen DACL:n ennen väliaikaistiedoston luomista ja välittää sen CreateFileW-kutsun lpSecurityAttributes-argumenttina, joten uusi tiedosto syntyy vanhan tiedoston käyttöoikeuksilla eikä uudelleennimeäminen muuta mitään sellaista, minkä ylläpitäjä huomaisi. Luku käyttää GetFileSecurityW-funktiota DACL_SECURITY_INFORMATION-lipulla ja mitoittaa puskurin ensimmäisen kutsun ERROR_INSUFFICIENT_BUFFER-tuloksen mukaan. Kolme ehtoa saavat kirjoittajan epäonnistumaan suljetusti sen sijaan että se arvailisi. Jos DACL:ää ei voi lukea, julkaisu pysähtyy EWriteError-virheeseen, jonka julkinen rajapinta kuvaa arvoksi 305. Jos kuvaus palaa ilman asetettua SE_DACL_PRESENT-bittiä, julkaisu pysähtyy myös, koska sellaisen kuvauksen antaminen CreateFileW-kutsulle päästäisi ytimen turvautumaan prosessin oletus-DACL:ään ja muuttaisi käyttöoikeussemantiikkaa kenenkään sitä pyytämättä. Ja jos kohteessa on FILE_ATTRIBUTE_ENCRYPTED-attribuutti, kirjoittaja kieltäytyy suoraan: väliaikaistiedosto olisi selväkielinen, ja selväkielisen tiedoston nimeäminen EFS-suojatun päälle julkaisee salaamattoman korvaajan jollekin, jonka käyttäjä valitsi salattavaksi tiedostojärjestelmätasolla. EFS ei liity PDF:n standardeihin turvakäsittelijöihin, joita käsitellään salatun asiakirjan latausta käsittelevässä artikkelissa, mutta vikatila on samaa hiljaista heikennystä
Attributes := GetFileAttributesW(PWideChar(Destination));
if Attributes <> INVALID_FILE_ATTRIBUTES then
begin
if (Attributes and FILE_ATTRIBUTE_ENCRYPTED) <> 0 then
raise EWriteError.Create('QDF replacement of an EFS encrypted file is not supported');
// mitoita kuvaus ja lue siitä sitten vain DACL-osuus
if not GetFileSecurityW(PWideChar(Destination), DACL_SECURITY_INFORMATION,
@Security[0], SecuritySize, SecuritySize) then
raise EWriteError.Create('Unable to read QDF destination permissions');
if not QDFGetSecurityDescriptorControl(@Security[0], Control, Revision) or
((Control and SE_DACL_PRESENT) = 0) then
raise EWriteError.Create('QDF destination has no explicit DACL');
SecurityAttributes.lpSecurityDescriptor := @Security[0];
SecurityPointer := @SecurityAttributes; // välitetään CreateFileW:lle / CREATE_NEW
end;
Yksi regressiotestin yksityiskohta kannattaa pitää mielessä, jos kirjoitat vastaavan testin itse. Rajoitetun fixturen rakentamiseksi testi soveltaa vain omistajalle tarkoitettua DACL:ää ja sen on asetettava SE_DACL_PROTECTED kuvauksen ohjauskenttään nimenomaisesti; pelkkä suojatun lipun välittäminen SetFileSecurityW-kutsun SecurityInformation-argumentissa ei muuta suojaamatonta kuvausta suojatuksi. Jälkeinen väite on, että julkaistu tiedosto ilmoittaa edelleen suojatun bitin ja nimenomaisen, ei-null DACL:n, sekä erilliselle tulostepolulle että korjaukselle lähdetiedoston itsensä päälle
Mikä LastErrorCode kertoo, mikä epäonnistui?
RepairQDFFile palauttaa onnistuessa 1 ja epäonnistuessa 0, ja LastErrorCode kertoo, mikä vaihe kieltäytyi. Lähde, jota ei voi lukea, mukaan lukien sellainen, jota toinen prosessi pitää yksinomaisessa lukossa, ilmoittaa 401; luku on nyt kääritty niin, että syötteen aikana tullut poikkeus kuvautuu arvoksi 401 eikä vuoda kirjoitusvirheeksi. Virheellinen tai monitulkintainen QDF-rakenne, kuten samalle objektille kahdesti esiintyvä stream-merkki, ilmoittaa PDFLIB_ERROR_QDF_REPAIR-virheen, joka on 107, eikä kohteeseen ole koskettu, koska kirjoittajaa ei koskaan rakennettu. Kaikki korjauksen jälkeinen, väliaikaistiedoston luonnista huuhtelun ja uudelleennimeämisen kautta, ilmoittaa PDFLIB_ERROR_QDF_WRITE-virheen, joka on 305. Regressio käy läpi realistiset tapaukset: kohteen, jonka toinen kahva on avannut ilman poisto-jakoa, kirjoitussuojatun kohteen, puuttuvan kohdehakemiston sekä jokaisen kolmesta kirjoittajavaiheesta injektion kautta epäonnistuvana. Kaikissa niissä paluuarvo on 0, koodi on 305, eikä uutta tai osittaista kohdetta ole sen jälkeen olemassa. Yleinen tapa lukea koodi eikä pelkkää paluuarvoa on sama, jota kuvataan kirjaston hiljaisten vikojen diagnosointia käsittelevässä artikkelissa
var
Pdf: TPDFlib;
begin
Pdf := TPDFlib.Create;
try
// Paikan päällä tehtävä korjaus: sama polku on syöte ja tuloste
if Pdf.RepairQDFFile('edited.qdf.pdf', 'edited.qdf.pdf') = 1 then
Log('published; the previous bytes were replaced in one rename')
else
case Pdf.LastErrorCode of
401: Log('could not read the input; it was not modified');
107: Log('QDF structure rejected; the destination was never opened');
305: Log('write, flush or replace failed; the destination still holds its old bytes');
end;
finally
Pdf.Free;
end;
end;
Mihin takeet päättyvät
Kirjoittaja lupaa yhdenmukaisuuden niitä vikoja vastaan, jotka prosessi pystyy näkemään, ja on rehellinen niistä, joita se ei näe. Jos prosessi tapetaan väliaikaistiedoston luonnin ja uudelleennimeämisen välissä, finally-lohko ei koskaan aja ja hakemistoon jää .pdflib-qdf-<GUID>.tmp-tiedosto; kohde on silti ehjä, mikä on se ominaisuus jolla on väliä, mutta roskat ovat sinun siivottaviasi. Sähkökatko on lupauksen ulkopuolella samoin: data huuhdellaan ja uudelleennimeäminen on write-through, mikä on parasta mitä käyttäjätilan kirjasto voi pyytää, mutta kirjoittaja ei fsync-kutsua hakemistomerkintää eikä esitä kestävyysväitteitä sen päälle, mitä tiedostojärjestelmä tarjoaa. Toista kirjoittajaa, joka muokkaa kohdetta samanaikaisesti, ei havaita, koska DACL ja attribuutit luetaan ennen väliaikaistiedoston luomista eikä mikään tarkista niitä uudelleen uudelleennimeämishetkellä. Ja onnistunut uudelleennimeäminen luo uuden tiedostoidentiteetin, joten vaihtoehtoiset datavirrat ja tavalliset attribuutit kuten vanhan tiedoston arkisto- tai piilotettu-bitti eivät säily; vain DACL siirtyy tarkoituksella mukana
Kapeampi raja on se, mikä rajapinta ylipäätään käyttää tätä polkua. Vain RepairQDFFile kulkee TPDFQDFFileWriter-luokan kautta. SaveQDFToFile ja ConvertFileToQDF avaavat tulosteensa edelleen PLCreateFileStream(FileName, fmCreate) -kutsulla ja kirjoittavat QDF-muunnoksen suoraan siihen, samalla tavalla kuin streamiin lisääviä päivityksiä käsittelevässä artikkelissa kuvattu inkrementaalinen polku kirjoittaa mihin tahansa streamiin, jonka sille antaa. Nuo kaksi kutsua tuottavat uuden virheenjäljitysartefaktin asiakirjasta, joka on jo ladattu ja validoitu, joten jäsennysvirheen reikä ei koskaan koskenut niihin, mutta ne eivät peri myöskään uudelleennimeämiseen perustuvaa julkaisua. Älä lue tätä artikkelia niin, että ”jokainen QDF-vienti on atominen”. Kyse on yhdestä uloskäynnistä, siitä jonka syöte on epäluotettava, käsin muokattu tiedosto ja jonka tuloste on rutiininomaisesti sama polku, ja juuri tuo yhdistelmä ansaitsi sille ylimääräisen koneiston. Kaiken tämän todistava vikainjektio on halpaa, koska kirjoittajan kolme vaihetta, WriteData, Flush ja Publish, ovat virtual. Testin aliluokka ylikirjoittaa yhden niistä nostamaan virheen sen jälkeen kun varsinainen työ on alkanut, kutsuu Save-metodia korjatulle streamille ja varmistaa, että poikkeus etenee, että lähteen ja kohteen tavut ovat muuttumattomia eivätkä mitkään väliaikaistiedostot jää jäljelle. Mitään globaalia tiedostorajapintaa ei koukuteta, mihinkään oikeaan käyttäjän tiedostoon ei kosketa, ja kolme vaihetta vastaavat yksi yhteen niitä kolmea tapaa, joilla julkaisu voi tuotannossa epäonnistua: levy täyttyy, huuhtelu hylätään tai uudelleennimeäminen evätään, koska joku muu pitää kohdetta hallussaan
RepairQDFFile-rajapinta, sen atominen julkaisukirjoittaja ja muu QDF-virheenjäljitystyönkulku ovat osa PDF Library for Delphiä, yhdessä ristiviitteiden palautuksen, inkrementaalisten päivitysten ja salausominaisuuksien kanssa, joita käsitellään muualla tässä blogissa