2021-08-23 13:14:53 +00:00
|
|
|
//go:build linux
|
2016-01-19 22:57:03 +00:00
|
|
|
// +build linux
|
|
|
|
|
2018-02-05 21:05:59 +00:00
|
|
|
package seccomp // import "github.com/docker/docker/profiles/seccomp"
|
2016-01-19 22:57:03 +00:00
|
|
|
|
|
|
|
import (
|
2020-10-01 11:25:37 +00:00
|
|
|
"encoding/json"
|
2021-08-24 10:10:50 +00:00
|
|
|
"os"
|
2020-10-01 10:26:46 +00:00
|
|
|
"strings"
|
2016-01-19 22:57:03 +00:00
|
|
|
"testing"
|
2016-07-13 13:41:30 +00:00
|
|
|
|
2020-09-28 12:55:28 +00:00
|
|
|
"github.com/opencontainers/runtime-spec/specs-go"
|
2020-10-01 11:25:37 +00:00
|
|
|
"gotest.tools/v3/assert"
|
2016-01-19 22:57:03 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
func TestLoadProfile(t *testing.T) {
|
2021-08-24 10:10:50 +00:00
|
|
|
f, err := os.ReadFile("fixtures/example.json")
|
2016-01-19 22:57:03 +00:00
|
|
|
if err != nil {
|
|
|
|
t.Fatal(err)
|
|
|
|
}
|
2020-09-28 12:55:28 +00:00
|
|
|
rs := createSpec()
|
2021-02-09 13:30:14 +00:00
|
|
|
p, err := LoadProfile(string(f), &rs)
|
|
|
|
if err != nil {
|
2016-02-19 08:22:36 +00:00
|
|
|
t.Fatal(err)
|
|
|
|
}
|
2021-02-09 13:30:14 +00:00
|
|
|
var expectedErrno uint = 12345
|
2021-07-30 14:47:17 +00:00
|
|
|
var expectedDefaultErrno uint = 1
|
2021-02-09 13:30:14 +00:00
|
|
|
expected := specs.LinuxSeccomp{
|
2021-07-30 14:47:17 +00:00
|
|
|
DefaultAction: specs.ActErrno,
|
|
|
|
DefaultErrnoRet: &expectedDefaultErrno,
|
2021-02-09 13:30:14 +00:00
|
|
|
Syscalls: []specs.LinuxSyscall{
|
|
|
|
{
|
|
|
|
Names: []string{"clone"},
|
2021-07-16 13:36:38 +00:00
|
|
|
Action: specs.ActAllow,
|
2021-02-09 13:30:14 +00:00
|
|
|
Args: []specs.LinuxSeccompArg{{
|
|
|
|
Index: 0,
|
|
|
|
Value: 2114060288,
|
|
|
|
ValueTwo: 0,
|
2021-07-16 13:36:38 +00:00
|
|
|
Op: specs.OpMaskedEqual,
|
2021-02-09 13:30:14 +00:00
|
|
|
}},
|
|
|
|
},
|
|
|
|
{
|
|
|
|
|
|
|
|
Names: []string{"open"},
|
2021-07-16 13:36:38 +00:00
|
|
|
Action: specs.ActAllow,
|
2021-02-09 13:30:14 +00:00
|
|
|
Args: []specs.LinuxSeccompArg{},
|
|
|
|
},
|
|
|
|
{
|
|
|
|
Names: []string{"close"},
|
2021-07-16 13:36:38 +00:00
|
|
|
Action: specs.ActAllow,
|
2021-02-09 13:30:14 +00:00
|
|
|
Args: []specs.LinuxSeccompArg{},
|
|
|
|
},
|
|
|
|
{
|
|
|
|
Names: []string{"syslog"},
|
2021-07-16 13:36:38 +00:00
|
|
|
Action: specs.ActErrno,
|
2021-02-09 13:30:14 +00:00
|
|
|
ErrnoRet: &expectedErrno,
|
|
|
|
Args: []specs.LinuxSeccompArg{},
|
|
|
|
},
|
|
|
|
},
|
|
|
|
}
|
|
|
|
|
|
|
|
assert.DeepEqual(t, expected, *p)
|
2016-02-19 08:22:36 +00:00
|
|
|
}
|
2016-01-19 22:57:03 +00:00
|
|
|
|
seccomp: Sync fields with runtime-spec fields
The runtime spec we are using has support for these 3 fields[1], but
moby doesn't have them in its seccomp struct. This patch just adds and
copies them when they are in the profile.
DefaultErrnoRet is implemented in the runc version moby is using (it is
implemented since runc-rc95[2]) but if we create a container without
this moby patch, we don't see an error nor the expected behavior. This
is not clear for the user (the profile they specify is valid, the syntax
is ok, but the wrong behavior is seen).
This is because the DefaultErrnoRet field is not copied to the config
passed ultimately to runc (i.e. is like the field was not specified).
With this patch, we see the expected behavior.
The other two fileds are in the runtime-spec but not yet in runc (a PR
is open and targets 1.1.0 milestone). However, I took the liberty to
copy them now too for two reasons:
1. If we don't add them now and end up using a runc version that
supports them, then the error that the user will see is not clear at
all:
docker: Error response from daemon: OCI runtime create failed: container_linux.go:380: starting container process caused: listenerPath is not set: unknown.
And it is not obvious to debug for the user, as the field _is_ set in
the profile they specify (just not copied by moby to the profile moby
specifies ultimately to runc).
2. When using a runc without seccomp notify support (like today), the
error we see is the same with and without this moby patch (when using a
seccomp profile with the new fields):
docker: Error response from daemon: OCI runtime create failed: string SCMP_ACT_NOTIFY is not a valid action for seccomp: unknown.
Then, it seems like a clear win to add them now: we don't have to do it
later (that implies not clear errors to the user if we forget, like we
did with DefaultErrnoRet) and the user sees the exact same error when
using a runc version that doesn't support these fields.
[1]: Note we are vendoring version 1c3f411f041711bbeecf35ff7e93461ea6789220 and this version has these 3 fields https://github.com/opencontainers/runtime-spec/blob/1c3f411f041711bbeecf35ff7e93461ea6789220/config-linux.md#seccomp
[2]: https://github.com/opencontainers/runc/pull/2954/
[3]: https://github.com/opencontainers/runc/pull/2682
Signed-off-by: Rodrigo Campos <rodrigo@kinvolk.io>
2021-06-25 13:51:06 +00:00
|
|
|
func TestLoadProfileWithDefaultErrnoRet(t *testing.T) {
|
|
|
|
var profile = []byte(`{
|
|
|
|
"defaultAction": "SCMP_ACT_ERRNO",
|
|
|
|
"defaultErrnoRet": 6
|
|
|
|
}`)
|
|
|
|
rs := createSpec()
|
|
|
|
p, err := LoadProfile(string(profile), &rs)
|
|
|
|
if err != nil {
|
|
|
|
t.Fatal(err)
|
|
|
|
}
|
|
|
|
|
|
|
|
expectedErrnoRet := uint(6)
|
|
|
|
expected := specs.LinuxSeccomp{
|
2021-07-16 13:36:38 +00:00
|
|
|
DefaultAction: specs.ActErrno,
|
seccomp: Sync fields with runtime-spec fields
The runtime spec we are using has support for these 3 fields[1], but
moby doesn't have them in its seccomp struct. This patch just adds and
copies them when they are in the profile.
DefaultErrnoRet is implemented in the runc version moby is using (it is
implemented since runc-rc95[2]) but if we create a container without
this moby patch, we don't see an error nor the expected behavior. This
is not clear for the user (the profile they specify is valid, the syntax
is ok, but the wrong behavior is seen).
This is because the DefaultErrnoRet field is not copied to the config
passed ultimately to runc (i.e. is like the field was not specified).
With this patch, we see the expected behavior.
The other two fileds are in the runtime-spec but not yet in runc (a PR
is open and targets 1.1.0 milestone). However, I took the liberty to
copy them now too for two reasons:
1. If we don't add them now and end up using a runc version that
supports them, then the error that the user will see is not clear at
all:
docker: Error response from daemon: OCI runtime create failed: container_linux.go:380: starting container process caused: listenerPath is not set: unknown.
And it is not obvious to debug for the user, as the field _is_ set in
the profile they specify (just not copied by moby to the profile moby
specifies ultimately to runc).
2. When using a runc without seccomp notify support (like today), the
error we see is the same with and without this moby patch (when using a
seccomp profile with the new fields):
docker: Error response from daemon: OCI runtime create failed: string SCMP_ACT_NOTIFY is not a valid action for seccomp: unknown.
Then, it seems like a clear win to add them now: we don't have to do it
later (that implies not clear errors to the user if we forget, like we
did with DefaultErrnoRet) and the user sees the exact same error when
using a runc version that doesn't support these fields.
[1]: Note we are vendoring version 1c3f411f041711bbeecf35ff7e93461ea6789220 and this version has these 3 fields https://github.com/opencontainers/runtime-spec/blob/1c3f411f041711bbeecf35ff7e93461ea6789220/config-linux.md#seccomp
[2]: https://github.com/opencontainers/runc/pull/2954/
[3]: https://github.com/opencontainers/runc/pull/2682
Signed-off-by: Rodrigo Campos <rodrigo@kinvolk.io>
2021-06-25 13:51:06 +00:00
|
|
|
DefaultErrnoRet: &expectedErrnoRet,
|
|
|
|
}
|
|
|
|
|
|
|
|
assert.DeepEqual(t, expected, *p)
|
|
|
|
}
|
|
|
|
|
|
|
|
func TestLoadProfileWithListenerPath(t *testing.T) {
|
|
|
|
var profile = []byte(`{
|
|
|
|
"defaultAction": "SCMP_ACT_ERRNO",
|
|
|
|
"listenerPath": "/var/run/seccompaget.sock",
|
|
|
|
"listenerMetadata": "opaque-metadata"
|
|
|
|
}`)
|
|
|
|
rs := createSpec()
|
|
|
|
p, err := LoadProfile(string(profile), &rs)
|
|
|
|
if err != nil {
|
|
|
|
t.Fatal(err)
|
|
|
|
}
|
|
|
|
|
|
|
|
expected := specs.LinuxSeccomp{
|
2021-07-16 13:36:38 +00:00
|
|
|
DefaultAction: specs.ActErrno,
|
seccomp: Sync fields with runtime-spec fields
The runtime spec we are using has support for these 3 fields[1], but
moby doesn't have them in its seccomp struct. This patch just adds and
copies them when they are in the profile.
DefaultErrnoRet is implemented in the runc version moby is using (it is
implemented since runc-rc95[2]) but if we create a container without
this moby patch, we don't see an error nor the expected behavior. This
is not clear for the user (the profile they specify is valid, the syntax
is ok, but the wrong behavior is seen).
This is because the DefaultErrnoRet field is not copied to the config
passed ultimately to runc (i.e. is like the field was not specified).
With this patch, we see the expected behavior.
The other two fileds are in the runtime-spec but not yet in runc (a PR
is open and targets 1.1.0 milestone). However, I took the liberty to
copy them now too for two reasons:
1. If we don't add them now and end up using a runc version that
supports them, then the error that the user will see is not clear at
all:
docker: Error response from daemon: OCI runtime create failed: container_linux.go:380: starting container process caused: listenerPath is not set: unknown.
And it is not obvious to debug for the user, as the field _is_ set in
the profile they specify (just not copied by moby to the profile moby
specifies ultimately to runc).
2. When using a runc without seccomp notify support (like today), the
error we see is the same with and without this moby patch (when using a
seccomp profile with the new fields):
docker: Error response from daemon: OCI runtime create failed: string SCMP_ACT_NOTIFY is not a valid action for seccomp: unknown.
Then, it seems like a clear win to add them now: we don't have to do it
later (that implies not clear errors to the user if we forget, like we
did with DefaultErrnoRet) and the user sees the exact same error when
using a runc version that doesn't support these fields.
[1]: Note we are vendoring version 1c3f411f041711bbeecf35ff7e93461ea6789220 and this version has these 3 fields https://github.com/opencontainers/runtime-spec/blob/1c3f411f041711bbeecf35ff7e93461ea6789220/config-linux.md#seccomp
[2]: https://github.com/opencontainers/runc/pull/2954/
[3]: https://github.com/opencontainers/runc/pull/2682
Signed-off-by: Rodrigo Campos <rodrigo@kinvolk.io>
2021-06-25 13:51:06 +00:00
|
|
|
ListenerPath: "/var/run/seccompaget.sock",
|
|
|
|
ListenerMetadata: "opaque-metadata",
|
|
|
|
}
|
|
|
|
|
|
|
|
assert.DeepEqual(t, expected, *p)
|
|
|
|
}
|
|
|
|
|
2021-07-16 15:55:04 +00:00
|
|
|
func TestLoadProfileWithFlag(t *testing.T) {
|
|
|
|
profile := `{"defaultAction": "SCMP_ACT_ERRNO", "flags": ["SECCOMP_FILTER_FLAG_SPEC_ALLOW", "SECCOMP_FILTER_FLAG_LOG"]}`
|
|
|
|
expected := specs.LinuxSeccomp{
|
|
|
|
DefaultAction: specs.ActErrno,
|
|
|
|
Flags: []specs.LinuxSeccompFlag{"SECCOMP_FILTER_FLAG_SPEC_ALLOW", "SECCOMP_FILTER_FLAG_LOG"},
|
|
|
|
}
|
|
|
|
rs := createSpec()
|
|
|
|
p, err := LoadProfile(profile, &rs)
|
|
|
|
assert.NilError(t, err)
|
|
|
|
assert.DeepEqual(t, expected, *p)
|
|
|
|
}
|
|
|
|
|
2021-07-16 13:50:12 +00:00
|
|
|
// TestLoadProfileValidation tests that invalid profiles produce the correct error.
|
|
|
|
func TestLoadProfileValidation(t *testing.T) {
|
|
|
|
tests := []struct {
|
|
|
|
doc string
|
|
|
|
profile string
|
|
|
|
expected string
|
|
|
|
}{
|
|
|
|
{
|
|
|
|
doc: "conflicting architectures and archMap",
|
|
|
|
profile: `{"defaultAction": "SCMP_ACT_ERRNO", "architectures": ["A", "B", "C"], "archMap": [{"architecture": "A", "subArchitectures": ["B", "C"]}]}`,
|
|
|
|
expected: `use either 'architectures' or 'archMap'`,
|
|
|
|
},
|
|
|
|
{
|
|
|
|
doc: "conflicting syscall.name and syscall.names",
|
|
|
|
profile: `{"defaultAction": "SCMP_ACT_ERRNO", "syscalls": [{"name": "accept", "names": ["accept"], "action": "SCMP_ACT_ALLOW"}]}`,
|
|
|
|
expected: `use either 'name' or 'names'`,
|
|
|
|
},
|
|
|
|
}
|
|
|
|
for _, tc := range tests {
|
|
|
|
tc := tc
|
|
|
|
rs := createSpec()
|
|
|
|
t.Run(tc.doc, func(t *testing.T) {
|
|
|
|
_, err := LoadProfile(tc.profile, &rs)
|
|
|
|
assert.ErrorContains(t, err, tc.expected)
|
|
|
|
})
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-09-28 07:50:03 +00:00
|
|
|
// TestLoadLegacyProfile tests loading a seccomp profile in the old format
|
|
|
|
// (before https://github.com/docker/docker/pull/24510)
|
|
|
|
func TestLoadLegacyProfile(t *testing.T) {
|
2021-08-24 10:10:50 +00:00
|
|
|
f, err := os.ReadFile("fixtures/default-old-format.json")
|
2020-09-28 07:50:03 +00:00
|
|
|
if err != nil {
|
|
|
|
t.Fatal(err)
|
|
|
|
}
|
2020-09-28 12:55:28 +00:00
|
|
|
rs := createSpec()
|
2021-07-16 13:50:12 +00:00
|
|
|
p, err := LoadProfile(string(f), &rs)
|
|
|
|
assert.NilError(t, err)
|
|
|
|
assert.Equal(t, p.DefaultAction, specs.ActErrno)
|
|
|
|
assert.DeepEqual(t, p.Architectures, []specs.Arch{"SCMP_ARCH_X86_64", "SCMP_ARCH_X86", "SCMP_ARCH_X32"})
|
|
|
|
assert.Equal(t, len(p.Syscalls), 311)
|
|
|
|
expected := specs.LinuxSyscall{
|
|
|
|
Names: []string{"accept"},
|
|
|
|
Action: specs.ActAllow,
|
|
|
|
Args: []specs.LinuxSeccompArg{},
|
2020-09-28 07:50:03 +00:00
|
|
|
}
|
2021-07-16 13:50:12 +00:00
|
|
|
assert.DeepEqual(t, p.Syscalls[0], expected)
|
2020-09-28 07:50:03 +00:00
|
|
|
}
|
|
|
|
|
2016-02-19 08:22:36 +00:00
|
|
|
func TestLoadDefaultProfile(t *testing.T) {
|
2021-08-24 10:10:50 +00:00
|
|
|
f, err := os.ReadFile("default.json")
|
2016-02-19 08:22:36 +00:00
|
|
|
if err != nil {
|
|
|
|
t.Fatal(err)
|
|
|
|
}
|
2020-09-28 12:55:28 +00:00
|
|
|
rs := createSpec()
|
2016-07-13 13:41:30 +00:00
|
|
|
if _, err := LoadProfile(string(f), &rs); err != nil {
|
2016-01-19 22:57:03 +00:00
|
|
|
t.Fatal(err)
|
|
|
|
}
|
|
|
|
}
|
2020-09-28 12:55:28 +00:00
|
|
|
|
2020-10-01 11:25:37 +00:00
|
|
|
func TestUnmarshalDefaultProfile(t *testing.T) {
|
|
|
|
expected := DefaultProfile()
|
|
|
|
if expected == nil {
|
|
|
|
t.Skip("seccomp not supported")
|
|
|
|
}
|
|
|
|
|
2021-08-24 10:10:50 +00:00
|
|
|
f, err := os.ReadFile("default.json")
|
2020-10-01 11:25:37 +00:00
|
|
|
if err != nil {
|
|
|
|
t.Fatal(err)
|
|
|
|
}
|
|
|
|
var profile Seccomp
|
|
|
|
err = json.Unmarshal(f, &profile)
|
|
|
|
if err != nil {
|
|
|
|
t.Fatal(err)
|
|
|
|
}
|
|
|
|
assert.DeepEqual(t, expected.Architectures, profile.Architectures)
|
|
|
|
assert.DeepEqual(t, expected.ArchMap, profile.ArchMap)
|
|
|
|
assert.DeepEqual(t, expected.DefaultAction, profile.DefaultAction)
|
|
|
|
assert.DeepEqual(t, expected.Syscalls, profile.Syscalls)
|
|
|
|
}
|
|
|
|
|
2020-10-01 10:26:46 +00:00
|
|
|
func TestMarshalUnmarshalFilter(t *testing.T) {
|
|
|
|
t.Parallel()
|
|
|
|
tests := []struct {
|
|
|
|
in string
|
|
|
|
out string
|
|
|
|
error bool
|
|
|
|
}{
|
|
|
|
{in: `{"arches":["s390x"],"minKernel":3}`, error: true},
|
|
|
|
{in: `{"arches":["s390x"],"minKernel":3.12}`, error: true},
|
|
|
|
{in: `{"arches":["s390x"],"minKernel":true}`, error: true},
|
|
|
|
{in: `{"arches":["s390x"],"minKernel":"0.0"}`, error: true},
|
|
|
|
{in: `{"arches":["s390x"],"minKernel":"3"}`, error: true},
|
|
|
|
{in: `{"arches":["s390x"],"minKernel":".3"}`, error: true},
|
|
|
|
{in: `{"arches":["s390x"],"minKernel":"3."}`, error: true},
|
|
|
|
{in: `{"arches":["s390x"],"minKernel":"true"}`, error: true},
|
|
|
|
{in: `{"arches":["s390x"],"minKernel":"3.12.1\""}`, error: true},
|
|
|
|
{in: `{"arches":["s390x"],"minKernel":"4.15abc"}`, error: true},
|
|
|
|
{in: `{"arches":["s390x"],"minKernel":null}`, out: `{"arches":["s390x"]}`},
|
|
|
|
{in: `{"arches":["s390x"],"minKernel":""}`, out: `{"arches":["s390x"],"minKernel":""}`}, // FIXME: try to fix omitempty for this
|
|
|
|
{in: `{"arches":["s390x"],"minKernel":"0.5"}`, out: `{"arches":["s390x"],"minKernel":"0.5"}`},
|
|
|
|
{in: `{"arches":["s390x"],"minKernel":"0.50"}`, out: `{"arches":["s390x"],"minKernel":"0.50"}`},
|
|
|
|
{in: `{"arches":["s390x"],"minKernel":"5.0"}`, out: `{"arches":["s390x"],"minKernel":"5.0"}`},
|
|
|
|
{in: `{"arches":["s390x"],"minKernel":"50.0"}`, out: `{"arches":["s390x"],"minKernel":"50.0"}`},
|
|
|
|
{in: `{"arches":["s390x"],"minKernel":"4.15"}`, out: `{"arches":["s390x"],"minKernel":"4.15"}`},
|
|
|
|
}
|
|
|
|
for _, tc := range tests {
|
|
|
|
tc := tc
|
|
|
|
t.Run(tc.in, func(t *testing.T) {
|
|
|
|
var filter Filter
|
|
|
|
err := json.Unmarshal([]byte(tc.in), &filter)
|
|
|
|
if tc.error {
|
|
|
|
if err == nil {
|
|
|
|
t.Fatal("expected an error")
|
|
|
|
} else if !strings.Contains(err.Error(), "invalid kernel version") {
|
|
|
|
t.Fatal("unexpected error:", err)
|
|
|
|
}
|
|
|
|
return
|
|
|
|
}
|
|
|
|
if err != nil {
|
|
|
|
t.Fatal(err)
|
|
|
|
}
|
|
|
|
out, err := json.Marshal(filter)
|
|
|
|
if err != nil {
|
|
|
|
t.Fatal(err)
|
|
|
|
}
|
|
|
|
if string(out) != tc.out {
|
|
|
|
t.Fatalf("expected %s, got %s", tc.out, string(out))
|
|
|
|
}
|
|
|
|
})
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-09-28 12:55:28 +00:00
|
|
|
func TestLoadConditional(t *testing.T) {
|
2021-08-24 10:10:50 +00:00
|
|
|
f, err := os.ReadFile("fixtures/conditional_include.json")
|
2020-09-28 12:55:28 +00:00
|
|
|
if err != nil {
|
|
|
|
t.Fatal(err)
|
|
|
|
}
|
|
|
|
tests := []struct {
|
|
|
|
doc string
|
|
|
|
cap string
|
|
|
|
expected []string
|
|
|
|
}{
|
|
|
|
{doc: "no caps", expected: []string{"chmod", "ptrace"}},
|
|
|
|
{doc: "with syslog", cap: "CAP_SYSLOG", expected: []string{"chmod", "syslog", "ptrace"}},
|
|
|
|
{doc: "no ptrace", cap: "CAP_SYS_ADMIN", expected: []string{"chmod"}},
|
|
|
|
}
|
|
|
|
|
|
|
|
for _, tc := range tests {
|
|
|
|
tc := tc
|
|
|
|
t.Run(tc.doc, func(t *testing.T) {
|
|
|
|
rs := createSpec(tc.cap)
|
|
|
|
p, err := LoadProfile(string(f), &rs)
|
|
|
|
if err != nil {
|
|
|
|
t.Fatal(err)
|
|
|
|
}
|
|
|
|
if len(p.Syscalls) != len(tc.expected) {
|
|
|
|
t.Fatalf("expected %d syscalls in profile, have %d", len(tc.expected), len(p.Syscalls))
|
|
|
|
}
|
|
|
|
for i, v := range p.Syscalls {
|
|
|
|
if v.Names[0] != tc.expected[i] {
|
|
|
|
t.Fatalf("expected %s syscall, have %s", tc.expected[i], v.Names[0])
|
|
|
|
}
|
|
|
|
}
|
|
|
|
})
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// createSpec() creates a minimum spec for testing
|
|
|
|
func createSpec(caps ...string) specs.Spec {
|
|
|
|
rs := specs.Spec{
|
|
|
|
Process: &specs.Process{
|
|
|
|
Capabilities: &specs.LinuxCapabilities{},
|
|
|
|
},
|
|
|
|
}
|
|
|
|
if caps != nil {
|
|
|
|
rs.Process.Capabilities.Bounding = append(rs.Process.Capabilities.Bounding, caps...)
|
|
|
|
}
|
|
|
|
return rs
|
|
|
|
}
|