Skip to content

Commit 4363011

Browse files
shailend-ggvisor-bot
authored andcommitted
Support privileged executables inside the sandbox
There is already some support for privileged executables before this change: file capabilities were taken into account when creating new processes inside the sandbox from external instigation (but not during execve(2)). set-user-id and set-group-id bits were never taken into account, and the Sentry behaved as if NO_NEW_PRIVS is always true. After this change, execve(2) and the external Exec entry point both take the set-user(group)-id bits and file capabilities into account. Because NO_NEW_PRIVS can no longer be assumed to be always set, a couple of extra checks to prevent privilege elevation during execve(2) are needed: the presence of a ptracer and the use of a shared fsContext. Moreover, execve had to be serialized with seccomp(2) when it is syncing filters to all threads in a group. Serializing execve with PTRACE_ATTACH and seccomp required some Task runState gymnastics. Task dumpability is reset when privilege elevation takes place and privilege elevation is denied for binaries living on nosuid mounts. To fix an old bug where some aux vectors were populated using data from the old pre-execve creds, the computation of the (file-privilege-augmented) creds during execve now takes place before the stack for the new program is set up. gvisor users who thus far have exec'd into the sandbox with UID as (sandbox) root will lose the caps an all-powerful namespace root'd have if: 1) the binary is a suid/guid binary 2) the owner of the binary is host root and there is no mapping for host root in the host user namespace of the sandbox. To avoid this surprise, the suid/sgid bits are ignored by default. Users can opt-in via the --allow-suid flag. One result of this change is that sudo now works inside the sandbox. PiperOrigin-RevId: 794414101
1 parent 0407777 commit 4363011

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

46 files changed

+1529
-408
lines changed

Makefile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -145,7 +145,7 @@ endif
145145
# Configure helpers for below.
146146
configure_noreload = \
147147
$(call header,CONFIGURE $(1)$(RUNTIME_BIN) $(RUNTIME_ARGS) $(2)); \
148-
sudo $(RUNTIME_BIN) install --config_file="$(DOCKER_DAEMON_CONFIG_PATH)" --experimental=true --runtime="$(1)" -- $(RUNTIME_ARGS) --debug-log "$(RUNTIME_LOGS)" $(2) && \
148+
sudo $(RUNTIME_BIN) install --config_file="$(DOCKER_DAEMON_CONFIG_PATH)" --experimental=true --runtime="$(1)" -- $(RUNTIME_ARGS) --debug-log "$(RUNTIME_LOGS)" --allow-suid $(2) && \
149149
sudo rm -rf "$(RUNTIME_LOG_DIR)" && mkdir -p "$(RUNTIME_LOG_DIR)"
150150

151151
reload_docker = \

images/basic/sudo/Dockerfile

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
FROM ubuntu:24.04
2+
3+
RUN apt-get update && apt-get upgrade -y
4+
RUN apt-get install -y strace sudo util-linux libcap2-bin
5+
6+
RUN groupadd container_admins
7+
RUN useradd -m alice -G container_admins
8+
RUN useradd -m bob
9+
RUN echo '%container_admins ALL=(ALL) NOPASSWD:ALL' >> /etc/sudoers
10+
11+
USER alice

