Comment by 1718627440
11 days ago
So the equivalent in C would be:
$ cat main.c
#include <stdlib.h>
#include <string.h>
struct Q {
/*@null@*/ /*@only@*/ char * t;
};
/*@only@*/
struct Q *
Q_new (/*@only@*/ char * t)
{
struct Q * q = malloc (sizeof *q);
q->t = t;
return q;
}
void
Q_set (struct Q * q, /*@only@*/ char * t)
{
free (q->t);
q->t = t;
return;
}
void
Q_free (/*@only@*/ struct Q * q)
{
free (q->t);
free (q);
return;
}
int
main (void)
{
/*@only@*/ char * l;
/*@only@*/ struct Q * q;
l = strdup ("");
q = Q_new (NULL);
Q_set (q, l);
free (l);
/* This is UB */
Q_free (q);
return EXIT_SUCCESS;
}
$ splint main.c
Splint 3.1.2 --- 21 Feb 2021
main.c: (in function Q_new)
main.c:15:3: Arrow access from possibly null pointer q: q->t
A possibly null pointer is dereferenced. Value is either the result of a
function which may return null (in which case, code should check it is not
null), or a global, parameter or structure field declared with the null
qualifier. (Use -nullderef to inhibit warning)
main.c:14:17: Storage q may become null
main.c: (in function main)
main.c:43:6: Unrecognized identifier: strdup
Identifier used in code has not been declared. (Use -unrecog to inhibit
warning)
main.c:44:13: Null storage passed as non-null param: Q_new (NULL)
A possibly null pointer is passed as a parameter corresponding to a formal
parameter with no /*@null@*/ annotation. If NULL may be used for this
parameter, add a /*@null@*/ annotation to the function parameter declaration.
(Use -nullpass to inhibit warning)
main.c:47:8: Dead storage l passed as out parameter to free: l
Memory is used after it has been released (either by passing as an only param
or assigning to an only global). (Use -usereleased to inhibit warning)
main.c:46:12: Storage l released
main.c:12:1: Function exported but not used outside main: Q_new
A declaration is exported, but not used outside this module. Declaration can
use static qualifier. (Use -exportlocal to inhibit warning)
main.c:17:1: Definition of Q_new
main.c:20:1: Function exported but not used outside main: Q_set
main.c:25:1: Definition of Q_set
main.c:28:1: Function exported but not used outside main: Q_free
main.c:33:1: Definition of Q_free
Finished checking --- 7 code warnings
(The equivalent warning is main.c:47)
If you wrote a Q_free function and called it after `free(l);` to actually demonstrate the UAF, that would be pretty close to the semantics of the Rust example.
I have added that now.
I originally didn't do this, because I didn't feel the need to invoke UB without necessity. Also I wanted to demonstrate, that the error can (and is) caught in C, without actually committing the double-free (so that the program behaviour is still well-defined, but the abstraction is already violated).
I'm not familiar with Splint, but my understanding is that the error is because of the @only@ marker indicating that the string is not shared. That's an incorrect translation of the Rust program, where the reference to the string is explicitly a shared reference rather than an owned reference. The issue isn't the sharing; it's that rustc mistakenly allows the code to assign a reference that is valid until the end of the current scope to a field that expects a reference valid forever (like a string constant in the program binary). The Rust program with equivalent semantics to your C program (passing an owned String instead of a shared reference) is correctly rejected by the compiler.
Also, the Q_free function should not free q->t. It may access it (e.g. printing it), but it does not own that reference and is not responsible for deallocating its memory.
Sorry, I should have looked closer the first time! My excuse is that it was too early in the morning to be thinking about such things.
2 replies →