Skip to contentSkip to conversation

Fix user data corrupted by old value return of sysctl

PATCH In review Yi Yang 6 participants 10 messages Dec 30, 2005 – Jan 4, 2006 Proc sysctl

lore
Share & tools
lore

Yi Yang

author
#0 lore raw

PATCH 1/1 Fix user data corrupted by old value return of sysctl

If the user reads a sysctl entry which is of string type
by sysctl syscall, this call probably corrupts the user data right after the old value buffer, the issue lies in sysctl_string seting 0 to oldval[len], len is the available buffer size specified by the user, obviously, this will write to the first byte of the user memory place immediate after the old value buffer
, the correct way is that sysctl_string doesn't set 0, the user should do it by self in the program.

The following program verifies this point:

#include <linux/unistd.h>
#include <linux/types.h>
#include <linux/sysctl.h>
#include <errno.h>

_syscall1(int, _sysctl, struct __sysctl_args *, args); int sysctl(int *name, int nlen, void *oldval, size_t *oldlenp,
void *newval, size_t newlen)

{
        struct __sysctl_args args
		= {name,nlen,oldval,oldlenp,newval,newlen};

        return _sysctl(&args);
}

#define SIZE(x) sizeof(x)/sizeof(x[0])
#define OSNAMESZ 4

struct mystruct {
	char osname[OSNAMESZ];
	int target;
	int osnamelth;
} myos;

int name[] = { CTL_KERN, KERN_NODENAME };

int main(int argc, char * argv[])

{
	myos.target = 1;
	printf("target = %d\n", myos.target);
	myos.osnamelth = SIZE(myos.osname);
        if (sysctl(name, SIZE(name), myos.osname,
			&myos.osnamelth, 0, 0))
                perror("sysctl");
	else {
                printf("Current host name: %s\n", myos.osname);
	}
	printf("target = %d\n", myos.target);
        return 0;
}

Copy it to file sysctl-safe.c, then

$ hostname
mylocalmachine
$ gcc sysctl-safe.c
$ ./a.out
target = 1
Current host name: mylo
target = 0
$

After apply this patch:

$ hostname
mylocalmachine
$ gcc sysctl-safe.c
$ ./a.out
target = 1
Current host name: mylo
target = 1
kernel/sysctl.c renamed from kernel/sysctl.c.orig +0 -2
@@ -2207,8 +2207,6 @@ int sysctl_string(ctl_table *table, int
 				len = table->maxlen;
 			if(copy_to_user(oldval, table->data, len))
 				return -EFAULT;
-			if(put_user(0, ((char __user *) oldval) + len))
-				return -EFAULT;
 			if(put_user(len, oldlenp))
 				return -EFAULT;
 		}
#1 lore raw
Quoted 9 lines · Yi Yang

If the user reads a sysctl entry which is of string type by sysctl syscall, this call probably corrupts the user data right after the old value buffer, the issue lies in sysctl_string seting 0 to oldval[len], len is the available buffer size specified by the user, obviously, this will write to the first byte of the user memory place immediate after the old value buffer, the correct way is that sysctl_string doesn't set 0, the user should do it by self in the program.

Hmm.. I think this patch is incomplete.

We _should_ zero-pad the data, at least if the result fits in the buffer.

