Skip to content

Commit f01f936

Browse files
ForestEckhardtryanmoran
authored andcommitted
Adds logic to prevent symlink cycles from causing deadlock
1 parent 563d46d commit f01f936

File tree

4 files changed

+116
-26
lines changed

4 files changed

+116
-26
lines changed

vacation/tar_archive.go

+22-9
Original file line numberDiff line numberDiff line change
@@ -130,11 +130,11 @@ func (ta TarArchive) Decompress(destination string) error {
130130
symlinkMap[filepath.Clean(h.path)] = h.linkname
131131
}
132132

133-
// Loop over map until it is empty this is potentially O(infinity) if there
134-
// is a cyclical link but I like to live on the edge
135-
for len(symlinkMap) > 0 {
136-
// Name the outer loop as an escape hatch
137-
Builder:
133+
// Iterate over the symlink map for every link that is found this ensures
134+
// that all symlinks that can be created will be created and any that are
135+
// left over are cyclically dependent
136+
maxIterations := len(symlinkMap)
137+
for i := 0; i < maxIterations; i++ {
138138
for path, linkname := range symlinkMap {
139139
// Check to see if the linkname lies on the path of another symlink in
140140
// the table or if it is another symlink in the table
@@ -147,11 +147,18 @@ func (ta TarArchive) Decompress(destination string) error {
147147
//
148148
// If there is a match either of the symlink or it is on the path then
149149
// skip the creation of this symlink for now
150-
sln := strings.Split(linkname, "/")
151-
for i := 0; i < len(sln); i++ {
152-
if _, ok := symlinkMap[linknameFullPath(path, filepath.Join(sln[:i+1]...))]; ok {
153-
continue Builder
150+
shouldSkipLink := func() bool {
151+
sln := strings.Split(linkname, "/")
152+
for j := 0; j < len(sln); j++ {
153+
if _, ok := symlinkMap[linknameFullPath(path, filepath.Join(sln[:j+1]...))]; ok {
154+
return true
155+
}
154156
}
157+
return false
158+
}
159+
160+
if shouldSkipLink() {
161+
continue
155162
}
156163

157164
// If the linkname is not an existing link in the symlink table then we
@@ -175,6 +182,12 @@ func (ta TarArchive) Decompress(destination string) error {
175182
}
176183
}
177184

185+
// Check to see if there are any symlinks left in the map which would
186+
// indicate a cyclical dependency
187+
if len(symlinkMap) > 0 {
188+
return fmt.Errorf("failed: max iterations reached: this symlink graph contains a cycle")
189+
}
190+
178191
return nil
179192
}
180193

vacation/tar_archive_test.go

+28
Original file line numberDiff line numberDiff line change
@@ -289,5 +289,33 @@ func testTarArchive(t *testing.T, context spec.G, it spec.S) {
289289
})
290290
})
291291
})
292+
293+
context("when there is a symlink cycle", func() {
294+
var cyclicalSymlinkTar vacation.TarArchive
295+
296+
it.Before(func() {
297+
var err error
298+
299+
buffer := bytes.NewBuffer(nil)
300+
tw := tar.NewWriter(buffer)
301+
302+
Expect(tw.WriteHeader(&tar.Header{Name: "a-symlink", Mode: 0755, Size: int64(0), Typeflag: tar.TypeSymlink, Linkname: "b-symlink"})).To(Succeed())
303+
_, err = tw.Write([]byte{})
304+
Expect(err).NotTo(HaveOccurred())
305+
306+
Expect(tw.WriteHeader(&tar.Header{Name: "b-symlink", Mode: 0755, Size: int64(0), Typeflag: tar.TypeSymlink, Linkname: "a-symlink"})).To(Succeed())
307+
_, err = tw.Write([]byte{})
308+
Expect(err).NotTo(HaveOccurred())
309+
310+
Expect(tw.Close()).To(Succeed())
311+
312+
cyclicalSymlinkTar = vacation.NewTarArchive(bytes.NewReader(buffer.Bytes()))
313+
})
314+
315+
it("returns an error", func() {
316+
err := cyclicalSymlinkTar.Decompress(tempDir)
317+
Expect(err).To(MatchError(ContainSubstring("failed: max iterations reached: this symlink graph contains a cycle")))
318+
})
319+
})
292320
})
293321
}

vacation/zip_archive.go

+22-9
Original file line numberDiff line numberDiff line change
@@ -137,11 +137,11 @@ func (z ZipArchive) Decompress(destination string) error {
137137
symlinkMap[filepath.Clean(h.path)] = h.linkname
138138
}
139139

