socket_peercred / SCM_CREDENTIALS: UB when kernel returns pid 0

Ouverte
#1,698 0 commentaires 0 réactions 0 personnes assignées Voir sur GitHub

Les mainteneurs répondent en général sous 2 jours

Personne n'a encore pris cette issue.

Évaluation

Difficulté
4/5
Temps estimé
3-5 jours
Accessibilité débutants
42/100
Type d'issue
Bug
Clarté
Plutôt claire
Activité
Active
Stack technique
linux, rust

Piste de recherche

Start with UCred and Pid in src/net/types.rs, then trace socket_peercred in src/backend/linux_raw/net/sockopt.rs and src/backend/libc/net/sockopt.rs, plus RecvAncillaryMessage::ScmCredentials where the issue identifies an unaligned read. The issue proposes representing an absent pid explicitly and converting from a raw integer-field struct; inspect related issues #1696 and #1678 before choosing the change. Done means both credential paths handle kernel pid 0 without constructing an invalid Pid.

Rédigé par le modèle d'indexation à partir du texte de l'issue.

Description

The Linux kernel returns a pid of 0 in a struct ucred in several situations. Rustix turns that struct directly into UCred without checking it. The result is a Pid (NonZeroI32) holding 0, which is undefined behavior.

Where

  • UCred is #[repr(C)] { pid: Pid, uid: Uid, gid: Gid } and Pid wraps NonZeroI32 (src/net/types.rs)
  • socket_peercred is getsockopt::<UCred>(fd, SOL_SOCKET, SO_PEERCRED) (src/backend/linux_raw/net/sockopt.rs, src/backend/libc/net/sockopt.rs): getsockopt fills a MaybeUninit<T> and calls assume_init() but with UCred a pid of 0 is an invalid bit pattern
  • RecvAncillaryMessage::ScmCredentials uses payload.as_ptr().cast::<UCred>().read_unaligned()

When is it 0

socket_peercred / SCM_CREDENTIALS use pid_vnr(), which translates a struct pid into the reader's pid namespace. It returns 0 when the pid is NULL or not visible in that namespace. (reference: https://github.com/torvalds/linux/blob/67f0943b394d920b6c142aad8c6af94340342ae7/kernel/pid.c#L542-L560)

So it's 0 for:

  • socket with no peer credentials e.g. TCP socket or unconnected UNIX socket (SO_PEERCRED succeeds with pid 0 and uid/gid −1)
  • peer that's not visible in the reader's pid namespace e.g. process in a container reading credentials of a peer on the host or in a sibling container. https://man7.org/linux/man-pages/man7/pid_namespaces.7.html mentions that pids passed over Unix sockets are translated into the receiving process's pid namespace.
  • SCM_CREDENTIALS sent without credentials e.g. a message sent before the receiver enabled 'SO_PASSCRED' on a stream socket

https://man7.org/linux/man-pages/man7/unix.7.html and https://man7.org/linux/man-pages/man7/socket.7.html describe SO_PEERCRED and SCM_CREDENTIALS but don't mention the 0 case but see the kernel code references above.

Related

#1696 (getpgid) and #1678 (tcgetpgrp on macOS) are also cases of a pid of 0 from the kernel turned into a Pid.

Reproduction

Cargo.toml:

[package]
name = "repro"
version = "0.1.0"
edition = "2024"

[dependencies]
libc = "0.2"
# "process" only because "net" alone does not build (#1689).
rustix = { version = "=1.1.5", features = ["net", "process"] }

[features]
use-libc = ["rustix/use-libc"]
use std::io::{IoSliceMut, Write};
use std::mem::MaybeUninit;
use std::os::fd::AsRawFd;
use std::os::unix::net::{UnixDatagram, UnixStream};

use rustix::net::{RecvAncillaryBuffer, RecvAncillaryMessage, RecvFlags, recvmsg, sockopt};

fn main() {
   so_peercred_pid_0();
   scm_credentials_pid_0();
}

