Remix.run Logo
▲ MrBuddyCasino 4 hours ago

The heart of the matter:

  struct Q<T: 'static>(Option<T>);

  impl<T: 'static> Drop for Q<T> {
    fn drop(&mut self) {
        // Code in here has access to a non-'static &String
        // but assumes that it's a &'static String
    }
  }

  fn main() {
    let l = String::new();
    let mut q = Q(None);
    q.0 = Some(&l);
  }
I wonder if the certified compiler for safety critical uses (edit: Ferrocene) has the same issue.
▲Groxx 4 minutes ago | parent | next [-]

[delayed]

▲1718627440 3 hours ago | parent | prev | next [-]

Can someone ELI5 for people who don't know Rust (but e.g. C)?

▲efficax 28 minutes ago | parent | next [-]

'static lifetime bounds are kind of confusing in Rust, but here goes: A 'static lifetime constraint on T says: types that satisfy T must not contain any borrowed value/content whose lifetime will be shorter than the life of the program. So suppose you have a string s, and Option<&String> q. If q has the constraint 'static, and you do q = Some(&s) this should be rejected by the compiler since a reference to s gets a shorter lifetime than 'static by default (you could do q = Some(&'static s) if s is say a string literal or a leaked box but not for a normal string). In the examples it looks like you have to use a tuple to trick the borrow checker but the concept is the same: Rust allowed violation of the 'static lifetime bounds. The drop implemenation demonstrates it: drop happens in reverse declaration order, so the string s gets dropped first. Now q has a dangling reference, a pointer to dropped memory essentially, and we get a pretty classic use after free bug.

▲thinkharderdev 2 hours ago | parent | prev [-]

Rust is supposed to guarantee statically that a reference cannot outlive its referent. The `T: 'static` bound means that the value that Q holds must have a 'static lifetime (e.g. either be an owned value or a reference to a static value). So this program should not compile since a reference to l is not 'static. This code is unsound because rust will drop the string l before q so when the Q destructor runs the underlying string has already been freed yet we have a valid reference to it.

▲biorach an hour ago | parent | next [-]

> rust will drop the string l before q

Why?

▲1718627440 22 minutes ago | parent [-]

My thoughts as well. The problem can be alleviated either by making the ownership change an error or "just" invoking the destructors in the correct order.

But I think https://news.ycombinator.com/item?id=50072252 answers it:

    A 'static lifetime constraint on T says: types that satisfy T must not contain any borrowed value/content whose lifetime will be shorter than the life of the program. 
So the compiler assumes it is guaranteed, that 'l' still exists, so it doesn't consider it in regard of finalizer ordering.
▲1718627440 2 hours ago | parent | prev | next [-]

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)
▲NobodyNada 43 minutes ago | parent [-]

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.

▲1718627440 24 minutes ago | parent [-]

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).

▲1718627440 2 hours ago | parent | prev [-]

What does "q.0" and "Some(...)" mean?

▲thinkharderdev 2 hours ago | parent | next [-]

So Q is a "tuple-struct". In rust you can define a struct like

struct Q<T> { value: Option<T> }

with a named field or you can define it like

struct<T> Q(Some<T>)

which is the same struct layout but without an explicit name for the value. In that case, the field is implicitly names `0`

So q.0 = Some(foo) is equivalent to q.value = Some(foo).

The Some(..) is just one variant of the Option enum:

enum Option<T> { None, Some(T) }

Option represents a value that may or may not be initialized. For uninitialized values you use the None variant and then you can initialize it to Some(value).

The C rough equivalent would be

struct Q { void* value; }

struct Q q = q { NULL }; q->value = ...

▲cowlevel 2 hours ago | parent | prev | next [-]

Some(...) is just constructor for an option type as seen in other languages e.g. java's Optional, haskell's Maybe, c++'s std::optional. I assume that it's needed here because the field can't start as a null pointer otherwise. Maybe the vulnerability still works if the field starts with one string value and swaps to a different string value, but having it start holding None made for a simpler demo.

q.0 is the first field of q, which is anonymous. They could have just as well called it x and written q.x.

▲bonzini 2 hours ago | parent | prev | next [-]

"Some(...)" means that this Option<T> does have a value of type T in it; "None" means it doesn't.

"q.0" is not really needed, it's a struct with unnamed fields. Here is the same with named fields:

  struct Q<T: 'static> { pub x: Option<T> }
  ...
  let l = String::new();
  let mut q = Q { x: None };
  q.x = Some(&l);
▲ 2 hours ago | parent | prev [-]
[deleted]
▲afdbcreid 3 hours ago | parent | prev | next [-]

> I wonder if the certified compiler for safety critical uses has the same issue.

It uses the same code, so yes.

(Although I don't know what are their guidelines, it might be that they require avoiding code relying on implied bounds, for example).

▲high_na_euv 4 hours ago | parent | prev | next [-]

which compiler?

▲sshine 4 hours ago | parent [-]

Ferrocene

▲wg0 3 hours ago | parent | prev [-]

DeepSeek v4.1 Flash thinks it should NOT compile. Whereas in reality it DOES compile which is why it is filed as a bug.

▲jug 3 hours ago | parent [-]

They're seriously using AI as a judge for what to file?

▲afdbcreid 3 hours ago | parent | next [-]

The issue and none of the comments mention AI. It is pretty clear your parent said it by themselves.

▲wg0 3 hours ago | parent | prev [-]

I was just checking what outcome an LLM would have based on the patterns in its probabilistic distribution it has seen the wild.

Most LLMs would probably say this piece of code won't compile.

▲ChickeNES 3 hours ago | parent [-]

chatgpt and grok linked to the the soundness issue, claude I am in usage timeout, and gemini shit the bed as usual: "The compiler rejects the program with error[E0597] on the assignment q.0 = Some(&l)"