Hacktoberfest 2026:メンテナが10月に向けて印を付けた、オープンで初心者向けの issue。 Hacktoberfest の issue を見る

TSI: impersonated INET sockets always get SO_REUSEPORT, so a second VM's `listen()` silently shares the host port

オープン
#860 コメント 2 件 リアクション 0 件 担当者 0 名 GitHub で見る

メンテナーはふだん 2 日以内に返信

まだ誰も着手していません。

評価

難易度
4/5
見積もり時間
3〜5日
初心者へのやさしさ
42/100
issue の種類
バグ
明瞭さ
おおむね明確
活発さ
活発
技術スタック
c, rust

調査の方向性

Start by reading the INET socket setup at src/devices/src/virtio/vsock/tsi_stream/unix.rs:64 and compare it with the earlier implementation at src/devices/src/virtio/vsock/tsi_stream.rs:102; the issue identifies unconditional SO_REUSEPORT as the suspected cause. Check how TSI can observe the guest socket option and consider the UNIX-socket precedent in #251. Done means guest-requested reuseport behavior remains supported while two VMs binding the same port no longer silently share connections; verify with the supplied two-VM reproducer and controls.

索引モデルが issue の本文から書いたものです。

説明

Summary

With KRUN_TSI_FLAGS_HIJACK_INET, the VMM creates a host-side socket for each guest INET socket. It sets SO_REUSEPORT on that socket unconditionally, whether or not the guest asked for it:

  • main (at 24d714b5): src/devices/src/virtio/vsock/tsi_stream/unix.rs:64
  • v1.19.4: src/devices/src/virtio/vsock/tsi_stream.rs:102

Take two TSI VMs run by the same user in the same network namespace, where both guests listen() on the same port. The second listen() does not fail with EADDRINUSE. Instead, both impersonated listeners join one reuseport group, and the host kernel spreads incoming connections across them.

The guest's connect to its own 127.0.0.1:<port> is also performed on the host, so it is answered by the other VM's guest:

  • On Linux, the kernel spreads connections across the group.
  • On macOS, the most recently bound listener takes every connection. A second VM's listen() therefore silently takes the port over, and the first VM can no longer reach its own loopback service at all.

Reproduction

The setup:

  • two VMs, each in its own process;
  • krun_vsock_device_new(3, HIJACK_INET|HIJACK_UNIX);
  • each guest listens on its own 127.0.0.1:6379 and answers with its own role letter;
  • each guest then connects to 127.0.0.1:6379 eight times.
=== host listeners on 6379 ===
LISTEN 127.0.0.1:6379 users:(("tsi_repro_main",pid=433519,fd=56))
LISTEN 127.0.0.1:6379 users:(("tsi_repro_main",pid=433518,fd=56))

=== VM A ===                          === VM B ===
PROBE A: attempt 0 answered by B      PROBE B: attempt 0 answered by B
PROBE A: attempt 1 answered by B      PROBE B: attempt 1 answered by B
PROBE A: attempt 2 answered by B      PROBE B: attempt 2 answered by B
PROBE A: attempt 3 answered by B      PROBE B: attempt 3 answered by A
PROBE A: attempt 4 answered by A      PROBE B: attempt 4 answered by B
PROBE A: attempt 5 answered by B      PROBE B: attempt 5 answered by A
PROBE A: attempt 6 answered by A      PROBE B: attempt 6 answered by B
PROBE A: attempt 7 answered by B      PROBE B: attempt 7 answered by A

VM A was answered by VM B in 6 of its 8 attempts. Both listeners are visible on the host while the VMs run.

Controls:

  • The same two VMs with tsi_features = 0: no host listener appears, and each guest answers itself 8/8.
  • A single VM with the hijack on: answered by itself 8/8.

So HIJACK_INET is exactly the switch, and a second VM in the same namespace is what triggers it.

macOS (libkrun 1.19.4)

The same experiment on macOS reproduces it deterministically. It uses the 1.19.x harness in macos/, which runs on a released libkrun, and HIJACK_INET only, since HIJACK_UNIX is refused on macOS.

Where each VM's 8 connections to its own 127.0.0.1:6379 were answered:

Start order VM A's 8 connects answered by VM B's 8 connects answered by
A, then B (0.5 s apart) B 8/8 B 8/8
B, then A (0.5 s apart) A 8/8 A 8/8
A, then B again B 8/8 B 8/8
  • Both listens succeeded. Both impersonated listeners were visible on the host while the VMs ran:

    tsi_repro 89171 hansjorg   96u  IPv4 ...  TCP 127.0.0.1:6379 (LISTEN)
    tsi_repro 89172 hansjorg   96u  IPv4 ...  TCP 127.0.0.1:6379 (LISTEN)
    
  • Controls:

    • the same pair with tsi_features = 0: each VM answered itself 8/8, and no host listener appeared;
    • a single VM with the hijack on: it answered itself 8/8.
  • No connect hung. The guest-side probe carries a 3 s connect timeout in case #344 applied here. The loopback path through an impersonated listener worked; it just led to the wrong VM.

Environment: macOS 15 (Darwin 24.6) on Apple Silicon, Hypervisor.framework, Homebrew libkrun 1.19.4 and libkrunfw 5.5.0.

Files

Every file below is inlined at the end of this issue, under Sources.

  • probe.rs (the guest program).
    • Build it with rustc --target x86_64-unknown-linux-musl -O -o probe probe.rs.
    • Place it at /probe in a rootfs directory that contains only it plus empty dev proc sys tmp etc.
  • tsi_loopback_repro.c (the host harness, written against the current main API).
    • Build libkrun with make FFI=1 BLK=1 NET=1.
    • Then build the harness with gcc -o tsi_repro_main tsi_loopback_repro.c -I<libkrun>/include -L<libkrun>/target/release -lkrun -lkrun_init.
    • Usage is ./tsi_repro_main <rootfs> <role-letter> [tsi_flags].
    • Run two instances at once with roles A and B. For the control, pass 0 as tsi_flags.
    • While they run, ss -tlnp 'sport = :6379' shows both listeners.
  • macos/ (the same experiment against the 1.19.x API, so it runs on a released libkrun):
    • tsi_loopback_repro_legacy.c is the harness, using krun_create_ctx, krun_disable_implicit_vsock + krun_add_vsock(ctx, tsi_features), and krun_start_enter.
    • probe/ is the guest probe with a connect timeout, built with cargo zigbuild --target aarch64-unknown-linux-musl.
    • run.sh builds both, signs the harness with the hypervisor entitlement, and runs the pair, the control and a single VM.

Environment:

  • libkrun main at 24d714b5 (2.0.0-dev). The listener setup is unchanged from v1.19.4.
  • libkrunfw 5.5.0.
  • Linux x86_64 with KVM.

Who this affects

Only TSI VMs run by the same user in one network namespace.

  • Runtimes that give each VM its own network namespace: separate VMs stay apart. crun/podman's krun handler does this for separate containers.
  • An embedder that starts several TSI VMs from one namespace: this embedder is affected.
  • macOS: there are no network namespaces, so any two TSI VMs run by the same user collide. There, the second one takes the port from the first entirely (shown above).

We hit it in a test harness. Our production VMs have a network device, so TSI is off there, and nothing on our side depends on a change here. We're reporting it because it is easy to hit and very hard to diagnose: a guest's 127.0.0.1 looks private and isn't.

The question

Is the unconditional SO_REUSEPORT on INET sockets deliberate? #251 removed it from impersonated UNIX sockets, where it was invalid, and INET kept it.

  • If it exists so a guest can listen again on a port it just closed: SO_REUSEADDR covers that on Linux (binding past TIME_WAIT).
  • If it exists for guest programs that use SO_REUSEPORT themselves, for example several workers on one port: setting it only when the guest's socket asked for it keeps that working, if TSI can observe the guest's option. An unintended collision between VMs would then fail with EADDRINUSE, which the guest can see and handle, instead of silently sharing connections with another VM.

Related:

  • #818: TSI listen() binds in the VMM's own namespace. Same mechanism, different symptom.
  • #510: guest-internal AF_UNIX traffic is intercepted by TSI.
  • #251: the precedent for UNIX sockets.

Sources

probe.rs - guest program for the Linux runs
// Guest-side probe for the libkrun TSI loopback-sharing question.
//
// Each VM runs one of these with a distinct role letter. It listens on
// the guest's own 127.0.0.1:6379, then connects to 127.0.0.1:6379 and
// prints who answered. In an isolated guest the answer is always its
// own role. Any other letter means the connection left this VM.
use std::io::{Read, Write};
use std::net::{TcpListener, TcpStream};
use std::thread;
use std::time::Duration;