pkg/sentry/control/proc.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,9 @@ type ExecArgs struct {
123123
// ExtraKGIDs is the list of additional groups to which the user belongs.
124124
ExtraKGIDs []auth.KGID
125125

126+
// NoNewPrivileges disallows the new process from acquiring new privileges.
127+
NoNewPrivileges bool
128+
126129
// Capabilities is the list of capabilities to give to the process.
127130
Capabilities *auth.TaskCapabilities
128131

@@ -204,6 +207,7 @@ func (proc *Proc) execAsync(args *ExecArgs) (*kernel.ThreadGroup, kernel.ThreadI
204207
WorkingDirectory: args.WorkingDirectory,
205208
MountNamespace: args.MountNamespace,
206209
Credentials: creds,
210+
NoNewPrivs: args.NoNewPrivileges,
207211
Umask: 0022,
208212
Limits: limitSet,
209213
MaxSymlinkTraversals: linux.MaxSymlinkTraversals,

pkg/sentry/kernel/auth/capability_set.go

Lines changed: 143 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,11 @@ func (cs *CapabilitySet) Clear(cp linux.Capability) {
5353
*cs &= ^CapabilitySetOf(cp)
5454
}
5555

56+
// IsSubsetOf returns true if the given capability set is a subset of "super".
57+
func (cs *CapabilitySet) IsSubsetOf(super CapabilitySet) bool {
58+
return *cs&super == *cs
59+
}
60+
5661
// VfsCapDataOf returns a VfsCapData containing the file capabilities for the given slice of bytes.
5762
// For each field of the cap data, which are in the structure of either vfs_cap_data or vfs_ns_cap_data,
5863
// the bytes are ordered in little endian.
@@ -78,30 +83,6 @@ func VfsCapDataOf(data []byte) (linux.VfsNsCapData, error) {
7883
return capData, nil
7984
}
8085

81-
// HandleVfsCaps updates creds based on the given vfsCaps. It returns two
82-
// booleans; the first indicates whether the effective flag is set, and the second
83-
// second indicates whether the file capability is applied.
84-
func HandleVfsCaps(vfsCaps linux.VfsNsCapData, creds *Credentials) (bool, bool, error) {
85-
// gVisor does not support ID-mapped mounts and all filesystems are owned by
86-
// the initial user namespace. So we an directly cast the root ID to KUID.
87-
rootID := KUID(vfsCaps.RootID)
88-
if !rootIDOwnsCurrentUserns(creds, rootID) {
89-
// Linux skips vfs caps in this situation.
90-
return false, false, nil
91-
}
92-
// Note that ambient capabilities are not yet supported in gVisor.
93-
// P'(permitted) = (P(inheritable) & F(inheritable)) | (F(permitted) & P(bounding)) | P'(ambient)
94-
creds.PermittedCaps = (CapabilitySet(vfsCaps.Permitted()) & creds.BoundingCaps) |
95-
(CapabilitySet(vfsCaps.Inheritable()) & creds.InheritableCaps)
96-
effective := (vfsCaps.MagicEtc & linux.VFS_CAP_FLAGS_EFFECTIVE) > 0
97-
// Insufficient to execute correctly. Linux only returns EPERM when effective
98-
// flag is set.
99-
if effective && (CapabilitySet(vfsCaps.Permitted()) & ^creds.PermittedCaps) != 0 {
100-
return effective, true, linuxerr.EPERM
101-
}
102-
return effective, true, nil
103-
}
104-
10586
// FixupVfsCapDataOnSet may convert the given value to v3 file capabilities. It
10687
// is analogous to security/commoncap.c:cap_convert_nscap().
10788
func FixupVfsCapDataOnSet(creds *Credentials, value string, kuid KUID, kgid KGID) (string, error) {
@@ -173,49 +154,157 @@ func rootIDOwnsCurrentUserns(creds *Credentials, rootID KUID) bool {
173154
return false
174155
}
175156

176-
// HandlePrivilegedRoot updates creds for a privileged root user as per
177-
// `Capabilities and execution of programs by root` in capabilities(7).
178-
// It returns true if the file effective bit should be considered set.
179-
func HandlePrivilegedRoot(creds *Credentials, hasVFSCaps bool, filename string) bool {
157+
// FilePrivileges contains the file privileges for a file.
158+
type FilePrivileges struct {
159+
// SetUserID, when not NoID, indicates that the file has the setuid bit set. It is the KUID of the
160+
// owner of the file.
161+
SetUserID KUID
162+
163+
// SetGroupID, when not NoID, indicates that the file has the setgid bit set. It is the KGID of
164+
// the owning group of the file.
165+
SetGroupID KGID
166+
167+
// HasCaps indicates whether the file has capabilities attached.
168+
HasCaps bool
169+
170+
// CapRootID is the KUID of the namespace root of the Task that created the file caps.
171+
CapRootID KUID
172+
173+
// "These capabilities are automatically permitted to the thread, regardless of the thread's
174+
// inheritable capabilities." - capabilities(7).
175+
PermittedCaps CapabilitySet
176+
177+
// "This set is ANDed with the thread's inheritable set to determine which inheritable capabilities
178+
// are enabled in the permitted set of the thread after the execve(2)." - capabilities(7).
179+
InheritableCaps CapabilitySet
180+
181+
// "Determines if all of the new permitted capabilities for the thread are also raised in the
182+
// effective set." - capabilities(7).
183+
Effective bool
184+
}
185+
186+
// handlePrivilegedRoot updates creds for a privileged root user as per
187+
// "Capabilities and execution of programs by root" in capabilities(7).
188+
func handlePrivilegedRoot(c *Credentials, f *FilePrivileges, filename string) {
180189
// gVisor currently does not support SECURE_NOROOT secure bit since
181190
// PR_SET_SECUREBITS is not supported. So no need to check here.
182-
root := creds.UserNamespace.MapToKUID(RootUID)
183-
if hasVFSCaps && creds.RealKUID != root && creds.EffectiveKUID == root {
191+
root := c.UserNamespace.MapToKUID(RootUID)
192+
193+
// "If (a) the binary that is being executed has capabilities attached and (b) the real user ID of
194+
// the process is not 0 (root) and (c) the effective user ID of the process is 0 (root), then the
195+
// file capability bits are honored. (i.e., they are not notionally considered to be all ones)."
196+
// - capabilities(7)
197+
if f.HasCaps && c.RealKUID != root && c.EffectiveKUID == root {
184198
log.Warningf("File %q has both SUID bit and file capabilities set, not raising all capabilities.", filename)
185-
return false
199+
return
186200
}
187-
if creds.RealKUID == root || creds.EffectiveKUID == root {
201+
202+
// "If the real or effective user ID of the process is 0 (root), then the file inheritable and
203+
// permitted sets are ignored; instead they are notionally considered to be all ones (i.e., all
204+
// capabilities enabled)." - capabilities(7)
205+
if c.RealKUID == root || c.EffectiveKUID == root {
188206
// P'(permitted) = P(inheritable) | P(bounding)
189-
creds.PermittedCaps = creds.BoundingCaps | creds.InheritableCaps
207+
c.PermittedCaps = c.BoundingCaps | c.InheritableCaps
190208
}
191-
// Linux only sets the effective bit if the effective KUID is root.
192-
return creds.EffectiveKUID == root
209+
210+
// "If the effective user ID of the process is 0 (root) or the file effective bit is in fact
211+
// enabled, then the file effective bit is notionally defined to be one (enabled)." - capabilities(7)
212+
f.Effective = c.EffectiveKUID == root || f.Effective
193213
}
194214

195-
// UpdateCredsForNewTask updates creds for a new task as per capabilities(7).
196-
func UpdateCredsForNewTask(creds *Credentials, fileCaps string, filename string) error {
197-
// Clear the permitted capability set. It is initialized below via
198-
// HandleVfsCaps() and HandlePrivilegedRoot().
199-
creds.PermittedCaps = 0
200-
hasVFSCaps := false
201-
setEffective := false
202-
if len(fileCaps) != 0 {
203-
vfsCaps, err := VfsCapDataOf([]byte(fileCaps))
204-
if err != nil {
205-
return err
215+
// ComputeCredsForExec computes the new credentials given the file privileges.
216+
// It returns the new creds and a bool indicating if the task is executing with
217+
// elevated privileges. A few words about the arguments:
218+
// - c: The current credentials of the task.
219+
// - f: The file privileges of the executable.
220+
// - filename: The name of the executable, used for logging.
221+
// - noNewPrivs: The current state of the prctl NO_NEW_PRIVS.
222+
// - stopPrivGain: Determines if privilege gain should be stopped for reasons beyond NO_NEW_PRIVS.
223+
// Both noNewPrivs and stopPrivGain prevent cap gain, but stopPrivGain does not by itself
224+
// prevent ID gain.
225+
// - allowSUID: If true, the task will be allowed to setuid.
226+
// Both noNewPrivs and allowSUID prevent ID gain, but allowSUID does not by itself prevent cap
227+
// gain. Note also that while noNewPrivs brings down the effective IDs down to the real IDs,
228+
// allowSUID at most prevents further ID gain due the SUID/GID bits.
229+
//
230+
// Note that gVisor does not support Ambient capabilities.
231+
func ComputeCredsForExec(c *Credentials, f FilePrivileges, filename string,
232+
noNewPrivs bool, stopPrivGain bool, allowSUID bool) (*Credentials, bool, error) {
233+
if noNewPrivs || !allowSUID {
234+
f.SetUserID = NoID
235+
f.SetGroupID = NoID
236+
}
237+
// "...if either the user or the group ID of the file has no mapping inside the namespace, the
238+
// set-user-ID (set-group-ID) bit is silently ignored: the new program is executed, but the
239+
// process's effective user (group) ID is left unchanged." - user_namespaces(7).
240+
if !f.SetUserID.In(c.UserNamespace).Ok() {
241+
f.SetUserID = NoID
242+
}
243+
if !f.SetGroupID.In(c.UserNamespace).Ok() {
244+
f.SetGroupID = NoID
245+
}
246+
// "...capabilities are conferred only if the binary is executed by a process that resides in a
247+
// user namespace whose UID 0 maps to the root user ID that is saved in the extended attribute,
248+
// or when executed by a process that resides in a descendant of such a namespace."
249+
// - capabilities(7).
250+
if !rootIDOwnsCurrentUserns(c, f.CapRootID) {
251+
f.HasCaps = false
252+
f.Effective = false
253+
}
254+
255+
newC := c.Fork()
256+
if f.SetUserID.Ok() {
257+
newC.EffectiveKUID = f.SetUserID
258+
}
259+
if f.SetGroupID.Ok() {
260+
newC.EffectiveKGID = f.SetGroupID
261+
}
262+
263+
newC.PermittedCaps = CapabilitySet(0)
264+
if f.HasCaps {
265+
// P'(permitted) = (P(inheritable) & F(inheritable)) | (F(permitted) & P(bounding))
266+
newC.PermittedCaps = (c.InheritableCaps & f.InheritableCaps) | (f.PermittedCaps & c.BoundingCaps)
267+
268+
// The "Safety checking for capability-dumb binaries" section of capabilities(7) says:
269+
// "...For such applications, the effective capability bit is set on the file...
270+
// ...If the process did not obtain the full set of file permitted capabilities,
271+
// then execve(2) fails with the error EPERM."
272+
if f.Effective && (newC.PermittedCaps&f.PermittedCaps != f.PermittedCaps) {
273+
return nil, false, linuxerr.EPERM
206274
}
207-
setEffective, hasVFSCaps, err = HandleVfsCaps(vfsCaps, creds)
208-
if err != nil {
209-
return err
275+
}
276+
// newC.PermittedCaps and f.Effective are set differently for namespace root.
277+
handlePrivilegedRoot(newC, &f, filename)
278+
279+
// Deny privilege elevation if we have to, see commoncap.c:cap_bprm_creds_from_file().
280+
gainedID := (newC.EffectiveKUID != c.RealKUID) || (newC.EffectiveKGID != c.RealKGID)
281+
gainedCaps := !newC.PermittedCaps.IsSubsetOf(c.PermittedCaps)
282+
if (gainedID || gainedCaps) && (noNewPrivs || stopPrivGain) {
283+
if noNewPrivs || !c.HasCapability(linux.CAP_SETUID) {
284+
newC.EffectiveKUID = c.RealKUID
285+
newC.EffectiveKGID = c.RealKGID
210286
}
287+
newC.PermittedCaps &= c.PermittedCaps
211288
}
212-
setEffective = HandlePrivilegedRoot(creds, hasVFSCaps, filename) || setEffective
289+
newC.SavedKUID = newC.EffectiveKUID
290+
newC.SavedKGID = newC.EffectiveKGID
291+
213292
// P'(effective) = effective ? P'(permitted) : P'(ambient).
214-
creds.EffectiveCaps = 0
215-
if setEffective {
216-
creds.EffectiveCaps = creds.PermittedCaps
293+
newC.EffectiveCaps = 0
294+
if f.Effective {
295+
newC.EffectiveCaps = newC.PermittedCaps
296+
}
297+
298+
// prctl(2): The "keep capabilities" value will be reset to 0 on subsequent calls to execve(2).
299+
newC.KeepCaps = false
300+
301+
root := c.UserNamespace.MapToKUID(RootUID)
302+
secureExec := false
303+
// See commoncap.c:cap_bprm_secureexec() in Linux 4.2 (before the introduction of ambient caps).
304+
if gainedID || (newC.RealKUID != root && (f.Effective || newC.PermittedCaps != CapabilitySet(0))) {
305+
secureExec = true
217306
}
218-
return nil
307+
return newC, secureExec, nil
219308
}
220309

221310
// TaskCapabilities represents all the capability sets for a task. Each of these

pkg/sentry/kernel/auth/capability_set_test.go

Lines changed: 45 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -23,12 +23,10 @@ import (
2323

2424
// credentialsWithCaps creates a credentials object with the given capabilities.
2525
func credentialsWithCaps(inheritable, bounding CapabilitySet) *Credentials {
26-
creds := NewRootCredentials(NewRootUserNamespace())
27-
creds.PermittedCaps = 0
28-
creds.InheritableCaps = inheritable
29-
creds.EffectiveCaps = 0
30-
creds.BoundingCaps = bounding
31-
return creds
26+
return NewUserCredentials(1001, 1001, nil, &TaskCapabilities{
27+
InheritableCaps: inheritable,
28+
BoundingCaps: bounding,
29+
}, NewRootUserNamespace())
3230
}
3331

3432
func vfsNsCapDataFrom(effective bool, rootid uint32, permitted, inheritable CapabilitySet) linux.VfsNsCapData {
@@ -54,70 +52,85 @@ func vfsCapDataFrom(effective bool, permitted, inheritable CapabilitySet) linux.
5452
return capData
5553
}
5654

57-
func TestCapsFromVfsCaps(t *testing.T) {
55+
func TestComputeCredsForExec(t *testing.T) {
5856
for _, tst := range []struct {
5957
name string
60-
capData linux.VfsNsCapData
58+
filePrivs FilePrivileges
6159
creds *Credentials
60+
noNewPrivs bool
61+
stopPrivGain bool
62+
allowSUID bool
6263
wantPermitted CapabilitySet
6364
wantEffective bool
6465
wantErr error
6566
}{
6667
{
6768
name: "TestSamePermittedAndInheritableCaps",
68-
capData: vfsCapDataFrom(
69-
true, // effective
70-
CapabilitySetOf(linux.CAP_NET_ADMIN), // permitted
71-
CapabilitySetOf(linux.CAP_NET_ADMIN)), // inheritable
69+
filePrivs: FilePrivileges{
70+
HasCaps: true,
71+
Effective: true,
72+
PermittedCaps: CapabilitySetOf(linux.CAP_NET_ADMIN),
73+
InheritableCaps: CapabilitySetOf(linux.CAP_NET_ADMIN),
74+
},
7275
creds: credentialsWithCaps(AllCapabilities, AllCapabilities),
7376
wantPermitted: CapabilitySetOf(linux.CAP_NET_ADMIN),
7477
wantEffective: true,
7578
},
7679
{
7780
name: "TestDifferentPermittedAndInheritableCaps",
78-
capData: vfsCapDataFrom(
79-
true, // effective
80-
CapabilitySetOfMany([]linux.Capability{linux.CAP_CHOWN, linux.CAP_SETUID}), // permitted
81-
CapabilitySetOfMany([]linux.Capability{linux.CAP_CHOWN, linux.CAP_SETGID})), // inheritable
81+
filePrivs: FilePrivileges{
82+
HasCaps: true,
83+
Effective: true,
84+
PermittedCaps: CapabilitySetOfMany([]linux.Capability{linux.CAP_CHOWN, linux.CAP_SETUID}),
85+
InheritableCaps: CapabilitySetOfMany([]linux.Capability{linux.CAP_CHOWN, linux.CAP_SETGID}),
86+
},
8287
creds: credentialsWithCaps(AllCapabilities, AllCapabilities),
8388
wantPermitted: CapabilitySetOfMany([]linux.Capability{linux.CAP_CHOWN, linux.CAP_SETUID, linux.CAP_SETGID}),
8489
wantEffective: true,
8590
},
8691
{
8792
name: "TestEffectiveBitOff",
88-
capData: vfsCapDataFrom(
89-
false, // effective
90-
CapabilitySetOfMany([]linux.Capability{linux.CAP_CHOWN, linux.CAP_SETUID}), // permitted
91-
CapabilitySetOfMany([]linux.Capability{linux.CAP_CHOWN, linux.CAP_SETGID})), // inheritable
93+
filePrivs: FilePrivileges{
94+
HasCaps: true,
95+
Effective: false,
96+
PermittedCaps: CapabilitySetOfMany([]linux.Capability{linux.CAP_CHOWN, linux.CAP_SETUID}),
97+
InheritableCaps: CapabilitySetOfMany([]linux.Capability{linux.CAP_CHOWN, linux.CAP_SETGID}),
98+
},
9299
creds: credentialsWithCaps(AllCapabilities, AllCapabilities),
93100
wantPermitted: CapabilitySetOfMany([]linux.Capability{linux.CAP_CHOWN, linux.CAP_SETUID, linux.CAP_SETGID}),
94101
wantEffective: false,
95102
},
96103
{
97104
name: "TestInsufficientCaps",
98-
capData: vfsCapDataFrom(
99-
true, // effective
100-
CapabilitySetOfMany([]linux.Capability{linux.CAP_CHOWN, linux.CAP_SETUID}), // permitted
101-
CapabilitySetOf(linux.CAP_CHOWN)), // inheritable
105+
filePrivs: FilePrivileges{
106+
HasCaps: true,
107+
Effective: true,
108+
PermittedCaps: CapabilitySetOfMany([]linux.Capability{linux.CAP_CHOWN, linux.CAP_SETUID}),
109+
InheritableCaps: CapabilitySetOf(linux.CAP_CHOWN),
110+
},
102111
creds: credentialsWithCaps(AllCapabilities, CapabilitySetOf(linux.CAP_CHOWN)),
103112
wantErr: linuxerr.EPERM,
104113
},
105114
} {
106115
t.Run(tst.name, func(t *testing.T) {
107-
setEff, _, err := HandleVfsCaps(tst.capData, tst.creds)
116+
newC, _, err := ComputeCredsForExec(tst.creds, tst.filePrivs, "", tst.noNewPrivs, tst.stopPrivGain, tst.allowSUID)
108117
if err == nil {
109118
if tst.wantErr != nil {
110-
t.Errorf("CapsFromVfsCaps(%v) returned unexpected error %v", tst.capData, tst.wantErr)
119+
t.Errorf("ComputeCredsForExec(%v) returned unexpected error %v", tst.filePrivs, tst.wantErr)
111120
}
112-
if tst.creds.PermittedCaps != tst.wantPermitted {
113-
t.Errorf("CapsFromVfsCaps(%v) set PermittedCaps to: %#x, want capabilities: %#x",
114-
tst.capData, tst.creds.PermittedCaps, tst.wantPermitted)
121+
if newC.PermittedCaps != tst.wantPermitted {
122+
t.Errorf("ComputeCredsForExec(%v) set PermittedCaps to: %#x, want capabilities: %#x",
123+
tst.filePrivs, newC.PermittedCaps, tst.wantPermitted)
115124
}
116-
if setEff != tst.wantEffective {
117-
t.Errorf("CapsFromVfsCaps(%v) returned effective=%t, want: %t", tst.capData, setEff, tst.wantEffective)
125+
if tst.wantEffective && newC.EffectiveCaps != newC.PermittedCaps {
126+
t.Errorf("ComputeCredsForExec(%v) did not set effective caps", tst.filePrivs)
127+
}
128+
if !tst.wantEffective && newC.EffectiveCaps != CapabilitySet(0) {
129+
t.Errorf("ComputeCredsForExec(%v) did not clear effective caps: %#x",
130+
tst.filePrivs, newC.EffectiveCaps)
118131
}
119132
} else if tst.wantErr == nil || tst.wantErr.Error() != err.Error() {
120-
t.Errorf("CapsFromVfsCaps(%v) returned error %v, wantErr: %v", tst.capData, err, tst.wantErr)
133+
t.Errorf("ComputeCredsForExec(%v) returned error %v, wantErr: %v", tst.filePrivs, err, tst.wantErr)
121134
}
122135
})
123136
}

0 commit comments

Comments
 (0)