Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
83dbdba
Stabilize live tfork source freezing
yiying-zhang Jul 11, 2026
c7cd348
Harden tfork source cgroup synchronization
yiying-zhang Jul 11, 2026
156dcfa
Fix cgroup v2 freeze-state parsing
yiying-zhang Jul 11, 2026
e0761c6
Merge pull request #1 from GenseeAI/fix/fork-synchronization
yiying-zhang Jul 11, 2026
c422f42
Update README.md
yiying-zhang Jul 11, 2026
3f989ff
Use direct crun path for single-copy tfork
yiying-zhang Jul 17, 2026
8001ff4
Expand tfork CRIU failure logs
yiying-zhang Jul 17, 2026
d682a64
Include tfork restore log on CRIU failure
yiying-zhang Jul 17, 2026
f9400ac
Avoid PID namespace collision in tfork ncopy
yiying-zhang Jul 17, 2026
71a23f1
Preserve PID hierarchy during tfork restore
yiying-zhang Jul 17, 2026
ead824f
Insert pstree PIDs after loading namespace IDs
yiying-zhang Jul 17, 2026
bac21f9
Fix tfork nested pid namespace ids
yiying-zhang Jul 17, 2026
c952db9
Avoid tfork freezer wait timeout on transient tasks
yiying-zhang Jul 17, 2026
3dea3a7
Recompute invalid tfork nested pid namespace cache
yiying-zhang Jul 17, 2026
88fef5e
Force nested pid leaf ids during tfork dump
yiying-zhang Jul 17, 2026
d838fca
Finalize nested pid namespace ids before pstree dump
yiying-zhang Jul 17, 2026
852c991
Avoid ncopy helper consuming restored pid 1
yiying-zhang Jul 17, 2026
9f2fb9f
Drop source pid chain for tfork restore copies
yiying-zhang Jul 17, 2026
7d87562
Rebase nested tfork restore pid chains
yiying-zhang Jul 18, 2026
98c2360
Force pid hierarchy truncation for tfork restores
yiying-zhang Jul 18, 2026
d35c693
Use fresh parent pids for tfork pidns inits
yiying-zhang Jul 18, 2026
3d108b0
Repair tfork pidns init clone flags
yiying-zhang Jul 18, 2026
13767ad
Use fresh parent tids for tfork thread restore
yiying-zhang Jul 18, 2026
d2b35f3
Allow fresh tfork thread tids
yiying-zhang Jul 18, 2026
7ee5852
Log tfork restore stage aborts
yiying-zhang Jul 18, 2026
2ced506
Fix restore abort diagnostic build
yiying-zhang Jul 18, 2026
e04c1d7
Tolerate clean child exits during tfork restore
yiying-zhang Jul 18, 2026
c1d28a7
Make CRIU module loading idempotent
yiying-zhang Jul 18, 2026
c862f96
Avoid fatal log for clean tfork child exits
yiying-zhang Jul 18, 2026
2358325
Default tfork single-copy to direct crun
yiying-zhang Jul 18, 2026
6363d89
Make tfork clone readiness timeout configurable
yiying-zhang Jul 18, 2026
8b0cbd9
Use n-copy restore for single-copy tfork clones
yiying-zhang Jul 18, 2026
5b77814
Allow external unix sockets during tfork
yiying-zhang Jul 18, 2026
ba94887
Propagate ext unix socket option to tfork
yiying-zhang Jul 18, 2026
7693c32
Skip inotify mark replay for tfork restores
yiying-zhang Jul 18, 2026
6a14400
Add tfork restore stage timeout diagnostics
yiying-zhang Jul 18, 2026
176d7f2
Log tfork restore stage participants
yiying-zhang Jul 18, 2026
e8426fd
Trace tfork leader restore stalls
yiying-zhang Jul 18, 2026
eb724fd
Avoid tfork zombie wait deadlock
yiying-zhang Jul 19, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Tclone
# Tclone: Low-Latency Full-Workspace Forking for AI Agents

Tclone is a workspace-versioning substrate built for computer-use agents. Tclone provides a versioned personal workspace that can be quickly forked, snapshotted, and rolledback.
It forks a live, running container in milliseconds: clones share
Expand Down
18 changes: 14 additions & 4 deletions criu/build.sh
Original file line number Diff line number Diff line change
Expand Up @@ -93,12 +93,22 @@ for mod in "${ACTIVE_MODULES[@]}"; do
fi

