mirror of
https://github.com/mudler/LocalAI.git
synced 2026-07-30 18:09:05 -04:00
Backend installation extracts the OCI image tar via containerd's archive.Apply, which calls os.Symlink directly. On filesystems that do not support symlinks (notably CIFS/SMB mounts, commonly used to back the /backends volume) the syscall fails with "operation not supported" and the whole install aborts, leaving an empty backend directory. The CUDA llama.cpp image trips this on the libcublas.so -> libcublas.so.12.x symlink. When archive.Apply fails with a link-unsupported error, reset the staging directory and re-extract with a pure-Go walker that still attempts real symlinks/hardlinks first and degrades to copying the link target's contents in place when the filesystem rejects them. mutate.Extract already flattened the layers, so the tar carries no whiteouts to interpret. Link copies are deferred to a second pass so forward references resolve. Signed-off-by: Ettore Di Giacinto <mudler@localai.io> Assisted-by: Claude:opus-4.8 [Claude Code]
119 lines
3.5 KiB
Go
119 lines
3.5 KiB
Go
package oci
|
|
|
|
import (
|
|
"archive/tar"
|
|
"bytes"
|
|
"os"
|
|
"path/filepath"
|
|
"syscall"
|
|
|
|
. "github.com/onsi/ginkgo/v2"
|
|
. "github.com/onsi/gomega"
|
|
)
|
|
|
|
// buildTar assembles an in-memory tar carrying a directory, a regular file and
|
|
// a relative symlink pointing at that file, mirroring the layout of a backend
|
|
// image (e.g. libcublas.so -> libcublas.so.12).
|
|
func buildTar() []byte {
|
|
var buf bytes.Buffer
|
|
tw := tar.NewWriter(&buf)
|
|
|
|
Expect(tw.WriteHeader(&tar.Header{
|
|
Name: "lib/",
|
|
Typeflag: tar.TypeDir,
|
|
Mode: 0755,
|
|
})).To(Succeed())
|
|
|
|
content := []byte("real library bytes")
|
|
Expect(tw.WriteHeader(&tar.Header{
|
|
Name: "lib/libcublas.so.12",
|
|
Typeflag: tar.TypeReg,
|
|
Mode: 0644,
|
|
Size: int64(len(content)),
|
|
})).To(Succeed())
|
|
_, err := tw.Write(content)
|
|
Expect(err).NotTo(HaveOccurred())
|
|
|
|
Expect(tw.WriteHeader(&tar.Header{
|
|
Name: "lib/libcublas.so",
|
|
Typeflag: tar.TypeSymlink,
|
|
Linkname: "libcublas.so.12",
|
|
Mode: 0777,
|
|
})).To(Succeed())
|
|
|
|
Expect(tw.Close()).To(Succeed())
|
|
return buf.Bytes()
|
|
}
|
|
|
|
var _ = Describe("Tar extraction fallback for link-less filesystems", func() {
|
|
Describe("isLinkUnsupportedError", func() {
|
|
It("recognises filesystem link-unsupported errors", func() {
|
|
Expect(isLinkUnsupportedError(syscall.ENOTSUP)).To(BeTrue())
|
|
Expect(isLinkUnsupportedError(syscall.EOPNOTSUPP)).To(BeTrue())
|
|
Expect(isLinkUnsupportedError(syscall.EPERM)).To(BeTrue())
|
|
Expect(isLinkUnsupportedError(&os.LinkError{
|
|
Op: "symlink",
|
|
Old: "libcublas.so.12",
|
|
New: "/backends/lib/libcublas.so",
|
|
Err: syscall.ENOTSUP,
|
|
})).To(BeTrue())
|
|
})
|
|
|
|
It("does not misclassify unrelated errors", func() {
|
|
Expect(isLinkUnsupportedError(os.ErrNotExist)).To(BeFalse())
|
|
Expect(isLinkUnsupportedError(syscall.ENOSPC)).To(BeFalse())
|
|
})
|
|
})
|
|
|
|
Describe("safeJoin", func() {
|
|
It("keeps entries inside the root", func() {
|
|
root := "/tmp/extract-root"
|
|
p, err := safeJoin(root, "lib/libcublas.so")
|
|
Expect(err).NotTo(HaveOccurred())
|
|
Expect(p).To(Equal(filepath.Join(root, "lib/libcublas.so")))
|
|
})
|
|
|
|
It("rejects path traversal entries", func() {
|
|
_, err := safeJoin("/tmp/extract-root", "../../etc/passwd")
|
|
Expect(err).To(HaveOccurred())
|
|
})
|
|
})
|
|
|
|
Describe("extractTarCopyingLinks", func() {
|
|
It("preserves symlinks when the filesystem supports them", func() {
|
|
dir := GinkgoT().TempDir()
|
|
Expect(extractTarCopyingLinks(bytes.NewReader(buildTar()), dir)).To(Succeed())
|
|
|
|
linkPath := filepath.Join(dir, "lib", "libcublas.so")
|
|
fi, err := os.Lstat(linkPath)
|
|
Expect(err).NotTo(HaveOccurred())
|
|
Expect(fi.Mode() & os.ModeSymlink).NotTo(BeZero())
|
|
|
|
data, err := os.ReadFile(linkPath)
|
|
Expect(err).NotTo(HaveOccurred())
|
|
Expect(string(data)).To(Equal("real library bytes"))
|
|
})
|
|
|
|
It("copies the target when symlink creation is unsupported", func() {
|
|
// Simulate a CIFS/SMB mount: symlink() reports ENOTSUP.
|
|
origSymlink := symlink
|
|
symlink = func(string, string) error { return syscall.ENOTSUP }
|
|
DeferCleanup(func() { symlink = origSymlink })
|
|
|
|
dir := GinkgoT().TempDir()
|
|
Expect(extractTarCopyingLinks(bytes.NewReader(buildTar()), dir)).To(Succeed())
|
|
|
|
linkPath := filepath.Join(dir, "lib", "libcublas.so")
|
|
fi, err := os.Lstat(linkPath)
|
|
Expect(err).NotTo(HaveOccurred())
|
|
// The entry must now be a real, regular file (a copy), not a symlink.
|
|
Expect(fi.Mode() & os.ModeSymlink).To(BeZero())
|
|
Expect(fi.Mode().IsRegular()).To(BeTrue())
|
|
|
|
data, err := os.ReadFile(linkPath)
|
|
Expect(err).NotTo(HaveOccurred())
|
|
Expect(string(data)).To(Equal("real library bytes"))
|
|
})
|
|
})
|
|
})
|