140-
// Loop over map until it is empty this is potentially O(infinity) if there
141-
// is a cyclical link but I like to live on the edge
142-
for len(symlinkMap) > 0 {
143-
// Name the outer loop as an escape hatch
144-
Builder:
140+
// Iterate over the symlink map for every link that is found this ensures
141+
// that all symlinks that can be created will be created and any that are
142+
// left over are cyclically dependent
143+
maxIterations := len(symlinkMap)
144+
for i := 0; i < maxIterations; i++ {
145145
for path, linkname := range symlinkMap {
146146
// Check to see if the linkname lies on the path of another symlink in
147147
// the table or if it is another symlink in the table
@@ -154,11 +154,18 @@ func (z ZipArchive) Decompress(destination string) error {
154154
//
155155
// If there is a match either of the symlink or it is on the path then
156156
// skip the creation of this symlink for now
157-
sln := strings.Split(linkname, "/")
158-
for i := 0; i < len(sln); i++ {
159-
if _, ok := symlinkMap[linknameFullPath(path, filepath.Join(sln[:i+1]...))]; ok {
160-
continue Builder
157+
shouldSkipLink := func() bool {
158+
sln := strings.Split(linkname, "/")
159+
for j := 0; j < len(sln); j++ {
160+
if _, ok := symlinkMap[linknameFullPath(path, filepath.Join(sln[:j+1]...))]; ok {
161+
return true
162+
}
161163
}
164+
return false
165+
}
166+
167+
if shouldSkipLink() {
168+
continue
162169
}
163170

164171
// If the linkname is not an existing link in the symlink table then we
@@ -182,6 +189,12 @@ func (z ZipArchive) Decompress(destination string) error {
182189
}
183190
}
184191

192+
// Check to see if there are any symlinks left in the map which would
193+
// indicate a cyclical dependency
194+
if len(symlinkMap) > 0 {
195+
return fmt.Errorf("failed: max iterations reached: this symlink graph contains a cycle")
196+
}
197+
185198
return nil
186199
}
187200

vacation/zip_archive_test.go

+44-8
Original file line numberDiff line numberDiff line change
@@ -244,6 +244,33 @@ func testZipArchive(t *testing.T, context spec.G, it spec.S) {
244244
})
245245
})
246246

247+
context("when it fails to unzip a file", func() {
248+
var buffer *bytes.Buffer
249+
it.Before(func() {
250+
var err error
251+
buffer = bytes.NewBuffer(nil)
252+
zw := zip.NewWriter(buffer)
253+
254+
_, err = zw.Create("some-file")
255+
Expect(err).NotTo(HaveOccurred())
256+
257+
Expect(zw.Close()).To(Succeed())
258+
259+
Expect(os.Chmod(tempDir, 0000)).To(Succeed())
260+
})
261+
262+
it.After(func() {
263+
Expect(os.Chmod(tempDir, os.ModePerm)).To(Succeed())
264+
})
265+
266+
it("returns an error", func() {
267+
readyArchive := vacation.NewZipArchive(buffer)
268+
269+
err := readyArchive.Decompress(tempDir)
270+
Expect(err).To(MatchError(ContainSubstring("failed to unzip file")))
271+
})
272+
})
273+
247274
context("when it tries to symlink to a file that does not exist", func() {
248275
var buffer *bytes.Buffer
249276
it.Before(func() {
@@ -305,30 +332,39 @@ func testZipArchive(t *testing.T, context spec.G, it spec.S) {
305332
})
306333
})
307334

308-
context("when it fails to unzip a file", func() {
335+
context("when there is a symlink cycle", func() {
309336
var buffer *bytes.Buffer
310337
it.Before(func() {
311338
var err error
312339
buffer = bytes.NewBuffer(nil)
313340
zw := zip.NewWriter(buffer)
314341

315-
_, err = zw.Create("some-file")
342+
header := &zip.FileHeader{Name: "a-symlink"}
343+
header.SetMode(0755 | os.ModeSymlink)
344+
345+
aSymlink, err := zw.CreateHeader(header)
316346
Expect(err).NotTo(HaveOccurred())
317347

318-
Expect(zw.Close()).To(Succeed())
348+
_, err = aSymlink.Write([]byte(filepath.Join("b-symlink")))
349+
Expect(err).NotTo(HaveOccurred())
319350

320-
Expect(os.Chmod(tempDir, 0000)).To(Succeed())
321-
})
351+
header = &zip.FileHeader{Name: "b-symlink"}
352+
header.SetMode(0755 | os.ModeSymlink)
322353

323-
it.After(func() {
324-
Expect(os.Chmod(tempDir, os.ModePerm)).To(Succeed())
354+
bSymlink, err := zw.CreateHeader(header)
355+
Expect(err).NotTo(HaveOccurred())
356+
357+
_, err = bSymlink.Write([]byte(filepath.Join("a-symlink")))
358+
Expect(err).NotTo(HaveOccurred())
359+
360+
Expect(zw.Close()).To(Succeed())
325361
})
326362

327363
it("returns an error", func() {
328364
readyArchive := vacation.NewZipArchive(buffer)
329365

330366
err := readyArchive.Decompress(tempDir)
331-
Expect(err).To(MatchError(ContainSubstring("failed to unzip file")))
367+
Expect(err).To(MatchError("failed: max iterations reached: this symlink graph contains a cycle"))
332368
})
333369
})
334370
})

0 commit comments

Comments
 (0)