fn main() {
    let role = std::env::args().nth(1).unwrap_or_else(|| "X".to_string());
    let listener = match TcpListener::bind("127.0.0.1:6379") {
        Ok(l) => l,
        Err(e) => {
            println!("PROBE {role}: bind 127.0.0.1:6379 failed: {e}");
            return;
        }
    };
    println!("PROBE {role}: listening on 127.0.0.1:6379");
    let served = role.clone();
    thread::spawn(move || {
        for stream in listener.incoming().flatten() {
            let mut stream = stream;
            let mut buf = [0u8; 64];
            let _ = stream.read(&mut buf);
            let _ = stream.write_all(format!("SERVED-BY-{served}\n").as_bytes());
        }
    });

    thread::sleep(Duration::from_secs(3));
    for attempt in 0..8 {
        match TcpStream::connect("127.0.0.1:6379") {
            Ok(mut s) => {
                let _ = s.write_all(b"ping\n");
                let _ = s.set_read_timeout(Some(Duration::from_secs(2)));
                let mut out = String::new();
                let _ = s.read_to_string(&mut out);
                println!("PROBE {role}: attempt {attempt} answered by {}", out.trim());
            }
            Err(e) => println!("PROBE {role}: attempt {attempt} connect failed: {e}"),
        }
        thread::sleep(Duration::from_millis(400));
    }
    println!("PROBE {role}: done");
}
tsi_loopback_repro.c - host harness, libkrun `main` API (Linux runs)
/*
 * Minimal reproducer: does a guest's loopback listen/connect under
 * libkrun TSI reach the *host's* loopback, so that two concurrently
 * running VMs share one host port?
 *
 * Usage: tsi_loopback_repro <rootfs-dir> <role-letter>
 *
 * Run two of these at once with different role letters. Each guest
 * listens on its own 127.0.0.1:6379 and then connects to it. If a
 * guest is ever answered by the other guest's role letter, guest
 * loopback is not guest-private.
 */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdbool.h>
#include <unistd.h>
#include <libkrun.h>
#include <libkrun_init.h>

static bool push_to_stderr(void *userdata, KrunStr s)
{
    (void)userdata;
    fwrite(s.data, 1, s.len, stderr);
    return true;
}

static KrunVtableHandle init_stderr_writer = KRUN_VTABLE_HANDLE(
    KRUN_INIT_PUSH_STR_TYPE_TAG,
    ((KrunInitPushStrVtable){ .drop = NULL, .push = push_to_stderr }),
    NULL);

static KrunVtableHandle krun_stderr_writer = KRUN_VTABLE_HANDLE(
    KRUN_PUSH_STR_TYPE_TAG,
    ((KrunPushStrVtable){ .drop = NULL, .push = push_to_stderr }),
    NULL);

