incusd/instance: Confine OCI network writes to instance root

Also rejects line breaks in the oci.dns.domain and oci.dns.search values
which are written to the generated resolv.conf.

This addresses GHSA-7fj9-65v4-rp7h (CVE pending)

Signed-off-by: Stéphane Graber <stgraber@stgraber.org>
This commit is contained in:
Stéphane Graber 2026-07-24 11:42:32 -04:00
parent 4d64535046
commit e46720cb6d
No known key found for this signature in database
GPG Key ID: C638974D64792D67
2 changed files with 24 additions and 7 deletions

View File

@ -27,6 +27,15 @@ func isNvidiaConfigValue(value string) error {
return nil
}
// isResolvConfValue rejects line breaks that would allow injecting arbitrary lines into the generated resolv.conf.
func isResolvConfValue(value string) error {
if strings.ContainsAny(value, "\r\n") {
return errors.New("Value cannot contain line breaks")
}
return nil
}
// ConfigVolatilePrefix indicates the prefix used for volatile config keys.
const ConfigVolatilePrefix = "volatile."
@ -836,7 +845,7 @@ var InstanceConfigKeysContainer = map[string]func(value string) error{
// liveupdate: no
// condition: OCI container
// shortdesc: DNS domain
"oci.dns.domain": validate.IsAny,
"oci.dns.domain": validate.Optional(isResolvConfValue),
// gendoc:generate(entity=instance, group=oci, key=oci.dns.search)
// Comma-separated list of search domains for the initial `resolv.conf`.
@ -845,7 +854,7 @@ var InstanceConfigKeysContainer = map[string]func(value string) error{
// liveupdate: no
// condition: OCI container
// shortdesc: DNS search domains
"oci.dns.search": validate.IsAny,
"oci.dns.search": validate.Optional(validate.IsListOf(isResolvConfValue)),
// Caller is responsible for full validation of any raw.* value.

View File

@ -2617,17 +2617,25 @@ func (d *lxc) startCommon() (string, []func() error, error) {
}
// Configure network handling.
err = os.MkdirAll(filepath.Join(d.Path(), "network"), 0o711)
// Confine all writes to the instance directory to avoid following image-planted symlinks.
instRoot, err := os.OpenRoot(d.Path())
if err != nil {
return "", nil, err
}
defer logger.WarnOnError(instRoot.Close, "Failed to close instance root")
err = instRoot.Mkdir("network", 0o711)
if err != nil && !errors.Is(err, fs.ErrExist) {
return "", nil, err
}
err = os.MkdirAll(filepath.Join(d.RootfsPath(), "etc"), 0o755)
if err != nil && !os.IsExist(err) {
return "", nil, err
}
err = os.WriteFile(filepath.Join(d.Path(), "network", "hosts"), fmt.Appendf(nil, `127.0.0.1 localhost
err = instRoot.WriteFile("network/hosts", fmt.Appendf(nil, `127.0.0.1 localhost
127.0.1.1 %s
::1 localhost ip6-localhost ip6-loopback
@ -2645,7 +2653,7 @@ ff02::2 ip6-allrouters
return "", nil, err
}
err = os.WriteFile(filepath.Join(d.Path(), "network", "hostname"), fmt.Appendf(nil, "%s\n", d.name), 0o644)
err = instRoot.WriteFile("network/hostname", fmt.Appendf(nil, "%s\n", d.name), 0o644)
if err != nil {
return "", nil, err
}
@ -2669,7 +2677,7 @@ ff02::2 ip6-allrouters
fmt.Fprintf(&resolvConf, "domain %s\n", d.expandedConfig["oci.dns.domain"])
}
err = os.WriteFile(filepath.Join(d.Path(), "network", "resolv.conf"), []byte(resolvConf.String()), 0o644)
err = instRoot.WriteFile("network/resolv.conf", []byte(resolvConf.String()), 0o644)
if err != nil {
return "", nil, err
}
@ -2699,7 +2707,7 @@ ff02::2 ip6-allrouters
return "", nil, err
}
err = os.WriteFile(filepath.Join(d.Path(), "network", "interfaces.json"), ifacesData, 0o644)
err = instRoot.WriteFile("network/interfaces.json", ifacesData, 0o644)
if err != nil {
return "", nil, err
}