helper: eliminate commandContext replacement
This is done more cleanly by modifying Args in cmdF. Signed-off-by: Ophestra <cat@gensokyo.uk>
This commit is contained in:
parent
bac4e67867
commit
6e7ddb2d2e
@ -3,6 +3,9 @@ package dbus_test
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
@ -100,12 +103,13 @@ func TestProxy_Seal(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestProxy_Start_Wait_Close_String(t *testing.T) {
|
func TestProxy_Start_Wait_Close_String(t *testing.T) {
|
||||||
t.Run("sandboxed", func(t *testing.T) {
|
t.Run("sandbox", func(t *testing.T) {
|
||||||
|
proxyName := dbus.ProxyName
|
||||||
|
dbus.ProxyName = os.Args[0]
|
||||||
|
t.Cleanup(func() { dbus.ProxyName = proxyName })
|
||||||
testProxyStartWaitCloseString(t, true)
|
testProxyStartWaitCloseString(t, true)
|
||||||
})
|
})
|
||||||
t.Run("direct", func(t *testing.T) {
|
t.Run("direct", func(t *testing.T) { testProxyStartWaitCloseString(t, false) })
|
||||||
testProxyStartWaitCloseString(t, false)
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func testProxyStartWaitCloseString(t *testing.T, sandbox bool) {
|
func testProxyStartWaitCloseString(t *testing.T, sandbox bool) {
|
||||||
@ -125,14 +129,30 @@ func testProxyStartWaitCloseString(t *testing.T, sandbox bool) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
t.Run("proxy for "+id, func(t *testing.T) {
|
t.Run("proxy for "+id, func(t *testing.T) {
|
||||||
helper.InternalReplaceExecCommand(t)
|
|
||||||
overridePath(t)
|
|
||||||
|
|
||||||
p := dbus.New(tc[0].bus, tc[1].bus)
|
p := dbus.New(tc[0].bus, tc[1].bus)
|
||||||
|
p.CmdF = func(cmd *exec.Cmd) {
|
||||||
|
wantArgv0 := dbus.ProxyName
|
||||||
|
if sandbox {
|
||||||
|
wantArgv0 = "bwrap"
|
||||||
|
}
|
||||||
|
if cmd.Args[0] != wantArgv0 {
|
||||||
|
panic(fmt.Sprintf("unexpected argv0 %q", os.Args[0]))
|
||||||
|
}
|
||||||
|
cmd.Err = nil
|
||||||
|
cmd.Path = os.Args[0]
|
||||||
|
|
||||||
|
if sandbox {
|
||||||
|
cmd.Args = append([]string{os.Args[0], "-test.run=TestHelperStub", "--"},
|
||||||
|
append(cmd.Args[:5], append([]string{"-test.run=TestHelperStub", "--"}, cmd.Args[5:]...)...)...)
|
||||||
|
cmd.Env = append(cmd.Env, "GO_TEST_FORTIFY_BWRAP_STUB_TYPE=dbus")
|
||||||
|
} else {
|
||||||
|
cmd.Args = append([]string{os.Args[0], "-test.run=TestHelperStub", "--"}, cmd.Args[1:]...)
|
||||||
|
}
|
||||||
|
}
|
||||||
output := new(strings.Builder)
|
output := new(strings.Builder)
|
||||||
|
|
||||||
t.Run("unsealed behaviour of "+id, func(t *testing.T) {
|
t.Run("unsealed", func(t *testing.T) {
|
||||||
t.Run("unsealed string of "+id, func(t *testing.T) {
|
t.Run("string", func(t *testing.T) {
|
||||||
want := "(unsealed dbus proxy)"
|
want := "(unsealed dbus proxy)"
|
||||||
if got := p.String(); got != want {
|
if got := p.String(); got != want {
|
||||||
t.Errorf("String() = %v, want %v",
|
t.Errorf("String() = %v, want %v",
|
||||||
@ -141,7 +161,7 @@ func testProxyStartWaitCloseString(t *testing.T, sandbox bool) {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("unsealed start of "+id, func(t *testing.T) {
|
t.Run("start", func(t *testing.T) {
|
||||||
want := "proxy not sealed"
|
want := "proxy not sealed"
|
||||||
if err := p.Start(context.Background(), nil, sandbox); err == nil || err.Error() != want {
|
if err := p.Start(context.Background(), nil, sandbox); err == nil || err.Error() != want {
|
||||||
t.Errorf("Start() error = %v, wantErr %q",
|
t.Errorf("Start() error = %v, wantErr %q",
|
||||||
@ -150,7 +170,7 @@ func testProxyStartWaitCloseString(t *testing.T, sandbox bool) {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("unsealed wait of "+id, func(t *testing.T) {
|
t.Run("wait", func(t *testing.T) {
|
||||||
wantErr := "dbus: not started"
|
wantErr := "dbus: not started"
|
||||||
if err := p.Wait(); err == nil || err.Error() != wantErr {
|
if err := p.Wait(); err == nil || err.Error() != wantErr {
|
||||||
t.Errorf("Wait() error = %v, wantErr %v",
|
t.Errorf("Wait() error = %v, wantErr %v",
|
||||||
@ -168,7 +188,7 @@ func testProxyStartWaitCloseString(t *testing.T, sandbox bool) {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("sealed behaviour of "+id, func(t *testing.T) {
|
t.Run("sealed", func(t *testing.T) {
|
||||||
want := strings.Join(append(tc[0].want, tc[1].want...), " ")
|
want := strings.Join(append(tc[0].want, tc[1].want...), " ")
|
||||||
if got := p.String(); got != want {
|
if got := p.String(); got != want {
|
||||||
t.Errorf("String() = %v, want %v",
|
t.Errorf("String() = %v, want %v",
|
||||||
@ -176,7 +196,7 @@ func testProxyStartWaitCloseString(t *testing.T, sandbox bool) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
t.Run("sealed start of "+id, func(t *testing.T) {
|
t.Run("start", func(t *testing.T) {
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
@ -185,8 +205,14 @@ func testProxyStartWaitCloseString(t *testing.T, sandbox bool) {
|
|||||||
err)
|
err)
|
||||||
}
|
}
|
||||||
|
|
||||||
t.Run("started string of "+id, func(t *testing.T) {
|
t.Run("string", func(t *testing.T) {
|
||||||
wantSubstr := dbus.ProxyName + " --args="
|
wantSubstr := fmt.Sprintf("%s -test.run=TestHelperStub -- --args=3 --fd=4", os.Args[0])
|
||||||
|
if sandbox {
|
||||||
|
wantSubstr = fmt.Sprintf(
|
||||||
|
"%s -test.run=TestHelperStub -- bwrap --args 6 -- %s -test.run=TestHelperStub -- --args=3 --fd=4",
|
||||||
|
os.Args[0], os.Args[0],
|
||||||
|
)
|
||||||
|
}
|
||||||
if got := p.String(); !strings.Contains(got, wantSubstr) {
|
if got := p.String(); !strings.Contains(got, wantSubstr) {
|
||||||
t.Errorf("String() = %v, want %v",
|
t.Errorf("String() = %v, want %v",
|
||||||
p.String(), wantSubstr)
|
p.String(), wantSubstr)
|
||||||
@ -194,7 +220,7 @@ func testProxyStartWaitCloseString(t *testing.T, sandbox bool) {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("started wait of "+id, func(t *testing.T) {
|
t.Run("wait", func(t *testing.T) {
|
||||||
p.Close()
|
p.Close()
|
||||||
if err := p.Wait(); err != nil {
|
if err := p.Wait(); err != nil {
|
||||||
t.Errorf("Wait() error = %v\noutput: %s",
|
t.Errorf("Wait() error = %v\noutput: %s",
|
||||||
@ -206,11 +232,3 @@ func testProxyStartWaitCloseString(t *testing.T, sandbox bool) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func overridePath(t *testing.T) {
|
|
||||||
proxyName := dbus.ProxyName
|
|
||||||
dbus.ProxyName = "/nonexistent-xdg-dbus-proxy"
|
|
||||||
t.Cleanup(func() {
|
|
||||||
dbus.ProxyName = proxyName
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
@ -8,6 +8,7 @@ import (
|
|||||||
"os/exec"
|
"os/exec"
|
||||||
"path"
|
"path"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"slices"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"syscall"
|
"syscall"
|
||||||
@ -26,25 +27,12 @@ func (p *Proxy) Start(ctx context.Context, output io.Writer, sandbox bool) error
|
|||||||
return errors.New("proxy not sealed")
|
return errors.New("proxy not sealed")
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var h helper.Helper
|
||||||
h helper.Helper
|
|
||||||
|
|
||||||
argF = func(argsFd, statFd int) []string {
|
|
||||||
if statFd == -1 {
|
|
||||||
return []string{"--args=" + strconv.Itoa(argsFd)}
|
|
||||||
} else {
|
|
||||||
return []string{"--args=" + strconv.Itoa(argsFd), "--fd=" + strconv.Itoa(statFd)}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
c, cancel := context.WithCancelCause(ctx)
|
c, cancel := context.WithCancelCause(ctx)
|
||||||
if !sandbox {
|
if !sandbox {
|
||||||
h = helper.NewDirect(c, p.name, p.seal, true, argF, func(cmd *exec.Cmd) {
|
h = helper.NewDirect(c, p.name, p.seal, true, argF, func(cmd *exec.Cmd) {
|
||||||
cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true}
|
cmdF(cmd, output, p.CmdF)
|
||||||
if output != nil {
|
|
||||||
cmd.Stdout, cmd.Stderr = output, output
|
|
||||||
}
|
|
||||||
|
|
||||||
// xdg-dbus-proxy does not need to inherit the environment
|
// xdg-dbus-proxy does not need to inherit the environment
|
||||||
cmd.Env = make([]string, 0)
|
cmd.Env = make([]string, 0)
|
||||||
@ -62,7 +50,7 @@ func (p *Proxy) Start(ctx context.Context, output io.Writer, sandbox bool) error
|
|||||||
|
|
||||||
// resolve libraries by parsing ldd output
|
// resolve libraries by parsing ldd output
|
||||||
var proxyDeps []*ldd.Entry
|
var proxyDeps []*ldd.Entry
|
||||||
if toolPath != "/nonexistent-xdg-dbus-proxy" {
|
if toolPath != os.Args[0] {
|
||||||
if l, err := ldd.Exec(ctx, toolPath); err != nil {
|
if l, err := ldd.Exec(ctx, toolPath); err != nil {
|
||||||
return err
|
return err
|
||||||
} else {
|
} else {
|
||||||
@ -71,7 +59,6 @@ func (p *Proxy) Start(ctx context.Context, output io.Writer, sandbox bool) error
|
|||||||
}
|
}
|
||||||
|
|
||||||
bc := &bwrap.Config{
|
bc := &bwrap.Config{
|
||||||
Unshare: nil,
|
|
||||||
Hostname: "fortify-dbus",
|
Hostname: "fortify-dbus",
|
||||||
Chdir: "/",
|
Chdir: "/",
|
||||||
Syscall: &bwrap.SyscallPolicy{DenyDevel: true, Multiarch: true},
|
Syscall: &bwrap.SyscallPolicy{DenyDevel: true, Multiarch: true},
|
||||||
@ -81,28 +68,35 @@ func (p *Proxy) Start(ctx context.Context, output io.Writer, sandbox bool) error
|
|||||||
}
|
}
|
||||||
|
|
||||||
// resolve proxy socket directories
|
// resolve proxy socket directories
|
||||||
bindTarget := make(map[string]struct{}, 2)
|
bindTargetM := make(map[string]struct{}, 2)
|
||||||
|
|
||||||
for _, ps := range []string{p.session[1], p.system[1]} {
|
for _, ps := range []string{p.session[1], p.system[1]} {
|
||||||
if pd := path.Dir(ps); len(pd) > 0 {
|
if pd := path.Dir(ps); len(pd) > 0 {
|
||||||
if pd[0] == '/' {
|
if pd[0] == '/' {
|
||||||
bindTarget[pd] = struct{}{}
|
bindTargetM[pd] = struct{}{}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for k := range bindTarget {
|
|
||||||
bc.Bind(k, k, false, true)
|
bindTarget := make([]string, 0, len(bindTargetM))
|
||||||
|
for k := range bindTargetM {
|
||||||
|
bindTarget = append(bindTarget, k)
|
||||||
|
}
|
||||||
|
slices.Sort(bindTarget)
|
||||||
|
for _, name := range bindTarget {
|
||||||
|
bc.Bind(name, name, false, true)
|
||||||
}
|
}
|
||||||
|
|
||||||
roBindTarget := make(map[string]struct{}, 2+1+len(proxyDeps))
|
roBindTargetM := make(map[string]struct{}, 2+1+len(proxyDeps))
|
||||||
|
|
||||||
// xdb-dbus-proxy bin and dependencies
|
// xdb-dbus-proxy bin and dependencies
|
||||||
roBindTarget[path.Dir(toolPath)] = struct{}{}
|
roBindTargetM[path.Dir(toolPath)] = struct{}{}
|
||||||
for _, ent := range proxyDeps {
|
for _, ent := range proxyDeps {
|
||||||
if path.IsAbs(ent.Path) {
|
if path.IsAbs(ent.Path) {
|
||||||
roBindTarget[path.Dir(ent.Path)] = struct{}{}
|
roBindTargetM[path.Dir(ent.Path)] = struct{}{}
|
||||||
}
|
}
|
||||||
if path.IsAbs(ent.Name) {
|
if path.IsAbs(ent.Name) {
|
||||||
roBindTarget[path.Dir(ent.Name)] = struct{}{}
|
roBindTargetM[path.Dir(ent.Name)] = struct{}{}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -110,20 +104,25 @@ func (p *Proxy) Start(ctx context.Context, output io.Writer, sandbox bool) error
|
|||||||
for _, as := range []string{p.session[0], p.system[0]} {
|
for _, as := range []string{p.session[0], p.system[0]} {
|
||||||
if len(as) > 0 && strings.HasPrefix(as, "unix:path=/") {
|
if len(as) > 0 && strings.HasPrefix(as, "unix:path=/") {
|
||||||
// leave / intact
|
// leave / intact
|
||||||
roBindTarget[path.Dir(as[10:])] = struct{}{}
|
roBindTargetM[path.Dir(as[10:])] = struct{}{}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for k := range roBindTarget {
|
roBindTarget := make([]string, 0, len(roBindTargetM))
|
||||||
bc.Bind(k, k)
|
for k := range roBindTargetM {
|
||||||
|
roBindTarget = append(roBindTarget, k)
|
||||||
|
}
|
||||||
|
slices.Sort(roBindTarget)
|
||||||
|
for _, name := range roBindTarget {
|
||||||
|
bc.Bind(name, name)
|
||||||
}
|
}
|
||||||
|
|
||||||
h = helper.MustNewBwrap(c, toolPath, p.seal, true, argF, func(cmd *exec.Cmd) {
|
h = helper.MustNewBwrap(c, toolPath,
|
||||||
cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true}
|
p.seal, true,
|
||||||
if output != nil {
|
argF, func(cmd *exec.Cmd) { cmdF(cmd, output, p.CmdF) },
|
||||||
cmd.Stdout, cmd.Stderr = output, output
|
nil,
|
||||||
}
|
bc, nil,
|
||||||
}, nil, bc, nil)
|
)
|
||||||
p.bwrap = bc
|
p.bwrap = bc
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -182,3 +181,21 @@ func (p *Proxy) Close() {
|
|||||||
p.cancel(proxyClosed)
|
p.cancel(proxyClosed)
|
||||||
p.cancel = nil
|
p.cancel = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func argF(argsFd, statFd int) []string {
|
||||||
|
if statFd == -1 {
|
||||||
|
return []string{"--args=" + strconv.Itoa(argsFd)}
|
||||||
|
} else {
|
||||||
|
return []string{"--args=" + strconv.Itoa(argsFd), "--fd=" + strconv.Itoa(statFd)}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func cmdF(cmd *exec.Cmd, output io.Writer, cmdF func(cmd *exec.Cmd)) {
|
||||||
|
cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true}
|
||||||
|
if output != nil {
|
||||||
|
cmd.Stdout, cmd.Stderr = output, output
|
||||||
|
}
|
||||||
|
if cmdF != nil {
|
||||||
|
cmdF(cmd)
|
||||||
|
}
|
||||||
|
}
|
@ -5,6 +5,7 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
|
"os/exec"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
"git.gensokyo.uk/security/fortify/helper"
|
"git.gensokyo.uk/security/fortify/helper"
|
||||||
@ -26,6 +27,7 @@ type Proxy struct {
|
|||||||
name string
|
name string
|
||||||
session [2]string
|
session [2]string
|
||||||
system [2]string
|
system [2]string
|
||||||
|
CmdF func(cmd *exec.Cmd)
|
||||||
sysP bool
|
sysP bool
|
||||||
|
|
||||||
seal io.WriterTo
|
seal io.WriterTo
|
||||||
|
@ -6,6 +6,12 @@ import (
|
|||||||
"git.gensokyo.uk/security/fortify/dbus"
|
"git.gensokyo.uk/security/fortify/dbus"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
sampleHostPath = "/run/user/1971/bus"
|
||||||
|
sampleHostAddr = "unix:path=" + sampleHostPath
|
||||||
|
sampleBindPath = "/tmp/fortify.1971/12622d846cc3fe7b4c10359d01f0eb47/bus"
|
||||||
|
)
|
||||||
|
|
||||||
var samples = []dbusTestCase{
|
var samples = []dbusTestCase{
|
||||||
{
|
{
|
||||||
"org.chromium.Chromium", &dbus.Config{
|
"org.chromium.Chromium", &dbus.Config{
|
||||||
@ -19,10 +25,10 @@ var samples = []dbusTestCase{
|
|||||||
Log: false,
|
Log: false,
|
||||||
Filter: true,
|
Filter: true,
|
||||||
}, false, false,
|
}, false, false,
|
||||||
[2]string{"unix:path=/run/user/1971/bus", "/tmp/fortify.1971/12622d846cc3fe7b4c10359d01f0eb47/bus"},
|
[2]string{sampleHostAddr, sampleBindPath},
|
||||||
[]string{
|
[]string{
|
||||||
"unix:path=/run/user/1971/bus",
|
sampleHostAddr,
|
||||||
"/tmp/fortify.1971/12622d846cc3fe7b4c10359d01f0eb47/bus",
|
sampleBindPath,
|
||||||
"--filter",
|
"--filter",
|
||||||
"--talk=org.freedesktop.Notifications",
|
"--talk=org.freedesktop.Notifications",
|
||||||
"--talk=org.freedesktop.FileManager1",
|
"--talk=org.freedesktop.FileManager1",
|
||||||
@ -48,9 +54,10 @@ var samples = []dbusTestCase{
|
|||||||
Log: false,
|
Log: false,
|
||||||
Filter: true,
|
Filter: true,
|
||||||
}, false, false,
|
}, false, false,
|
||||||
[2]string{"unix:path=/run/dbus/system_bus_socket", "/tmp/fortify.1971/12622d846cc3fe7b4c10359d01f0eb47/system_bus_socket"},
|
[2]string{sampleHostAddr, sampleBindPath},
|
||||||
[]string{"unix:path=/run/dbus/system_bus_socket",
|
[]string{
|
||||||
"/tmp/fortify.1971/12622d846cc3fe7b4c10359d01f0eb47/system_bus_socket",
|
sampleHostAddr,
|
||||||
|
sampleBindPath,
|
||||||
"--filter",
|
"--filter",
|
||||||
"--talk=org.bluez",
|
"--talk=org.bluez",
|
||||||
"--talk=org.freedesktop.Avahi",
|
"--talk=org.freedesktop.Avahi",
|
||||||
@ -68,10 +75,10 @@ var samples = []dbusTestCase{
|
|||||||
Log: false,
|
Log: false,
|
||||||
Filter: true,
|
Filter: true,
|
||||||
}, false, false,
|
}, false, false,
|
||||||
[2]string{"unix:path=/run/user/1971/bus", "/tmp/fortify.1971/34c24f16a0d791d28835ededaf446033/bus"},
|
[2]string{sampleHostAddr, sampleBindPath},
|
||||||
[]string{
|
[]string{
|
||||||
"unix:path=/run/user/1971/bus",
|
sampleHostAddr,
|
||||||
"/tmp/fortify.1971/34c24f16a0d791d28835ededaf446033/bus",
|
sampleBindPath,
|
||||||
"--filter",
|
"--filter",
|
||||||
"--talk=org.freedesktop.Notifications",
|
"--talk=org.freedesktop.Notifications",
|
||||||
"--talk=org.kde.StatusNotifierWatcher",
|
"--talk=org.kde.StatusNotifierWatcher",
|
||||||
@ -91,10 +98,10 @@ var samples = []dbusTestCase{
|
|||||||
Log: true,
|
Log: true,
|
||||||
Filter: true,
|
Filter: true,
|
||||||
}, false, false,
|
}, false, false,
|
||||||
[2]string{"unix:path=/run/user/1971/bus", "/tmp/fortify.1971/5da7845287a936efbc2fa75d7d81e501/bus"},
|
[2]string{sampleHostAddr, sampleBindPath},
|
||||||
[]string{
|
[]string{
|
||||||
"unix:path=/run/user/1971/bus",
|
sampleHostAddr,
|
||||||
"/tmp/fortify.1971/5da7845287a936efbc2fa75d7d81e501/bus",
|
sampleBindPath,
|
||||||
"--filter",
|
"--filter",
|
||||||
"--see=uk.gensokyo.CrashTestDummy1",
|
"--see=uk.gensokyo.CrashTestDummy1",
|
||||||
"--talk=org.freedesktop.Notifications",
|
"--talk=org.freedesktop.Notifications",
|
||||||
@ -114,10 +121,10 @@ var samples = []dbusTestCase{
|
|||||||
Log: true,
|
Log: true,
|
||||||
Filter: true,
|
Filter: true,
|
||||||
}, false, true,
|
}, false, true,
|
||||||
[2]string{"unix:path=/run/user/1971/bus", "/tmp/fortify.1971/5da7845287a936efbc2fa75d7d81e501/bus"},
|
[2]string{sampleHostAddr, sampleBindPath},
|
||||||
[]string{
|
[]string{
|
||||||
"unix:path=/run/user/1971/bus",
|
sampleHostAddr,
|
||||||
"/tmp/fortify.1971/5da7845287a936efbc2fa75d7d81e501/bus",
|
sampleBindPath,
|
||||||
"--filter",
|
"--filter",
|
||||||
"--see=uk.gensokyo.CrashTestDummy",
|
"--see=uk.gensokyo.CrashTestDummy",
|
||||||
"--talk=org.freedesktop.Notifications",
|
"--talk=org.freedesktop.Notifications",
|
||||||
|
@ -6,6 +6,4 @@ import (
|
|||||||
"git.gensokyo.uk/security/fortify/helper"
|
"git.gensokyo.uk/security/fortify/helper"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestHelperChildStub(t *testing.T) {
|
func TestHelperStub(t *testing.T) { helper.InternalHelperStub() }
|
||||||
helper.InternalChildStub()
|
|
||||||
}
|
|
||||||
|
@ -3,6 +3,8 @@ package helper_test
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"strings"
|
"strings"
|
||||||
@ -17,7 +19,7 @@ func TestBwrap(t *testing.T) {
|
|||||||
sc := &bwrap.Config{
|
sc := &bwrap.Config{
|
||||||
Net: true,
|
Net: true,
|
||||||
Hostname: "localhost",
|
Hostname: "localhost",
|
||||||
Chdir: "/nonexistent",
|
Chdir: "/proc/nonexistent",
|
||||||
Clearenv: true,
|
Clearenv: true,
|
||||||
NewSession: true,
|
NewSession: true,
|
||||||
DieWithParent: true,
|
DieWithParent: true,
|
||||||
@ -26,14 +28,12 @@ func TestBwrap(t *testing.T) {
|
|||||||
|
|
||||||
t.Run("nonexistent bwrap name", func(t *testing.T) {
|
t.Run("nonexistent bwrap name", func(t *testing.T) {
|
||||||
bubblewrapName := helper.BubblewrapName
|
bubblewrapName := helper.BubblewrapName
|
||||||
helper.BubblewrapName = "/nonexistent"
|
helper.BubblewrapName = "/proc/nonexistent"
|
||||||
t.Cleanup(func() {
|
t.Cleanup(func() { helper.BubblewrapName = bubblewrapName })
|
||||||
helper.BubblewrapName = bubblewrapName
|
|
||||||
})
|
|
||||||
|
|
||||||
h := helper.MustNewBwrap(
|
h := helper.MustNewBwrap(
|
||||||
context.Background(),
|
context.Background(),
|
||||||
"fortify",
|
"false",
|
||||||
argsWt, false,
|
argsWt, false,
|
||||||
argF, nil,
|
argF, nil,
|
||||||
nil,
|
nil,
|
||||||
@ -49,14 +49,14 @@ func TestBwrap(t *testing.T) {
|
|||||||
t.Run("valid new helper nil check", func(t *testing.T) {
|
t.Run("valid new helper nil check", func(t *testing.T) {
|
||||||
if got := helper.MustNewBwrap(
|
if got := helper.MustNewBwrap(
|
||||||
context.TODO(),
|
context.TODO(),
|
||||||
"fortify",
|
"false",
|
||||||
argsWt, false,
|
argsWt, false,
|
||||||
argF, nil,
|
argF, nil,
|
||||||
nil,
|
nil,
|
||||||
sc, nil,
|
sc, nil,
|
||||||
); got == nil {
|
); got == nil {
|
||||||
t.Errorf("MustNewBwrap(%#v, %#v, %#v) got nil",
|
t.Errorf("MustNewBwrap(%#v, %#v, %#v) got nil",
|
||||||
sc, argsWt, "fortify")
|
sc, argsWt, "false")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@ -72,7 +72,7 @@ func TestBwrap(t *testing.T) {
|
|||||||
|
|
||||||
helper.MustNewBwrap(
|
helper.MustNewBwrap(
|
||||||
context.TODO(),
|
context.TODO(),
|
||||||
"fortify",
|
"false",
|
||||||
argsWt, false,
|
argsWt, false,
|
||||||
argF, nil,
|
argF, nil,
|
||||||
nil,
|
nil,
|
||||||
@ -81,15 +81,13 @@ func TestBwrap(t *testing.T) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
t.Run("start without pipes", func(t *testing.T) {
|
t.Run("start without pipes", func(t *testing.T) {
|
||||||
helper.InternalReplaceExecCommand(t)
|
|
||||||
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
stdout, stderr := new(strings.Builder), new(strings.Builder)
|
stdout, stderr := new(strings.Builder), new(strings.Builder)
|
||||||
h := helper.MustNewBwrap(
|
h := helper.MustNewBwrap(
|
||||||
ctx, "crash-test-dummy",
|
ctx, os.Args[0],
|
||||||
nil, false,
|
nil, false,
|
||||||
argFChecked, func(cmd *exec.Cmd) { cmd.Stdout, cmd.Stderr = stdout, stderr },
|
argFChecked, func(cmd *exec.Cmd) { cmd.Stdout, cmd.Stderr = stdout, stderr; hijackBwrap(cmd) },
|
||||||
nil,
|
nil,
|
||||||
sc, nil,
|
sc, nil,
|
||||||
)
|
)
|
||||||
@ -107,14 +105,23 @@ func TestBwrap(t *testing.T) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
t.Run("implementation compliance", func(t *testing.T) {
|
t.Run("implementation compliance", func(t *testing.T) {
|
||||||
testHelper(t, func(ctx context.Context, cmdF func(cmd *exec.Cmd), stat bool) helper.Helper {
|
testHelper(t, func(ctx context.Context, setOutput func(stdoutP, stderrP *io.Writer), stat bool) helper.Helper {
|
||||||
return helper.MustNewBwrap(
|
return helper.MustNewBwrap(
|
||||||
ctx, "crash-test-dummy",
|
ctx, os.Args[0],
|
||||||
argsWt, stat,
|
argsWt, stat,
|
||||||
argF, cmdF,
|
argF, func(cmd *exec.Cmd) { setOutput(&cmd.Stdout, &cmd.Stderr); hijackBwrap(cmd) },
|
||||||
nil,
|
nil,
|
||||||
sc, nil,
|
sc, nil,
|
||||||
)
|
)
|
||||||
})
|
}, "exec")
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func hijackBwrap(cmd *exec.Cmd) {
|
||||||
|
if cmd.Args[0] != "bwrap" {
|
||||||
|
panic(fmt.Sprintf("unexpected argv0 %q", cmd.Args[0]))
|
||||||
|
}
|
||||||
|
cmd.Err = nil
|
||||||
|
cmd.Path = os.Args[0]
|
||||||
|
cmd.Args = append([]string{os.Args[0], "-test.run=TestHelperStub", "--"}, cmd.Args...)
|
||||||
|
}
|
||||||
|
@ -42,7 +42,7 @@ func newHelperCmd(
|
|||||||
) (cmd *helperCmd, args []string) {
|
) (cmd *helperCmd, args []string) {
|
||||||
cmd = new(helperCmd)
|
cmd = new(helperCmd)
|
||||||
cmd.helperFiles, args = newHelperFiles(ctx, wt, stat, argF, extraFiles)
|
cmd.helperFiles, args = newHelperFiles(ctx, wt, stat, argF, extraFiles)
|
||||||
cmd.Cmd = commandContext(ctx, name)
|
cmd.Cmd = exec.CommandContext(ctx, name)
|
||||||
cmd.Cmd.Cancel = func() error { return cmd.Process.Signal(syscall.SIGTERM) }
|
cmd.Cmd.Cancel = func() error { return cmd.Process.Signal(syscall.SIGTERM) }
|
||||||
cmd.WaitDelay = WaitDelay
|
cmd.WaitDelay = WaitDelay
|
||||||
return
|
return
|
||||||
|
@ -3,6 +3,7 @@ package helper_test
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
|
"io"
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"testing"
|
"testing"
|
||||||
@ -10,9 +11,9 @@ import (
|
|||||||
"git.gensokyo.uk/security/fortify/helper"
|
"git.gensokyo.uk/security/fortify/helper"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestDirect(t *testing.T) {
|
func TestCmd(t *testing.T) {
|
||||||
t.Run("start non-existent helper path", func(t *testing.T) {
|
t.Run("start non-existent helper path", func(t *testing.T) {
|
||||||
h := helper.NewDirect(context.Background(), "/nonexistent", argsWt, false, argF, nil, nil)
|
h := helper.NewDirect(context.Background(), "/proc/nonexistent", argsWt, false, argF, nil, nil)
|
||||||
|
|
||||||
if err := h.Start(); !errors.Is(err, os.ErrNotExist) {
|
if err := h.Start(); !errors.Is(err, os.ErrNotExist) {
|
||||||
t.Errorf("Start: error = %v, wantErr %v",
|
t.Errorf("Start: error = %v, wantErr %v",
|
||||||
@ -22,15 +23,17 @@ func TestDirect(t *testing.T) {
|
|||||||
|
|
||||||
t.Run("valid new helper nil check", func(t *testing.T) {
|
t.Run("valid new helper nil check", func(t *testing.T) {
|
||||||
if got := helper.NewDirect(context.TODO(), "fortify", argsWt, false, argF, nil, nil); got == nil {
|
if got := helper.NewDirect(context.TODO(), "fortify", argsWt, false, argF, nil, nil); got == nil {
|
||||||
t.Errorf("New(%q, %q) got nil",
|
t.Errorf("NewDirect(%q, %q) got nil",
|
||||||
argsWt, "fortify")
|
argsWt, "fortify")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("implementation compliance", func(t *testing.T) {
|
t.Run("implementation compliance", func(t *testing.T) {
|
||||||
testHelper(t, func(ctx context.Context, cmdF func(cmd *exec.Cmd), stat bool) helper.Helper {
|
testHelper(t, func(ctx context.Context, setOutput func(stdoutP, stderrP *io.Writer), stat bool) helper.Helper {
|
||||||
return helper.NewDirect(ctx, "crash-test-dummy", argsWt, stat, argF, cmdF, nil)
|
return helper.NewDirect(ctx, os.Args[0], argsWt, stat, argF, func(cmd *exec.Cmd) {
|
||||||
})
|
setOutput(&cmd.Stdout, &cmd.Stderr)
|
||||||
|
}, nil)
|
||||||
|
}, "exec")
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -6,17 +6,12 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"git.gensokyo.uk/security/fortify/helper/proc"
|
"git.gensokyo.uk/security/fortify/helper/proc"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var WaitDelay = 2 * time.Second
|
||||||
WaitDelay = 2 * time.Second
|
|
||||||
|
|
||||||
commandContext = exec.CommandContext
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
const (
|
||||||
// FortifyHelper is set to 1 when args fd is enabled and 0 otherwise.
|
// FortifyHelper is set to 1 when args fd is enabled and 0 otherwise.
|
||||||
|
@ -4,7 +4,7 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os/exec"
|
"io"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
@ -36,7 +36,8 @@ func argF(argsFd, statFd int) []string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func argFChecked(argsFd, statFd int) (args []string) {
|
func argFChecked(argsFd, statFd int) (args []string) {
|
||||||
args = make([]string, 0, 4)
|
args = make([]string, 0, 6)
|
||||||
|
args = append(args, "-test.run=TestHelperStub", "--")
|
||||||
if argsFd > -1 {
|
if argsFd > -1 {
|
||||||
args = append(args, "--args", strconv.Itoa(argsFd))
|
args = append(args, "--args", strconv.Itoa(argsFd))
|
||||||
}
|
}
|
||||||
@ -47,13 +48,14 @@ func argFChecked(argsFd, statFd int) (args []string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// this function tests an implementation of the helper.Helper interface
|
// this function tests an implementation of the helper.Helper interface
|
||||||
func testHelper(t *testing.T, createHelper func(ctx context.Context, cmdF func(cmd *exec.Cmd), stat bool) helper.Helper) {
|
func testHelper(t *testing.T,
|
||||||
helper.InternalReplaceExecCommand(t)
|
createHelper func(ctx context.Context, setOutput func(stdoutP, stderrP *io.Writer), stat bool) helper.Helper,
|
||||||
|
prefix string,
|
||||||
|
) {
|
||||||
t.Run("start helper with status channel and wait", func(t *testing.T) {
|
t.Run("start helper with status channel and wait", func(t *testing.T) {
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||||
stdout, stderr := new(strings.Builder), new(strings.Builder)
|
stdout, stderr := new(strings.Builder), new(strings.Builder)
|
||||||
h := createHelper(ctx, func(cmd *exec.Cmd) { cmd.Stdout, cmd.Stderr = stdout, stderr }, true)
|
h := createHelper(ctx, func(stdoutP, stderrP *io.Writer) { *stdoutP, *stderrP = stdout, stderr }, true)
|
||||||
|
|
||||||
t.Run("wait not yet started helper", func(t *testing.T) {
|
t.Run("wait not yet started helper", func(t *testing.T) {
|
||||||
defer func() {
|
defer func() {
|
||||||
@ -75,7 +77,7 @@ func testHelper(t *testing.T, createHelper func(ctx context.Context, cmdF func(c
|
|||||||
cancel()
|
cancel()
|
||||||
|
|
||||||
t.Run("start already started helper", func(t *testing.T) {
|
t.Run("start already started helper", func(t *testing.T) {
|
||||||
wantErr := "exec: already started"
|
wantErr := prefix + ": already started"
|
||||||
if err := h.Start(); err != nil && err.Error() != wantErr {
|
if err := h.Start(); err != nil && err.Error() != wantErr {
|
||||||
t.Errorf("Start: error = %v, wantErr %v",
|
t.Errorf("Start: error = %v, wantErr %v",
|
||||||
err, wantErr)
|
err, wantErr)
|
||||||
@ -108,7 +110,7 @@ func testHelper(t *testing.T, createHelper func(ctx context.Context, cmdF func(c
|
|||||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
stdout, stderr := new(strings.Builder), new(strings.Builder)
|
stdout, stderr := new(strings.Builder), new(strings.Builder)
|
||||||
h := createHelper(ctx, func(cmd *exec.Cmd) { cmd.Stdout, cmd.Stderr = stdout, stderr }, false)
|
h := createHelper(ctx, func(stdoutP, stderrP *io.Writer) { *stdoutP, *stderrP = stdout, stderr }, false)
|
||||||
|
|
||||||
if err := h.Start(); err != nil {
|
if err := h.Start(); err != nil {
|
||||||
t.Errorf("Start() error = %v",
|
t.Errorf("Start() error = %v",
|
||||||
|
@ -1,25 +1,24 @@
|
|||||||
package helper
|
package helper
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
|
||||||
"flag"
|
"flag"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"path"
|
||||||
|
"slices"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"syscall"
|
"syscall"
|
||||||
"testing"
|
|
||||||
|
|
||||||
"git.gensokyo.uk/security/fortify/helper/bwrap"
|
"git.gensokyo.uk/security/fortify/helper/bwrap"
|
||||||
"git.gensokyo.uk/security/fortify/helper/proc"
|
"git.gensokyo.uk/security/fortify/helper/proc"
|
||||||
"git.gensokyo.uk/security/fortify/internal"
|
"git.gensokyo.uk/security/fortify/internal"
|
||||||
)
|
)
|
||||||
|
|
||||||
// InternalChildStub is an internal function but exported because it is cross-package;
|
// InternalHelperStub is an internal function but exported because it is cross-package;
|
||||||
// it is part of the implementation of the helper stub.
|
// it is part of the implementation of the helper stub.
|
||||||
func InternalChildStub() {
|
func InternalHelperStub() {
|
||||||
// this test mocks the helper process
|
// this test mocks the helper process
|
||||||
var ap, sp string
|
var ap, sp string
|
||||||
if v, ok := os.LookupEnv(FortifyHelper); !ok {
|
if v, ok := os.LookupEnv(FortifyHelper); !ok {
|
||||||
@ -33,32 +32,15 @@ func InternalChildStub() {
|
|||||||
sp = v
|
sp = v
|
||||||
}
|
}
|
||||||
|
|
||||||
switch os.Args[3] {
|
if len(os.Args) > 3 && os.Args[3] == "bwrap" {
|
||||||
case "bwrap":
|
|
||||||
bwrapStub()
|
bwrapStub()
|
||||||
default:
|
} else {
|
||||||
genericStub(flagRestoreFiles(4, ap, sp))
|
genericStub(flagRestoreFiles(3, ap, sp))
|
||||||
}
|
}
|
||||||
|
|
||||||
internal.Exit(0)
|
internal.Exit(0)
|
||||||
}
|
}
|
||||||
|
|
||||||
// InternalReplaceExecCommand is an internal function but exported because it is cross-package;
|
|
||||||
// it is part of the implementation of the helper stub.
|
|
||||||
func InternalReplaceExecCommand(t *testing.T) {
|
|
||||||
t.Cleanup(func() { commandContext = exec.CommandContext })
|
|
||||||
|
|
||||||
// replace execCommand to have the resulting *exec.Cmd launch TestHelperChildStub
|
|
||||||
commandContext = func(ctx context.Context, name string, arg ...string) *exec.Cmd {
|
|
||||||
// pass through nonexistent path
|
|
||||||
if name == "/nonexistent" && len(arg) == 0 {
|
|
||||||
return exec.CommandContext(ctx, name)
|
|
||||||
}
|
|
||||||
|
|
||||||
return exec.CommandContext(ctx, os.Args[0], append([]string{"-test.run=TestHelperChildStub", "--", name}, arg...)...)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func newFile(fd int, name, p string) *os.File {
|
func newFile(fd int, name, p string) *os.File {
|
||||||
present := false
|
present := false
|
||||||
switch p {
|
switch p {
|
||||||
@ -149,26 +131,54 @@ func bwrapStub() {
|
|||||||
sc := &bwrap.Config{
|
sc := &bwrap.Config{
|
||||||
Net: true,
|
Net: true,
|
||||||
Hostname: "localhost",
|
Hostname: "localhost",
|
||||||
Chdir: "/nonexistent",
|
Chdir: "/proc/nonexistent",
|
||||||
Clearenv: true,
|
Clearenv: true,
|
||||||
NewSession: true,
|
NewSession: true,
|
||||||
DieWithParent: true,
|
DieWithParent: true,
|
||||||
AsInit: true,
|
AsInit: true,
|
||||||
}
|
}
|
||||||
if _, err := MustNewCheckedArgs(sc.Args(nil, new(proc.ExtraFilesPre), new([]proc.File))).
|
|
||||||
|
efp := new(proc.ExtraFilesPre)
|
||||||
|
if t, ok := os.LookupEnv("GO_TEST_FORTIFY_BWRAP_STUB_TYPE"); ok {
|
||||||
|
switch t {
|
||||||
|
case "dbus":
|
||||||
|
sc.Net = false
|
||||||
|
sc.Hostname = "fortify-dbus"
|
||||||
|
sc.Chdir = "/"
|
||||||
|
sc.Syscall = &bwrap.SyscallPolicy{DenyDevel: true, Multiarch: true}
|
||||||
|
sc.AsInit = false
|
||||||
|
|
||||||
|
bindTarget := []string{"/tmp/fortify.1971/12622d846cc3fe7b4c10359d01f0eb47"}
|
||||||
|
slices.Sort(bindTarget)
|
||||||
|
for _, name := range bindTarget {
|
||||||
|
sc.Bind(name, name, false, true)
|
||||||
|
}
|
||||||
|
roBindTarget := []string{"/run/user/1971", path.Dir(os.Args[0])}
|
||||||
|
slices.Sort(roBindTarget)
|
||||||
|
for _, name := range roBindTarget {
|
||||||
|
sc.Bind(name, name)
|
||||||
|
}
|
||||||
|
|
||||||
|
// manipulate extra files list so fd ends up as 5
|
||||||
|
efp.Append()
|
||||||
|
efp.Append()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := MustNewCheckedArgs(sc.Args(nil, efp, new([]proc.File))).
|
||||||
WriteTo(want); err != nil {
|
WriteTo(want); err != nil {
|
||||||
panic("cannot read want: " + err.Error())
|
panic("cannot read want: " + err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(flag.CommandLine.Args()) > 0 && flag.CommandLine.Args()[0] == "crash-test-dummy" && got.String() != want.String() {
|
if got.String() != want.String() {
|
||||||
panic("bad bwrap args\ngot: " + got.String() + "\nwant: " + want.String())
|
panic("bad bwrap args\ngot: " + got.String() + "\nwant: " + want.String())
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
if err := syscall.Exec(
|
if err := syscall.Exec(
|
||||||
os.Args[0],
|
flag.CommandLine.Args()[0],
|
||||||
append([]string{os.Args[0], "-test.run=TestHelperChildStub", "--"}, flag.CommandLine.Args()...),
|
flag.CommandLine.Args(),
|
||||||
os.Environ()); err != nil {
|
os.Environ()); err != nil {
|
||||||
panic("cannot start general stub: " + err.Error())
|
panic("cannot start helper stub: " + err.Error())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -6,6 +6,4 @@ import (
|
|||||||
"git.gensokyo.uk/security/fortify/helper"
|
"git.gensokyo.uk/security/fortify/helper"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestHelperChildStub(t *testing.T) {
|
func TestHelperStub(t *testing.T) { helper.InternalHelperStub() }
|
||||||
helper.InternalChildStub()
|
|
||||||
}
|
|
||||||
|
Loading…
Reference in New Issue
Block a user