From fd5b328e86bf14107f1a73325dc3bb4e12c5980f Mon Sep 17 00:00:00 2001
From: Aleksa Sarai <asarai@suse.de>
Date: Wed, 11 Oct 2023 21:19:12 +1100
Subject: [PATCH 6/6] SLE12: revert "apparmor: remove version-conditionals from
 template"

This reverts the following commits:

 * 7008a514493a ("profiles/apparmor: remove version-conditional constraints (< 2.8.96)")
 * 2e19a4d56bf2 ("contrib/apparmor: remove version-conditionals (< 2.9) from template")
 * d169a5730649 ("contrib/apparmor: remove remaining version-conditionals (< 2.9) from template")
 * ecaab085db4b ("profiles/apparmor: remove use of aaparser.GetVersion()")
 * e3e715666f95 ("pkg/aaparser: deprecate GetVersion, as it's no longer used")

These version conditionals are still required on SLE 12, where our
apparmor_parser version is quite old.

Signed-off-by: Aleksa Sarai <cyphar@cyphar.com>
---
 contrib/apparmor/main.go      | 16 ++++++-
 contrib/apparmor/template.go  | 16 +++++++
 pkg/aaparser/aaparser.go      | 86 +++++++++++++++++++++++++++++++++++
 profiles/apparmor/apparmor.go | 16 ++++++-
 profiles/apparmor/template.go |  4 ++
 5 files changed, 134 insertions(+), 4 deletions(-)
 create mode 100644 pkg/aaparser/aaparser.go

diff --git a/contrib/apparmor/main.go b/contrib/apparmor/main.go
index 899d8378edae..93f98cbd20e5 100644
--- a/contrib/apparmor/main.go
+++ b/contrib/apparmor/main.go
@@ -6,9 +6,13 @@ import (
 	"os"
 	"path"
 	"text/template"
+
+	"github.com/docker/docker/pkg/aaparser"
 )
 