#define CHECK(call)                                                            \
    err = NULL;                                                                \
    call;                                                                      \
    if (err) {                                                                 \
        fprintf(stderr, "%s failed: ", #call);                                 \
        krun_error_message(err, &krun_stderr_writer);                          \
        fputc('\n', stderr);                                                   \
        krun_error_destroy(err);                                               \
        return 1;                                                              \
    }

int main(int argc, char **argv)
{
    KrunError err;
    if (argc < 3) {
        fprintf(stderr, "usage: %s <rootfs-dir> <role-letter>\n", argv[0]);
        return 2;
    }
    const char *rootfs = argv[1];
    const char *role = argv[2];

    CHECK(krun_init_log(-1, KRUN_LOG_LEVEL_WARN, KRUN_LOG_STYLE_AUTO, 0, &err));

    KrunFsOverlay overlay = krun_fs_overlay_new();
    KrunPayload payload = krun_payload_load_krunfw(&err);
    if (!payload) { fprintf(stderr, "loading krunfw payload failed\n"); return 1; }

    {
        KrunInitError ierr = NULL;
        KrunInitBuilder b = krun_init_config_builder();
        krun_init_builder_arg(&b, KRUN_STR("/probe"));
        krun_init_builder_arg(&b, KRUN_STR((char *)role));
        krun_init_builder_env_var(&b, KRUN_STR("PATH=/"));
        krun_init_builder_workdir(&b, KRUN_STR("/"));
        KrunInitConfig cfg = krun_init_builder_build(&b);
        krun_init_config_apply(cfg, overlay, payload, &ierr);
        if (ierr) {
            fprintf(stderr, "applying init config failed: ");
            krun_init_error_message(ierr, &init_stderr_writer);
            fputc('\n', stderr);
            krun_init_error_destroy(ierr);
            return 1;
        }
    }

    KrunMmioDeviceManager devices = krun_mmio_device_manager_new();
    {
        KrunConsoleBuilder cb = krun_console_device_builder();
        CHECK(krun_console_builder_add_default_console(cb, STDIN_FILENO, STDOUT_FILENO, STDERR_FILENO, &err));
        KrunConsoleDevice console = krun_console_builder_build(cb, &err);
        krun_mmio_device_manager_add(devices, console);
    }
    {
        KrunFsDevice fs;
        CHECK(fs = krun_fs_device_new(KRUN_STR("/dev/root"), KRUN_STR((char *)rootfs), &err));
        krun_fs_device_set_overlay(fs, overlay);
        krun_mmio_device_manager_add(devices, fs);
    }
    {
        /* No net device; TSI hijacks the guest's INET sockets - the
         * exact configuration the krun sccache test boots on Linux. */
        KrunVsockDevice vsock;
        uint32_t tsi_flags = (argc > 3) ? (uint32_t)strtoul(argv[3], NULL, 0)
                                        : (KRUN_TSI_FLAGS_HIJACK_INET | KRUN_TSI_FLAGS_HIJACK_UNIX);
        fprintf(stderr, "tsi_flags=0x%x\n", tsi_flags);
        CHECK(vsock = krun_vsock_device_new(3, tsi_flags, &err));
        krun_mmio_device_manager_add(devices, vsock);
    }
    {
        KrunRngDevice rng;
        CHECK(rng = krun_rng_device_new(&err));
        krun_mmio_device_manager_add(devices, rng);
    }

    KrunVmmBuilder builder = krun_vmm_builder_new();
    CHECK(krun_vmm_builder_vcpus(&builder, 1, &err));
    CHECK(krun_vmm_builder_ram_mib(&builder, 512, &err));
    krun_vmm_builder_payload(&builder, payload);
    krun_vmm_builder_devices(&builder, devices);
    CHECK(krun_vmm_builder_split_irqchip(&builder, false, &err));

    KrunVmm vmm;
    CHECK(vmm = krun_vmm_builder_build(&builder, &err));
    krun_vmm_run(vmm);
    return 0;
}
macos/tsi_loopback_repro_legacy.c - host harness, libkrun 1.19.x API (macOS runs)
/*
 * The same experiment as ../tsi_loopback_repro.c, written against the
 * libkrun 1.19.x API: the one Homebrew and the distributions ship. It
 * runs on a released library, macOS included.
 *
 * Usage: tsi_loopback_repro_legacy <rootfs-dir> <role-letter> [tsi_flags]
 *
 * tsi_flags defaults to KRUN_TSI_HIJACK_INET (HIJACK_UNIX is refused on
 * macOS). Pass 0 for the control: no impersonation at all.
 */
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <libkrun.h>

#define CHECK(call)                                                            \
    do {                                                                       \
        int32_t rc_ = (call);                                                  \
        if (rc_ < 0) {                                                         \
            fprintf(stderr, "%s failed: %d\n", #call, rc_);                    \
            return 1;                                                          \
        }                                                                      \
    } while (0)

int main(int argc, char **argv)
{
    if (argc < 3) {
        fprintf(stderr, "usage: %s <rootfs-dir> <role-letter> [tsi_flags]\n", argv[0]);
        return 2;
    }
    const char *rootfs = argv[1];
    const char *role = argv[2];
    uint32_t tsi_flags = (argc > 3) ? (uint32_t)strtoul(argv[3], NULL, 0)
                                    : KRUN_TSI_HIJACK_INET;
    fprintf(stderr, "role=%s tsi_flags=0x%x\n", role, tsi_flags);

    CHECK(krun_set_log_level(KRUN_LOG_LEVEL_WARN));
    int32_t ctx = krun_create_ctx();
    if (ctx < 0) {
        fprintf(stderr, "krun_create_ctx failed: %d\n", ctx);
        return 1;
    }

    CHECK(krun_set_vm_config(ctx, 1, 512));
    CHECK(krun_set_root(ctx, rootfs));
    /* No network device - the configuration in which libkrun's implicit
     * vsock would turn HIJACK_INET on by itself. Configure the device
     * explicitly instead, so the flags are the only thing that differs
     * between the experiment and the control. */
    CHECK(krun_disable_implicit_vsock(ctx));
    CHECK(krun_add_vsock(ctx, tsi_flags));
    CHECK(krun_set_workdir(ctx, "/"));

    /* argv excludes argv[0]; libkrun fills that in with the exec path. */
    const char *const exec_argv[] = { role, NULL };
    const char *const exec_envp[] = { "PATH=/", NULL };
    CHECK(krun_set_exec(ctx, "/probe", exec_argv, exec_envp));

    /* Only returns if the VM could not be started. */
    CHECK(krun_start_enter(ctx));
    return 0;
}
macos/probe/src/main.rs - guest program for the macOS runs: `probe.rs` plus a connect timeout
// Guest-side probe for the libkrun TSI loopback-sharing question.
//
// Same as ../../probe.rs, except that the connect has a timeout. Under
// TSI on macOS a guest connect to 127.0.0.1 is known to be able to wedge
// (libkrun/libkrun#344), and a probe stuck in connect() reports nothing.
//
// Each VM runs one of these with a distinct role letter. It listens on
// the guest's own 127.0.0.1:6379, then connects to 127.0.0.1:6379 and
// prints who answered. In an isolated guest the answer is always its
// own role. Any other letter means the connection left this VM.
use std::io::{Read, Write};
use std::net::{SocketAddr, TcpListener, TcpStream};
use std::thread;
use std::time::Duration;

fn main() {
    let role = std::env::args().nth(1).unwrap_or_else(|| "X".to_string());
    let listener = match TcpListener::bind("127.0.0.1:6379") {
        Ok(l) => l,
        Err(e) => {
            println!("PROBE {role}: bind 127.0.0.1:6379 failed: {e}");
            return;
        }
    };
    println!("PROBE {role}: listening on 127.0.0.1:6379");
    let served = role.clone();
    thread::spawn(move || {
        for stream in listener.incoming().flatten() {
            let mut stream = stream;
            let mut buf = [0u8; 64];
            let _ = stream.read(&mut buf);
            let _ = stream.write_all(format!("SERVED-BY-{served}\n").as_bytes());
        }
    });

    thread::sleep(Duration::from_secs(3));
    let addr: SocketAddr = "127.0.0.1:6379".parse().expect("a literal socket address");
    for attempt in 0..8 {
        match TcpStream::connect_timeout(&addr, Duration::from_secs(3)) {
            Ok(mut s) => {
                let _ = s.write_all(b"ping\n");
                let _ = s.set_read_timeout(Some(Duration::from_secs(2)));
                let mut out = String::new();
                let _ = s.read_to_string(&mut out);
                let who = match out.trim() {
                    "" => "nobody (connected, empty reply)",
                    other => other,
                };
                println!("PROBE {role}: attempt {attempt} answered by {who}");
            }
            Err(e) => println!("PROBE {role}: attempt {attempt} connect failed: {e}"),
        }
        thread::sleep(Duration::from_millis(400));
    }
    println!("PROBE {role}: done");
}
macos/probe/Cargo.toml - builds the macOS guest program with `cargo zigbuild`
[package]
name = "probe"
version = "0.1.0"
edition = "2021"

[[bin]]
name = "probe"
path = "src/main.rs"

[profile.release]
opt-level = "s"

# Standalone: not a member of any enclosing workspace.
[workspace]
macos/run.sh - builds, signs and runs the macOS pair, control and single VM
#!/usr/bin/env bash
# Build and run the TSI loopback experiment on macOS against Homebrew's
# libkrun (brew tap libkrun/krun && brew install libkrun).
#
#   ./run.sh           build, then run all three experiments
#
# 1. two VMs at once with HIJACK_INET - does a guest's connect to its own
#    127.0.0.1:6379 reach the other VM?
# 2. the same two VMs with flags 0 - the control
# 3. one VM alone with HIJACK_INET
#
# Each VM gets a hard deadline, because a TSI loopback connect on macOS
# is known to be able to wedge (libkrun/libkrun#344).
set -euo pipefail

HERE="$(cd "$(dirname "$0")" && pwd)"
cd "$HERE"
export PATH="$(brew --prefix)/bin:$HOME/.cargo/bin:$PATH"
LK="$(brew --prefix libkrun)"
LKFW="$(brew --prefix libkrunfw)"
DEADLINE=60
OUT="$HERE/out"

build() {
    # The guest probe cross-compiles, so the Rust toolchain in use needs the
    # musl target. Set RUSTUP_TOOLCHAIN to pick a toolchain that has it.
    if ! rustup target list --installed 2>/dev/null | grep -qx aarch64-unknown-linux-musl; then
        echo "the active Rust toolchain ($(rustup show active-toolchain 2>/dev/null | cut -d' ' -f1)) lacks" \
            "aarch64-unknown-linux-musl: run 'rustup target add aarch64-unknown-linux-musl'," \
            "or set RUSTUP_TOOLCHAIN to a toolchain that has it" >&2
        exit 1
    fi
    echo "== building the guest probe (aarch64-unknown-linux-musl)"
    (cd probe && cargo zigbuild --quiet --release --target aarch64-unknown-linux-musl)
    rm -rf rootfs
    mkdir -p rootfs/dev rootfs/proc rootfs/sys rootfs/tmp rootfs/etc
    cp probe/target/aarch64-unknown-linux-musl/release/probe rootfs/probe

    echo "== building and signing the host harness against $LK"
    cc -O2 -Wall -o tsi_repro_legacy tsi_loopback_repro_legacy.c \
        -I"$LK/include" -L"$LK/lib" -lkrun
    codesign --force --sign - --entitlements hv.entitlements tsi_repro_legacy
}

# run_vms <label> <flags> <role>...: start one VM per role at once, show
# the host's listeners on 6379 while they run, then collect the output.
run_vms() {
    local label="$1" flags="$2"
    shift 2
    local pids=() role
    mkdir -p "$OUT"
    echo
    echo "================ $label (tsi_flags=$flags, VMs: $*) ================"
    for role in "$@"; do
        DYLD_LIBRARY_PATH="$LK/lib:$LKFW/lib" ./tsi_repro_legacy "$HERE/rootfs" "$role" "$flags" \
            > "$OUT/$label-$role.log" 2>&1 &
        pids+=("$!")
    done
    sleep 2
    echo "-- host listeners on 6379, 2s in:"
    lsof -nP -iTCP:6379 -sTCP:LISTEN 2>/dev/null | sed 's/^/   /' || true
    local waited=0
    while [ "$waited" -lt "$DEADLINE" ]; do
        local alive=0 pid
        for pid in "${pids[@]}"; do kill -0 "$pid" 2>/dev/null && alive=1; done
        [ "$alive" = 0 ] && break
        sleep 1
        waited=$((waited + 1))
    done
    local pid
    for pid in "${pids[@]}"; do
        if kill -0 "$pid" 2>/dev/null; then
            echo "-- pid $pid still running after ${DEADLINE}s: killing it"
            kill -9 "$pid" 2>/dev/null || true
        fi
    done
    for role in "$@"; do
        echo "-- VM $role:"
        grep -E "PROBE|failed|error|tsi_flags" "$OUT/$label-$role.log" | sed 's/^/   /' || true
    done
}

build
run_vms pair-hijack 1 A B
run_vms pair-control 0 A B
run_vms single-hijack 1 A
macos/hv.entitlements - hypervisor entitlement the macOS harness is signed with
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <key>com.apple.security.hypervisor</key>
    <true/>
</dict>
</plist>
主要言語
Rust
スター
2.8k
フォーク
279
平均マージ
3日 7時間
マージ済み PR(30日)
24

環境構築

はじめの一歩

  1. issue を最後まで読み、次にプロジェクトのコントリビューションガイドを読みます。
  2. 着手することを issue にコメントします — 二人が同じ作業をするのを防げます。
  3. リポジトリをフォークし、ブランチを切って変更します。
  4. issue 番号を参照したプルリクエストを送ります。

libkrun/libkrun のほかの issue

libkrun/libkrun の issue をすべて見る

似ている issue

Rust の issue をもっと見る

新しい issue をメールで受け取る

初心者向けの GitHub issue を短くまとめたダイジェスト。