So I think the correct fix is to just _copy_ the last zero if it fits in the buffer, rather than do the unconditional "add NUL at the end" thing. The simplest way to do that is to just make "l" be "strlen(str)+1", so that we count the ending NUL in the length (and then, if the buffer isn't big enough, we will truncate it).

In other words, I would instead suggest a patch like the appended.

But even that is questionable: one alternative is to always zero-pad (like we used to), but make sure that the buffer size is sufficient for it (ie instead of adding one to the length of the string, we'd subtract one from the buffer length and make sure that the '\0' fits..

Comments?

Linus
---

kernel/sysctl.c +1 -3
index 9990e10..ad0425a 100644
@@ -2201,14 +2201,12 @@ int sysctl_string(ctl_table *table, int 
 		if (get_user(len, oldlenp))
 			return -EFAULT;
 		if (len) {
-			l = strlen(table->data);
+			l = strlen(table->data)+1;
 			if (len > l) len = l;
 			if (len >= table->maxlen)
 				len = table->maxlen;
 			if(copy_to_user(oldval, table->data, len))
 				return -EFAULT;
-			if(put_user(0, ((char __user *) oldval) + len))
-				return -EFAULT;
 			if(put_user(len, oldlenp))
 				return -EFAULT;
 		}
#5 lore raw
In reply to Yi Yang
Quoted 8 lines · Yi Yang

If the user reads a sysctl entry which is of string type
by sysctl syscall, this call probably corrupts the user data right after the old value buffer, the issue lies in sysctl_string seting 0 to oldval[len], len is the available buffer size specified by the user, obviously, this will write to the first byte of the user memory place immediate after the old value buffer
, the correct way is that sysctl_string doesn't set 0, the user should do it by self in the program.

That's not just data corruption -- it's also a buffer overrun. Granted, it's "only" a one-byte overrun, but I have seen one-byte overruns be exploitable occasionally in the past. So this sounds to me like a potential security issue, too.

author
#2 lore raw
In reply to Linus Torvalds
Quoted 18 lines · Linus Torvalds
Quoted 10 lines · Yi Yang

If the user reads a sysctl entry which is of string type by sysctl syscall, this call probably corrupts the user data right after the old value buffer, the issue lies in sysctl_string seting 0 to oldval[len], len is the available buffer size specified by the user, obviously, this will write to the first byte of the user memory place immediate after the old value buffer, the correct way is that sysctl_string doesn't set 0, the user should do it by self in the program.

Hmm.. I think this patch is incomplete.

We _should_ zero-pad the data, at least if the result fits in the buffer.

Quoted 9 lines

So I think the correct fix is to just _copy_ the last zero if it fits in the buffer, rather than do the unconditional "add NUL at the end" thing. The simplest way to do that is to just make "l" be "strlen(str)+1", so that we count the ending NUL in the length (and then, if the buffer isn't big enough, we will truncate it).

In other words, I would instead suggest a patch like the appended.

But even that is questionable: one alternative is to always zero-pad (like we used to), but make sure that the buffer size is sufficient for it (ie instead of adding one to the length of the string, we'd subtract one from the buffer length and make sure that the '\0' fits..

Comments?

Yes, you are more complete, I agree with it very much.

Quoted 25 lines

Linus
---

kernel/sysctl.c +1 -3
index 9990e10..ad0425a 100644
@@ -2201,14 +2201,12 @@ int sysctl_string(ctl_table *table, int 
 		if (get_user(len, oldlenp))
 			return -EFAULT;
 		if (len) {
-			l = strlen(table->data);
+			l = strlen(table->data)+1;
 			if (len > l) len = l;
 			if (len >= table->maxlen)
 				len = table->maxlen;
 			if(copy_to_user(oldval, table->data, len))
 				return -EFAULT;
-			if(put_user(0, ((char __user *) oldval) + len))
-				return -EFAULT;
 			if(put_user(len, oldlenp))
 				return -EFAULT;
 		}
#6 lore raw
In reply to Yi Yang

2005/12/30, Yi Yang <[email protected]>:

Quoted 62 lines

If the user reads a sysctl entry which is of string type
by sysctl syscall, this call probably corrupts the user data right after the old value buffer, the issue lies in sysctl_string seting 0 to oldval[len], len is the available buffer size specified by the user, obviously, this will write to the first byte of the user memory place immediate after the old value buffer
, the correct way is that sysctl_string doesn't set 0, the user should do it by self in the program.

The following program verifies this point:

#include <linux/unistd.h>
#include <linux/types.h>
#include <linux/sysctl.h>
#include <errno.h>

_syscall1(int, _sysctl, struct __sysctl_args *, args); int sysctl(int *name, int nlen, void *oldval, size_t *oldlenp,
void *newval, size_t newlen)

{
         struct __sysctl_args args
                = {name,nlen,oldval,oldlenp,newval,newlen};

         return _sysctl(&args);
}

#define SIZE(x) sizeof(x)/sizeof(x[0])
#define OSNAMESZ 4

struct mystruct {
        char osname[OSNAMESZ];
        int target;
        int osnamelth;
} myos;

int name[] = { CTL_KERN, KERN_NODENAME };

int main(int argc, char * argv[])

{
        myos.target = 1;
        printf("target = %d\n", myos.target);
        myos.osnamelth = SIZE(myos.osname);
         if (sysctl(name, SIZE(name), myos.osname,
                        &myos.osnamelth, 0, 0))
                 perror("sysctl");
        else {
                 printf("Current host name: %s\n", myos.osname);
        }
        printf("target = %d\n", myos.target);
         return 0;
}

Copy it to file sysctl-safe.c, then

$ hostname
mylocalmachine
$ gcc sysctl-safe.c
$ ./a.out
target = 1
Current host name: mylo
target = 0
$
Quoted 7 lines

After apply this patch:

$ hostname
mylocalmachine
$ gcc sysctl-safe.c
$ ./a.out
target = 1
Current host name: mylo

You didn't set the trailing '\0', I wonder how your printf did work properly ever. You've just been lucky or something.

-- Coywolf

Quoted 11 lines

target = 1

kernel/sysctl.c 2005-12-30 15:58:15.000000000 +0000 int sysctl_string(ctl_table *table, int · line 2212

Quoted 8 lines
kernel/sysctl.c 2005-12-30 15:58:15.000000000 +0000 renamed from kernel/sysctl.c.orig 2005-12-30 09:21:34.000000000 +0000 +0 -2
@@ -2207,8 +2207,6 @@ int sysctl_string(ctl_table *table, int
                                len = table->maxlen;
                        if(copy_to_user(oldval, table->data, len))
                                return -EFAULT;
-                       if(put_user(0, ((char __user *) oldval) + len))
-                               return -EFAULT;
                        if(put_user(len, oldlenp))
                                return -EFAULT;
                }

--
Coywolf Qi Hunt

#3 lore raw
In reply to Linus Torvalds
Quoted 23 lines · Linus Torvalds
Quoted 9 lines · Yi Yang

If the user reads a sysctl entry which is of string type by sysctl syscall, this call probably corrupts the user data right after the old value buffer, the issue lies in sysctl_string seting 0 to oldval[len], len is the available buffer size specified by the user, obviously, this will write to the first byte of the user memory place immediate after the old value buffer, the correct way is that sysctl_string doesn't set 0, the user should do it by self in the program.

Hmm.. I think this patch is incomplete.

We _should_ zero-pad the data, at least if the result fits in the buffer.

So I think the correct fix is to just _copy_ the last zero if it fits in the buffer, rather than do the unconditional "add NUL at the end" thing. The simplest way to do that is to just make "l" be "strlen(str)+1", so that we count the ending NUL in the length (and then, if the buffer isn't big enough, we will truncate it).

Quoted 8 lines

In other words, I would instead suggest a patch like the appended.

But even that is questionable: one alternative is to always zero-pad (like we used to), but make sure that the buffer size is sufficient for it (ie instead of adding one to the length of the string, we'd subtract one from the buffer length and make sure that the '\0' fits..

Comments?

Always do zero-pad please. I'd feel more comfortable with C strings (NULL terminated). Don't you?

Poor code, also a small cleanup attached.

---

kernel/sysctl.c +1 -1
index e5102ea..9960a26 100644
@@ -2203,7 +2203,7 @@ int sysctl_string(ctl_table *table, int 
 		if (len) {
 			l = strlen(table->data)+1;
 			if (len > l) len = l;
-			if (len >= table->maxlen)
+			if (len > table->maxlen)
 				len = table->maxlen;
 			if(copy_to_user(oldval, table->data, len))
 				return -EFAULT;
author
#7 lore raw
In reply to Coywolf Qi Hunt
Quoted 75 lines · Coywolf Qi Hunt

2005/12/30, Yi Yang <[email protected]>:

Quoted 62 lines

If the user reads a sysctl entry which is of string type
by sysctl syscall, this call probably corrupts the user data right after the old value buffer, the issue lies in sysctl_string seting 0 to oldval[len], len is the available buffer size specified by the user, obviously, this will write to the first byte of the user memory place immediate after the old value buffer
, the correct way is that sysctl_string doesn't set 0, the user should do it by self in the program.

The following program verifies this point:

#include <linux/unistd.h>
#include <linux/types.h>
#include <linux/sysctl.h>
#include <errno.h>

_syscall1(int, _sysctl, struct __sysctl_args *, args); int sysctl(int *name, int nlen, void *oldval, size_t *oldlenp,
void *newval, size_t newlen)

{
        struct __sysctl_args args
               = {name,nlen,oldval,oldlenp,newval,newlen};

        return _sysctl(&args);
}

#define SIZE(x) sizeof(x)/sizeof(x[0])
#define OSNAMESZ 4

struct mystruct {
       char osname[OSNAMESZ];
       int target;
       int osnamelth;
} myos;

int name[] = { CTL_KERN, KERN_NODENAME };

int main(int argc, char * argv[])

{
       myos.target = 1;
       printf("target = %d\n", myos.target);
       myos.osnamelth = SIZE(myos.osname);
        if (sysctl(name, SIZE(name), myos.osname,
                       &myos.osnamelth, 0, 0))
                perror("sysctl");
       else {
                printf("Current host name: %s\n", myos.osname);
       }
       printf("target = %d\n", myos.target);
        return 0;
}

Copy it to file sysctl-safe.c, then

$ hostname
mylocalmachine
$ gcc sysctl-safe.c
$ ./a.out
target = 1
Current host name: mylo
target = 0
$
Quoted 9 lines

After apply this patch:

$ hostname
mylocalmachine
$ gcc sysctl-safe.c
$ ./a.out
target = 1
Current host name: mylo
Quoted 6 lines

You didn't set the trailing '\0', I wonder how your printf did work properly ever. You've just been lucky or something.

-- Coywolf

The variable target does it, its value is 0x00000001, so you mustn't worry it. osname only has 4-bytes space, so if you set '\0' to its tail, a byte information will be lost.

Quoted 29 lines
Quoted 16 lines

target = 1

kernel/sysctl.c 2005-12-30 15:58:15.000000000 +0000 int sysctl_string(ctl_table *table, int · line 2212

Quoted 8 lines
kernel/sysctl.c 2005-12-30 15:58:15.000000000 +0000 renamed from kernel/sysctl.c.orig 2005-12-30 09:21:34.000000000 +0000 +0 -2
@@ -2207,8 +2207,6 @@ int sysctl_string(ctl_table *table, int
                               len = table->maxlen;
                       if(copy_to_user(oldval, table->data, len))
                               return -EFAULT;
-                       if(put_user(0, ((char __user *) oldval) + len))
-                               return -EFAULT;
                       if(put_user(len, oldlenp))
                               return -EFAULT;
               }

--
Coywolf Qi Hunt

#8 lore raw

2005/12/31, Yi Yang <[email protected]>:

Quoted 11 lines
Quoted 6 lines · Coywolf Qi Hunt

You didn't set the trailing '\0', I wonder how your printf did work properly ever. You've just been lucky or something.

-- Coywolf

The variable target does it, its value is 0x00000001, so you mustn't worry it.
osname only has 4-bytes space, so if you set '\0' to its tail, a byte information will be lost.

I'm worrying more. We should set '\0'. Let the one byte information lost, the caller deserve that. Actually here printf sees "mylo"+'\01' if little endian.

Linus, besides fixing bug, your commit certainly breaks userland compatibility. Please consider.
--
Coywolf Qi Hunt

#4 lore raw
In reply to Linus Torvalds

Hello.

In article <[email protected]> (at Fri, 30 Dec 2005 09:25:35 -0800 (PST)), Linus Torvalds <[email protected]> says:

Quoted 10 lines · Yi Yang
Quoted 9 lines

If the user reads a sysctl entry which is of string type by sysctl syscall, this call probably corrupts the user data right after the old value buffer, the issue lies in sysctl_string seting 0 to oldval[len], len is the available buffer size specified by the user, obviously, this will write to the first byte of the user memory place immediate after the old value buffer, the correct way is that sysctl_string doesn't set 0, the user should do it by self in the program.

:

Quoted 1 line

We _should_ zero-pad the data, at least if the result fits in the buffer.

:

Quoted 4 lines

But even that is questionable: one alternative is to always zero-pad (like we used to), but make sure that the buffer size is sufficient for it (ie instead of adding one to the length of the string, we'd subtract one from the buffer length and make sure that the '\0' fits..

How about returning -ENOMEM, as BSDs (FreeBSD and NetBSD at least) do. No?

--yoshfuji

author
#9 lore raw
In reply to Coywolf Qi Hunt
Quoted 21 lines · Coywolf Qi Hunt

2005/12/31, Yi Yang <[email protected]>:

Quoted 11 lines
Quoted 8 lines · Coywolf Qi Hunt

You didn't set the trailing '\0', I wonder how your printf did work properly ever. You've just been lucky or something.

-- Coywolf

Quoted 6 lines

The variable target does it, its value is 0x00000001, so you mustn't worry it.
osname only has 4-bytes space, so if you set '\0' to its tail, a byte information will be lost.

Quoted 5 lines

I'm worrying more. We should set '\0'. Let the one byte information lost, the caller deserve that. Actually here printf sees "mylo"+'\01' if little endian.

I want to remind of you, my program is an example to verify this bug, it can run on both little-endian and big-endian computer correctly, because the variable target is 0x00000001 or 0x00000000, it sets 0 to the tail of osname, only a byte '\01' isn't expected for little-endian, but my program really can run correctly.

For compatibility, you don't worry, you can try the application 'sysctl -a'.

This bug is very serious as far as security is concerned, so it is necessary, the kernel shouldn't corrupt the user data no matter what the reason is, if the user provides more space, Linus's patch has set 0 to the tail, but if the user space is not enough, to set 0 to the tail is not necessary, because a user mode application should do it by itself, but not depends on kernel.

Quoted 7 lines

Linus, besides fixing bug, your commit certainly breaks userland compatibility. Please consider.
--
Coywolf Qi Hunt