Skip to content

Commit d22d415

Browse files
committed
test(utils): cover hardlink overwrite and in-root hardlinks
The existing hardlink test names a link target two levels above the extraction root, so its final assertion checked a path the link never resolved to and could not fail. Point the target one level up instead, at the path that assertion already names. Add two cases. The first uses a .tar.gz, where ExtractArchive binds a Tar config with OverwriteExisting set, and follows the link entry with a regular entry of the same name. Before the fix that pair linked to a file outside the root and then truncated it through the link, which the plain .tar case does not reach. The second extracts a hardlink whose target is an earlier member of the same archive, covering the claim that ordinary archives are unaffected. Assisted-by: Claude:claude-opus-5
1 parent 3ff1cf2 commit d22d415

1 file changed

Lines changed: 127 additions & 1 deletion

File tree

pkg/utils/untar_test.go

Lines changed: 127 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package utils_test
33
import (
44
"archive/tar"
55
"archive/zip"
6+
"compress/gzip"
67
"os"
78
"path/filepath"
89

@@ -60,12 +61,45 @@ var _ = Describe("utils/archive tests", func() {
6061
Expect(filepath.Join(tmpDir, "escaped.txt")).ToNot(BeAnExistingFile())
6162
})
6263

64+
It("rejects tar hardlinks that overwrite a file outside the destination", func() {
65+
tmpDir := GinkgoT().TempDir()
66+
archivePath := filepath.Join(tmpDir, "model.tar.gz")
67+
extractPath := filepath.Join(tmpDir, "models")
68+
outsidePath := filepath.Join(tmpDir, "outside.txt")
69+
70+
Expect(os.WriteFile(outsidePath, []byte("original"), 0o600)).To(Succeed())
71+
Expect(writeTarGzArchiveWithHardlinkedFile(archivePath, "payload.bin", "../outside.txt", "overwritten")).To(Succeed())
72+
73+
err := ExtractArchive(archivePath, extractPath)
74+
75+
Expect(err).To(HaveOccurred())
76+
Expect(err.Error()).To(ContainSubstring("unsafe path"))
77+
78+
contents, readErr := os.ReadFile(outsidePath)
79+
Expect(readErr).ToNot(HaveOccurred())
80+
Expect(string(contents)).To(Equal("original"))
81+
})
82+
83+
It("extracts tar hardlinks that stay inside the destination", func() {
84+
tmpDir := GinkgoT().TempDir()
85+
archivePath := filepath.Join(tmpDir, "model.tar.gz")
86+
extractPath := filepath.Join(tmpDir, "models")
87+
88+
Expect(writeTarGzArchiveWithInternalHardlink(archivePath, "model.bin", "alias.bin", "weights")).To(Succeed())
89+
90+
Expect(ExtractArchive(archivePath, extractPath)).To(Succeed())
91+
92+
extracted, err := os.ReadFile(filepath.Join(extractPath, "alias.bin"))
93+
Expect(err).ToNot(HaveOccurred())
94+
Expect(string(extracted)).To(Equal("weights"))
95+
})
96+
6397
It("rejects tar hardlinks that point outside the destination", func() {
6498
tmpDir := GinkgoT().TempDir()
6599
archivePath := filepath.Join(tmpDir, "model.tar")
66100
extractPath := filepath.Join(tmpDir, "models")
67101

68-
Expect(writeTarArchiveWithHardlink(archivePath, "payload.bin", "../../escaped.txt")).To(Succeed())
102+
Expect(writeTarArchiveWithHardlink(archivePath, "payload.bin", "../escaped.txt")).To(Succeed())
69103

70104
err := ExtractArchive(archivePath, extractPath)
71105

@@ -166,3 +200,95 @@ func writeTarArchiveWithHardlink(path, name, linkname string) (err error) {
166200
Mode: 0o600,
167201
})
168202
}
203+
204+
func writeTarGzArchiveWithHardlinkedFile(path, name, linkname, contents string) (err error) {
205+
out, err := os.Create(path)
206+
if err != nil {
207+
return err
208+
}
209+
defer func() {
210+
if closeErr := out.Close(); err == nil {
211+
err = closeErr
212+
}
213+
}()
214+
215+
compressor := gzip.NewWriter(out)
216+
defer func() {
217+
if closeErr := compressor.Close(); err == nil {
218+
err = closeErr
219+
}
220+
}()
221+
222+
writer := tar.NewWriter(compressor)
223+
defer func() {
224+
if closeErr := writer.Close(); err == nil {
225+
err = closeErr
226+
}
227+
}()
228+
229+
if err := writer.WriteHeader(&tar.Header{
230+
Name: name,
231+
Linkname: linkname,
232+
Typeflag: tar.TypeLink,
233+
Mode: 0o600,
234+
}); err != nil {
235+
return err
236+
}
237+
238+
data := []byte(contents)
239+
if err := writer.WriteHeader(&tar.Header{
240+
Name: name,
241+
Mode: 0o600,
242+
Size: int64(len(data)),
243+
}); err != nil {
244+
return err
245+
}
246+
_, err = writer.Write(data)
247+
248+
return err
249+
}
250+
251+
func writeTarGzArchiveWithInternalHardlink(path, targetName, linkName, contents string) (err error) {
252+
out, err := os.Create(path)
253+
if err != nil {
254+
return err
255+
}
256+
defer func() {
257+
if closeErr := out.Close(); err == nil {
258+
err = closeErr
259+
}
260+
}()
261+
262+
compressor := gzip.NewWriter(out)
263+
defer func() {
264+
if closeErr := compressor.Close(); err == nil {
265+
err = closeErr
266+
}
267+
}()
268+
269+
writer := tar.NewWriter(compressor)
270+
defer func() {
271+
if closeErr := writer.Close(); err == nil {
272+
err = closeErr
273+
}
274+
}()
275+
276+
data := []byte(contents)
277+
if err := writer.WriteHeader(&tar.Header{
278+
Name: targetName,
279+
Mode: 0o600,
280+
Size: int64(len(data)),
281+
}); err != nil {
282+
return err
283+
}
284+
if _, err := writer.Write(data); err != nil {
285+
return err
286+
}
287+
288+
return writer.WriteHeader(&tar.Header{
289+
Name: linkName,
290+
Linkname: targetName,
291+
Typeflag: tar.TypeLink,
292+
Mode: 0o600,
293+
})
294+
}

0 commit comments

Comments
 (0)