/// `socket_peercred`: An unbound socket has no peer, so SO_PEERCRED succeeds with pid 0 but rustix returns an error.
fn so_peercred_pid_0() {
   // An unbound socket has no peer, so SO_PEERCRED succeeds with pid 0.
   let socket = UnixDatagram::unbound().unwrap();

   let mut cred = libc::ucred { pid: -1, uid: 0, gid: 0 };
   let mut len = size_of::<libc::ucred>() as libc::socklen_t;
   let ret = unsafe {
      libc::getsockopt(
         socket.as_raw_fd(),
         libc::SOL_SOCKET,
         libc::SO_PEERCRED,
         (&raw mut cred).cast(),
         &mut len,
      )
   };
   println!("SO_PEERCRED libc:   ret={ret} pid={} uid={} gid={}", cred.pid, cred.uid as i32, cred.gid as i32);

   println!("SO_PEERCRED rustix: {:?}", sockopt::socket_peercred(&socket));
}

/// `RecvAncillaryMessage::ScmCredentials`: message sent before the receiver set SO_PASSCRED has no credentials so kernel succeeds with pid 0
fn scm_credentials_pid_0() {
   let (mut sender, receiver) = UnixStream::pair().unwrap();
   sender.write_all(b"x").unwrap();
   sockopt::set_socket_passcred(&receiver, true).unwrap();

   let mut space = [MaybeUninit::uninit(); rustix::cmsg_space!(ScmCredentials(1))];
   let mut control = RecvAncillaryBuffer::new(&mut space);
   recvmsg(&receiver, &mut [IoSliceMut::new(&mut [0])], &mut control, RecvFlags::empty()).unwrap();
   for message in control.drain() {
      if let RecvAncillaryMessage::ScmCredentials(cred) = message {
         // A `Pid` can't be 0 so the compiler may turn this into `false`.
         println!(
            "SCM_CREDENTIALS rustix: {cred:?}, pid == 0: {}",
            cred.pid.as_raw_pid() == 0
         );
      }
   }
}

uid is returned as Errno

raw: cargo run - uid bits truncated to 16 bits and negated: -1 -> 1 (EPERM)

SO_PEERCRED libc:   ret=0 pid=0 uid=-1 gid=-1
SO_PEERCRED rustix: Err(Os { code: 1, kind: PermissionDenied, message: "Operation not permitted" })
SCM_CREDENTIALS rustix: UCred { pid: Pid(0), uid: Uid(65534), gid: Gid(65534) }, pid == 0: true

libc: cargo run --features use-libc - uid -1 -> -1

SO_PEERCRED libc:   ret=0 pid=0 uid=-1 gid=-1
SO_PEERCRED rustix: Err(Os { code: -1, kind: Uncategorized, message: "Unknown error -1" })
SCM_CREDENTIALS rustix: UCred { pid: Pid(0), uid: Uid(65534), gid: Gid(65534) }, pid == 0: true

Idea

(Breaking) make UCred::pid an Option<Pid>: make explicit that a pid may be absent. std's UCred does the same.

  • read the kernel's bytes into a private raw struct e.g. RawUCred with plain integer fields and convert explicitly
  • msg.rs can do the same with read_unaligned::<RawUCred>()
Langage dominant
Rust
Étoiles
2.1k
Forks
320
Merge moyen
6 j 15 h
PR mergées (30 j)
11

Préparer son environnement

Par où commencer

  1. Lisez l'issue en entier, puis le guide de contribution du projet.
  2. Signalez en commentaire que vous la prenez — cela évite que deux personnes fassent le même travail.
  3. Forkez le dépôt et travaillez sur une branche.
  4. Ouvrez une pull request qui référence le numéro de l'issue.

Autres issues de bytecodealliance/rustix

Toutes les issues de bytecodealliance/rustix

Issues similaires

Plus d'issues Rust

Recevez les nouvelles issues par e-mail

Un résumé court des issues GitHub adaptées aux débutants.