if lsmod | awk '{print $1}' | grep -qx "${mod}"; then
echo " -- ${mod}: already loaded, rmmod then insmod"
echo " -- ${mod}: already loaded, trying reload"
rmmod "${mod}" 2>/dev/null || true
else
echo " -- ${mod}: insmod"
if lsmod | awk '{print $1}' | grep -qx "${mod}"; then
echo " -- ${mod}: still loaded; keeping existing module"
continue
fi
fi

echo " -- ${mod}: insmod"
if ! insmod "${ko_path}"; then
if lsmod | awk '{print $1}' | grep -qx "${mod}"; then
echo " -- ${mod}: insmod reported already loaded; keeping existing module"
continue
fi
exit 1
fi
insmod "${ko_path}"
done

echo " -- verify all modules loaded"
Expand Down
16 changes: 16 additions & 0 deletions criu/criu/clone-noasan.c
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,10 @@ int clone3_with_pid_noasan(int (*fn)(void *), void *arg, int flags, int exit_sig
c_args.flags = flags;
c_args.set_tid = ptr_to_u64(&pid);
c_args.set_tid_size = 1;
pr_info("clone3 set_tid pid=%d flags=0x%x size=1\n", pid, flags);
pid = syscall(__NR_clone3, &c_args, sizeof(c_args));
if (pid < 0)
pr_perror("clone3 set_tid failed flags=0x%x size=1", flags);
if (pid == 0)
exit(fn(arg));
return pid;
Expand All @@ -99,6 +102,12 @@ int clone3_with_nested_pid_noasan(int (*fn)(void *), void *arg, int flags, int e
BUG_ON(pid->ns_level > MAX_PID_NS_LEVEL || pid->ns_level <= 1);
for (i = 0; i < pid->ns_level; i++)
tids[i] = pid->ns[i].ns_pid;
pr_info("clone3 nested set_tid flags=0x%x size=%d tids=%d/%d/%d/%d\n",
flags, pid->ns_level,
tids[0],
pid->ns_level > 1 ? tids[1] : -1,
pid->ns_level > 2 ? tids[2] : -1,
pid->ns_level > 3 ? tids[3] : -1);

if (!(flags & CLONE_PARENT)) {
if (exit_signal != SIGCHLD) {
Expand All @@ -112,6 +121,13 @@ int clone3_with_nested_pid_noasan(int (*fn)(void *), void *arg, int flags, int e
c_args.set_tid = ptr_to_u64(tids);
c_args.set_tid_size = pid->ns_level;
pid_ret = syscall(__NR_clone3, &c_args, sizeof(c_args));
if (pid_ret < 0)
pr_perror("clone3 nested set_tid failed flags=0x%x size=%d tids=%d/%d/%d/%d",
flags, pid->ns_level,
tids[0],
pid->ns_level > 1 ? tids[1] : -1,
pid->ns_level > 2 ? tids[2] : -1,
pid->ns_level > 3 ? tids[3] : -1);
if (pid_ret == 0)
exit(fn(arg));
return pid_ret;
Expand Down
3 changes: 3 additions & 0 deletions criu/criu/cr-dump.c
Original file line number Diff line number Diff line change
Expand Up @@ -2360,6 +2360,9 @@ int cr_dump_tasks(pid_t pid)
if (dump_zombies())
goto err;

if (finalize_nested_pid_ns_ids())
goto err;

if (dump_pstree(root_item))
goto err;

Expand Down
87 changes: 82 additions & 5 deletions criu/criu/cr-restore.c
Original file line number Diff line number Diff line change
Expand Up @@ -186,9 +186,32 @@ static int __restore_wait_inprogress_tasks(int participants)
int ret;
futex_t *np = &task_entries->nr_in_progress;

futex_wait_while_gt(np, participants);
if (opts.tfork.active) {
int waited;

for (waited = 0; waited < 100; waited++) {
if ((int)futex_get(np) <= participants)
break;
usleep(100000);
}

if ((int)futex_get(np) > participants) {
pr_err("tfork restore wait timed out: participants=%d nr_in_progress=%d start_stage=%d nr_tasks=%d nr_threads=%d nr_helpers=%d\n",
participants, (int)futex_get(np),
(int)futex_get(&task_entries->start),
task_entries->nr_tasks, task_entries->nr_threads,
task_entries->nr_helpers);
return -ETIMEDOUT;
}
} else {
futex_wait_while_gt(np, participants);
}

ret = (int)futex_get(np);
if (ret < 0) {
pr_err("restore wait aborted: participants=%d nr_in_progress=%d start_stage=%d task_cr_err=%d\n",
participants, ret, (int)futex_get(&task_entries->start),
get_task_cr_err());
set_cr_errno(get_task_cr_err());
return ret;
}
Expand Down Expand Up @@ -227,6 +250,17 @@ static inline void __restore_switch_stage(int next_stage)

static int restore_switch_stage(int next_stage)
{
if (opts.tfork.active)
pr_warn("tfork: restore_switch_stage %d participants=%d nr_tasks=%d nr_threads=%d nr_helpers=%d\n",
next_stage, stage_participants(next_stage),
task_entries->nr_tasks, task_entries->nr_threads,
task_entries->nr_helpers);
else
pr_info("restore_switch_stage %d participants=%d nr_tasks=%d nr_threads=%d nr_helpers=%d\n",
next_stage, stage_participants(next_stage),
task_entries->nr_tasks, task_entries->nr_threads,
task_entries->nr_helpers);

__restore_switch_stage(next_stage);
return restore_wait_inprogress_tasks();
}
Expand Down Expand Up @@ -1395,6 +1429,16 @@ static inline int fork_with_pid(struct pstree_item *item)

ca.item = item;
ca.clone_flags = rsti(item)->clone_flags;
if (opts.tfork.active && item != root_item &&
!(ca.clone_flags & CLONE_NEWPID) &&
item->pid->ns_level > 1 &&
item->pid->ns[0].ns_pid == INIT_PID) {
pr_info("tfork: repairing missing CLONE_NEWPID for pidns init uid=%d local=%d parent_local=%d level=%d\n",
uid(item), pid,
item->parent ? localpid(item->parent) : -1,
item->pid->ns_level);
ca.clone_flags |= CLONE_NEWPID;
}

BUG_ON(ca.clone_flags & CLONE_VM);

Expand Down Expand Up @@ -1434,27 +1478,59 @@ static inline int fork_with_pid(struct pstree_item *item)
strip |= CLONE_NEWUSER;

if (kdat.has_clone3_set_tid) {
if (item->pid->ns_level == 1)
if (opts.tfork.active && (ca.clone_flags & CLONE_NEWPID)) {
pr_info("tfork: restore pidns init uid=%d local pid %d with fresh parent pid, dumped chain level=%d\n",
uid(item), pid, item->pid->ns_level);
ret = clone3_with_pid_noasan(restore_task_with_children, &ca,
ca.clone_flags & ~strip, SIGCHLD, pid);
else
} else if (item->pid->ns_level == 1)
ret = clone3_with_pid_noasan(restore_task_with_children, &ca,
ca.clone_flags & ~strip, SIGCHLD, pid);
else {
struct pid tfork_pid = {};
struct pid *restore_pid = item->pid;

if (opts.tfork.active && (root_ns_mask & CLONE_NEWPID) &&
root_item && root_item->pid->ns_level > 1 &&
item->pid->ns_level > 1) {
tfork_pid = *item->pid;
tfork_pid.ns_level--;
restore_pid = &tfork_pid;
pr_info("tfork: restore pid uid=%d local=%d with rebased pid chain level %d -> %d\n",
uid(item), pid, item->pid->ns_level,
restore_pid->ns_level);
}
ret = clone3_with_nested_pid_noasan(restore_task_with_children, &ca,
ca.clone_flags & ~strip,
SIGCHLD, item->pid);
SIGCHLD, restore_pid);
}
} else {
BUG_ON(item->pid->ns_level >= 1);
close_pid_proc();
ret = clone_noasan(restore_task_with_children,
(ca.clone_flags & ~strip) | SIGCHLD, &ca);
}
if (ret < 0) {
pr_err("fork_with_pid failed item uid=%d local=%d real=%d parent_local=%d flags=0x%lx stripped_flags=0x%lx ns_level=%d root_ns_mask=0x%lx tfork=%d\n",
uid(item), pid, realpid(item),
item->parent ? localpid(item->parent) : -1,
ca.clone_flags, ca.clone_flags & ~strip,
item->pid->ns_level, root_ns_mask,
opts.tfork.active ? 1 : 0);
if (item->pid->ns_level > 0)
pr_err("fork_with_pid pid chain uid=%d ns=%d/%d/%d/%d\n",
uid(item),
item->pid->ns[0].ns_pid,
item->pid->ns_level > 1 ? item->pid->ns[1].ns_pid : -1,
item->pid->ns_level > 2 ? item->pid->ns[2].ns_pid : -1,
item->pid->ns_level > 3 ? item->pid->ns[3].ns_pid : -1);
pr_perror("Can't fork for %d", pid);
if (errno == EEXIST)
set_cr_errno(EEXIST);
goto err_unlock;
}

if (item == root_item) {
if (opts.tfork.active || item == root_item) {
item->pid->real = ret;
pr_debug("PID: real %d virt %d\n", item->pid->real, localpid(item));
}
Expand Down Expand Up @@ -4045,6 +4121,7 @@ static int sigreturn_restore(struct task_restore_args *task_args, unsigned long
task_args->vdso_rt_size = vdso_rt_size;
task_args->can_map_vdso = kdat.can_map_vdso;
task_args->has_clone3_set_tid = kdat.has_clone3_set_tid;
task_args->tfork_active = opts.tfork.active;

new_sp = restorer_stack(task_args->t->mz);

Expand Down
15 changes: 9 additions & 6 deletions criu/criu/cr-tfork.c
Original file line number Diff line number Diff line change
Expand Up @@ -271,7 +271,7 @@ int tfork_read_cropt(void)
return 0;

snap_path = opts.tfork.snap_root;
if (!snap_path && opts.tfork.copies > 1 && opts.tfork.snap_roots &&
if (!snap_path && opts.tfork.copies >= 1 && opts.tfork.snap_roots &&
opts.tfork.copy_idx < opts.tfork.snap_roots_n)
snap_path = opts.tfork.snap_roots[opts.tfork.copy_idx];

Expand Down Expand Up @@ -966,8 +966,9 @@ int cr_tfork_tasks(pid_t pid)
opts.tfork.pidfd_map[opts.tfork.pidfd_map_nr].pidfd = pidfd;
opts.tfork.pidfd_map[opts.tfork.pidfd_map_nr].memfd = -1;
opts.tfork.pidfd_map_nr++;
pr_info("tfork: pidfd %d for pid %d (vpid %d uid %d)\n",
pidfd, item->pid->real, localpid(item), uid(item));
pr_info("tfork: pidfd %d for pid %d (vpid %d uid %d nsid %d level %d)\n",
pidfd, item->pid->real, localpid(item), uid(item),
item->pid->leaf_ns_id, item->pid->ns_level);
}

ret = run_scripts(ACT_PRE_TFORK_RESTORE);
Expand Down Expand Up @@ -1078,7 +1079,7 @@ int cr_tfork_tasks(pid_t pid)
list_for_each_entry(cgo_iter, &opts.new_cgroup_roots, node)
rpc_n_cg_root++;

rpc_max = 32 + 2 * (rpc_n_ifd + rpc_n_ext + rpc_n_cg_root
rpc_max = 33 + 2 * (rpc_n_ifd + rpc_n_ext + rpc_n_cg_root
+ opts.tfork.snap_mount_n)
+ rpc_n_copy_args + 2;
rpc_argv = calloc(rpc_max, sizeof(*rpc_argv));
Expand All @@ -1098,6 +1099,7 @@ int cr_tfork_tasks(pid_t pid)
rpc_argv[rpc_n++] = "-o";
rpc_argv[rpc_n++] = restore_log_arg;
rpc_argv[rpc_n++] = "-v2";
rpc_argv[rpc_n++] = "--keep-pid-hierarchy";

if (opts.root) {
rpc_argv[rpc_n++] = "--root";
Expand Down Expand Up @@ -1171,7 +1173,7 @@ int cr_tfork_tasks(pid_t pid)
rpc_argv[rpc_n++] = "--tfork-snap-mounts";
rpc_argv[rpc_n++] = snap_mounts_csv;
}
if (opts.tfork.copies > 1) {
if (opts.tfork.copies >= 1) {
snprintf(copies_arg, sizeof(copies_arg),
"%d", opts.tfork.copies);
rpc_argv[rpc_n++] = "--tfork-copies";
Expand Down Expand Up @@ -1267,7 +1269,7 @@ int cr_tfork_tasks(pid_t pid)
buf[off] = '\0';
end = buf + off;

argv_max = 8 + 2;
argv_max = 8 + 3;
for (p = buf; p < end; p++)
if (*p == '\0')
argv_max++;
Expand Down Expand Up @@ -1312,6 +1314,7 @@ int cr_tfork_tasks(pid_t pid)
argv_new[argc_new++] = "--pidfile";
argv_new[argc_new++] = pidfile_arg;
}
argv_new[argc_new++] = "--keep-pid-hierarchy";
argv_new[argc_new] = NULL;

execv("/proc/self/exe", argv_new);
Expand Down
17 changes: 12 additions & 5 deletions criu/criu/crtools.c
Original file line number Diff line number Diff line change
Expand Up @@ -332,25 +332,30 @@ int main(int argc, char *argv[], char *envp[])
if (opts.tree_id)
pr_warn("Using -t with criu restore is obsoleted\n");

if (opts.tfork.copies > 1) {
if (opts.tfork.active && opts.tfork.copies >= 1) {
int n = opts.tfork.copies, i;
pid_t *children;
int (*ready_pipes)[2];
int failed = 0;
const char *base_log = opts.output;

const int ns_flags =
CLONE_NEWPID | CLONE_NEWNS;
/*
* The n-copy helper must not create/occupy PID 1 in a new
* PID namespace. CRIU restores the real root task as PID 1
* from the image; if the helper has already consumed it,
* restore fails with EEXIST ("Can't fork for 1").
*/
const int ns_flags = CLONE_NEWNS;

if (!opts.tfork.active) {
pr_err("--tfork-copies>1 requires --tfork-restore "
pr_err("--tfork-copies requires --tfork-restore "
"(use 'criu tfork --tfork-copies=N', not "
"'criu restore --tfork-copies=N')\n");
return 1;
}

if (!opts.restore_detach) {
pr_err("--tfork-copies>1 requires --restore-detached\n");
pr_err("--tfork-copies requires --restore-detached\n");
return 1;
}

Expand Down Expand Up @@ -445,6 +450,8 @@ int main(int argc, char *argv[], char *envp[])
if (opts.tfork.snap_roots_n > 0)
opts.root = opts.tfork.snap_roots[i];

opts.keep_pid_hierarchy = 0;

if (tfork_load_ncopy_fabric(i)) {
pr_err("tfork-ncopy: copy %d fabric load failed\n",
i);
Expand Down
17 changes: 17 additions & 0 deletions criu/criu/fsnotify.c
Original file line number Diff line number Diff line change
Expand Up @@ -1037,6 +1037,23 @@ static int open_inotify_fd(struct file_desc *d, int *new_fd)
return -1;
}

if (opts.tfork.active) {
unsigned int skipped = 0;

list_for_each_entry(wd_info, &info->marks, list)
skipped++;

if (skipped)
pr_warn("tfork: restored inotify fd %#08x without %u watch mark(s)\n",
info->ife->id, skipped);

if (restore_fown(tmp, info->ife->fown))
close_safe(&tmp);

*new_fd = tmp;
return tmp < 0 ? -1 : 0;
}

list_for_each_entry(wd_info, &info->marks, list) {
pr_info("\tRestore 0x%x wd for %#08x\n", wd_info->iwe->wd, wd_info->iwe->id);
if (restore_one_inotify(tmp, wd_info)) {
Expand Down
1 change: 1 addition & 0 deletions criu/criu/include/namespaces.h
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,7 @@ extern int restore_mnt_ns(int rst, int *cwd_fd);

extern int dump_task_ns_ids(struct pstree_item *);
extern int predump_task_ns_ids(struct pstree_item *);
extern int finalize_nested_pid_ns_ids(void);
extern int rst_add_ns_id(unsigned int id, struct pstree_item *, struct ns_desc *nd);
extern struct ns_id *lookup_ns_by_id(unsigned int id, struct ns_desc *nd);

Expand Down
1 change: 1 addition & 0 deletions criu/criu/include/restorer.h
Original file line number Diff line number Diff line change
Expand Up @@ -242,6 +242,7 @@ struct task_restore_args {
int child_subreaper;
int membarrier_registration_mask;
bool has_clone3_set_tid;
bool tfork_active;

/*
* info about rseq from libc used to
Expand Down
Loading