-type profileData struct{}
+type profileData struct {
+	Version int
+}
 
 func main() {
 	if len(os.Args) < 2 {
@@ -18,6 +22,15 @@ func main() {
 	// parse the arg
 	apparmorProfilePath := os.Args[1]
 
+	version, err := aaparser.GetVersion()
+	if err != nil {
+		log.Fatal(err)
+	}
+	data := profileData{
+		Version: version,
+	}
+	fmt.Printf("apparmor_parser is of version %+v\n", data)
+
 	// parse the template
 	compiled, err := template.New("apparmor_profile").Parse(dockerProfileTemplate)
 	if err != nil {
@@ -35,7 +48,6 @@ func main() {
 	}
 	defer f.Close()
 
-	data := profileData{}
 	if err := compiled.Execute(f, data); err != nil {
 		log.Fatalf("executing template failed: %v", err)
 	}
diff --git a/contrib/apparmor/template.go b/contrib/apparmor/template.go
index 58afcbe845ee..e6d0b6d37c58 100644
--- a/contrib/apparmor/template.go
+++ b/contrib/apparmor/template.go
@@ -20,9 +20,11 @@ profile /usr/bin/docker (attach_disconnected, complain) {
 
   umount,
   pivot_root,
+{{if ge .Version 209000}}
   signal (receive) peer=@{profile_name},
   signal (receive) peer=unconfined,
   signal (send),
+{{end}}
   network,
   capability,
   owner /** rw,
@@ -45,10 +47,12 @@ profile /usr/bin/docker (attach_disconnected, complain) {
   /etc/ld.so.cache r,
   /etc/passwd r,
 
+{{if ge .Version 209000}}
   ptrace peer=@{profile_name},
   ptrace (read) peer=docker-default,
   deny ptrace (trace) peer=docker-default,
   deny ptrace peer=/usr/bin/docker///bin/ps,
+{{end}}
 
   /usr/lib/** rm,
   /lib/** rm,
@@ -69,9 +73,11 @@ profile /usr/bin/docker (attach_disconnected, complain) {
   /sbin/zfs rCx,
   /sbin/apparmor_parser rCx,
 
+{{if ge .Version 209000}}
   # Transitions
   change_profile -> docker-*,
   change_profile -> unconfined,
+{{end}}
 
   profile /bin/cat (complain) {
     /etc/ld.so.cache r,
@@ -93,8 +99,10 @@ profile /usr/bin/docker (attach_disconnected, complain) {
     /dev/null rw,
     /bin/ps mr,
 
+{{if ge .Version 209000}}
     # We don't need ptrace so we'll deny and ignore the error.
     deny ptrace (read, trace),
+{{end}}
 
     # Quiet dac_override denials
     deny capability dac_override,
@@ -112,11 +120,15 @@ profile /usr/bin/docker (attach_disconnected, complain) {
     /proc/tty/drivers r,
   }
   profile /sbin/iptables (complain) {
+{{if ge .Version 209000}}
     signal (receive) peer=/usr/bin/docker,
+{{end}}
     capability net_admin,
   }
   profile /sbin/auplink flags=(attach_disconnected, complain) {
+{{if ge .Version 209000}}
     signal (receive) peer=/usr/bin/docker,
+{{end}}
     capability sys_admin,
     capability dac_override,
 
@@ -135,7 +147,9 @@ profile /usr/bin/docker (attach_disconnected, complain) {
     /proc/[0-9]*/mounts rw,
   }
   profile /sbin/modprobe /bin/kmod (complain) {
+{{if ge .Version 209000}}
     signal (receive) peer=/usr/bin/docker,
+{{end}}
     capability sys_module,
     /etc/ld.so.cache r,
     /lib/** rm,
@@ -149,7 +163,9 @@ profile /usr/bin/docker (attach_disconnected, complain) {
   }
   # xz works via pipes, so we do not need access to the filesystem.
   profile /usr/bin/xz (complain) {
+{{if ge .Version 209000}}
     signal (receive) peer=/usr/bin/docker,
+{{end}}
     /etc/ld.so.cache r,
     /lib/** rm,
     /usr/bin/xz rm,
diff --git a/pkg/aaparser/aaparser.go b/pkg/aaparser/aaparser.go
new file mode 100644
index 000000000000..89b48b2dba58
--- /dev/null
+++ b/pkg/aaparser/aaparser.go
@@ -0,0 +1,86 @@
+// Package aaparser is a convenience package interacting with `apparmor_parser`.
+package aaparser // import "github.com/docker/docker/pkg/aaparser"
+
+import (
+	"fmt"
+	"os/exec"
+	"strconv"
+	"strings"
+)
+
+const (
+	binary = "apparmor_parser"
+)
+
+// GetVersion returns the major and minor version of apparmor_parser.
+func GetVersion() (int, error) {
+	output, err := cmd("", "--version")
+	if err != nil {
+		return -1, err
+	}
+
+	return parseVersion(output)
+}
+
+// cmd runs `apparmor_parser` with the passed arguments.
+func cmd(dir string, arg ...string) (string, error) {
+	c := exec.Command(binary, arg...)
+	c.Dir = dir
+
+	output, err := c.CombinedOutput()
+	if err != nil {
+		return "", fmt.Errorf("running `%s %s` failed with output: %s\nerror: %v", c.Path, strings.Join(c.Args, " "), output, err)
+	}
+
+	return string(output), nil
+}
+
+// parseVersion takes the output from `apparmor_parser --version` and returns
+// a representation of the {major, minor, patch} version as a single number of
+// the form MMmmPPP {major, minor, patch}.
+func parseVersion(output string) (int, error) {
+	// output is in the form of the following:
+	// AppArmor parser version 2.9.1
+	// Copyright (C) 1999-2008 Novell Inc.
+	// Copyright 2009-2012 Canonical Ltd.
+
+	lines := strings.SplitN(output, "\n", 2)
+	words := strings.Split(lines[0], " ")
+	version := words[len(words)-1]
+
+	// trim "-beta1" suffix from version="3.0.0-beta1" if exists
+	version = strings.SplitN(version, "-", 2)[0]
+	// also trim "~..." suffix used historically (https://gitlab.com/apparmor/apparmor/-/commit/bca67d3d27d219d11ce8c9cc70612bd637f88c10)
+	version = strings.SplitN(version, "~", 2)[0]
+
+	// split by major minor version
+	v := strings.Split(version, ".")
+	if len(v) == 0 || len(v) > 3 {
+		return -1, fmt.Errorf("parsing version failed for output: `%s`", output)
+	}
+
+	// Default the versions to 0.
+	var majorVersion, minorVersion, patchLevel int
+
+	majorVersion, err := strconv.Atoi(v[0])
+	if err != nil {
+		return -1, err
+	}
+
+	if len(v) > 1 {
+		minorVersion, err = strconv.Atoi(v[1])
+		if err != nil {
+			return -1, err
+		}
+	}
+	if len(v) > 2 {
+		patchLevel, err = strconv.Atoi(v[2])
+		if err != nil {
+			return -1, err
+		}
+	}
+
+	// major*10^5 + minor*10^3 + patch*10^0
+	numericVersion := majorVersion*1e5 + minorVersion*1e3 + patchLevel
+	return numericVersion, nil
+}
diff --git a/profiles/apparmor/apparmor.go b/profiles/apparmor/apparmor.go
index 445eed64e979..871b1f7d63c2 100644
--- a/profiles/apparmor/apparmor.go
+++ b/profiles/apparmor/apparmor.go
@@ -11,10 +11,14 @@ import (
 	"path"
 	"strings"
 	"text/template"
+
+	"github.com/docker/docker/pkg/aaparser"
 )
 
-// profileDirectory is the file store for apparmor profiles and macros.
-const profileDirectory = "/etc/apparmor.d"
+var (
+	// profileDirectory is the file store for apparmor profiles and macros.
+	profileDirectory = "/etc/apparmor.d"
+)
 
 // profileData holds information about the given profile for generation.
 type profileData struct {
@@ -26,6 +30,8 @@ type profileData struct {
 	Imports []string
 	// InnerImports defines the apparmor functions to import in the profile.
 	InnerImports []string
+	// Version is the {major, minor, patch} version of apparmor_parser as a single number.
+	Version int
 }
 
 // generateDefault creates an apparmor profile from ProfileData.
@@ -45,6 +51,12 @@ func (p *profileData) generateDefault(out io.Writer) error {
 		p.InnerImports = append(p.InnerImports, "#include <abstractions/base>")
 	}
 
+	ver, err := aaparser.GetVersion()
+	if err != nil {
+		return err
+	}
+	p.Version = ver
+
 	return compiled.Execute(out, p)
 }
 
diff --git a/profiles/apparmor/template.go b/profiles/apparmor/template.go
index 35c75300f8f0..b7a0299af2b8 100644
--- a/profiles/apparmor/template.go
+++ b/profiles/apparmor/template.go
@@ -23,6 +23,7 @@ profile {{.Name}} flags=(attach_disconnected,mediate_deleted) {
   capability,
   file,
   umount,
+{{if ge .Version 208096}}
   # Host (privileged) processes may send signals to container processes.
   signal (receive) peer=unconfined,
   # runc may send signals to container processes (for "docker stop").
@@ -33,6 +34,7 @@ profile {{.Name}} flags=(attach_disconnected,mediate_deleted) {
   signal (receive) peer={{.DaemonProfile}},
   # Container processes may send signals amongst themselves.
   signal (send,receive) peer={{.Name}},
+{{end}}
 
   deny @{PROC}/* w,   # deny write for all files directly in /proc (not in a subdir)
   # deny write to files not in /proc/<number>/** or /proc/sys/**
@@ -53,7 +55,9 @@ profile {{.Name}} flags=(attach_disconnected,mediate_deleted) {
   deny /sys/devices/virtual/powercap/** rwklx,
   deny /sys/kernel/security/** rwklx,
 
+{{if ge .Version 208095}}
   # suppress ptrace denials when using 'docker ps' or using 'ps' inside a container
   ptrace (trace,read,tracedby,readby) peer={{.Name}},
+{{end}}
 }
 `
-- 
2.49.0

