Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
123 commits
Select commit Hold shift + click to select a range
e233f6a
Sessions that come back, own keys, pinch zoom, SSH server, web server…
claude Oct 24, 2026
0a52b5c
Native extractor: read a .tar.zst to its end, so that zstd checks its…
claude Oct 24, 2026
575bb78
Emulator test: export ssh_login, which expect runs in a new bash
claude Oct 24, 2026
35abde8
Emulator test: align cortex-emu-runner for 16 KB pages
claude Oct 24, 2026
49164be
Native extractor: decompress a .tar.zst on a thread of its own
claude Oct 24, 2026
ba1a75e
Hook: the app's files are root's in stat(), as the programs are root
claude Oct 24, 2026
6e98a24
Changelog: git's ownership check is fixed, not restored
claude Oct 24, 2026
45ecea3
Hook: accept() as accept4(), the call Android's seccomp filter is mad…
claude Oct 24, 2026
9a61e5b
Emulator test: time limits on adb, no packages on the 16 KB image
claude Oct 24, 2026
cc7e60a
Hook: sshd's privilege-separated user has root's ids
claude Oct 24, 2026
5f7e2fc
Hook: the fortified getgroups and recvfrom go through it too
claude Oct 24, 2026
2f15a34
Changelog: the emulator test's 16 KB run, ownership check and adb limits
claude Oct 24, 2026
1d4ffa7
Hook: sandbox limits that take away descriptors are not applied
claude Oct 24, 2026
c521330
Hook: closefrom() and close_range() leave the ARM translator's memory…
claude Oct 24, 2026
3993d55
Hook: know the ARM translator's memory file by its name
claude Oct 24, 2026
ce61c6e
Emulator test: record what a program sees of its executable and descr…
claude Oct 24, 2026
9054195
Static Go programs see the rootfs: Ubuntu's paths and programs for Go
claude Oct 25, 2026
f3cbede
Merge the Go support onto the branch's merged history (v1.26.38 was s…
claude Oct 25, 2026
fcb2f3b
A new look around the terminal: tabs, themed keys and drawer, seven m…
claude Oct 25, 2026
649be84
Scroll position and a way back to the end, a themed first-start dialo…
claude Oct 25, 2026
545da3c
Go programs patched in normal mode keep working in root mode; README
claude Oct 25, 2026
ce39a5b
Website and README: Go programs no longer need root mode; tabs and co…
claude Oct 25, 2026
ddd5c33
Tests: the tabs, keys, search bar, layouts, first-start dialog and sc…
claude Oct 25, 2026
721e363
Go programs: CORTEX_NO_GOSYS switch and an unpatched control in the e…
claude Oct 25, 2026
7812fe5
Settings theme: the interface's own containers and outline colors, no…
claude Oct 25, 2026
adc951a
Emulator test: a second session is started, and the scheme picker is …
claude Oct 25, 2026
ef79aa8
Scheme picker in the interface's colors; the emulator test retries op…
claude Oct 25, 2026
7b079fa
README and website: screenshots of the new interface, and the scheme …
claude Oct 25, 2026
56e138d
Package compatibility run: apt-get is not started by timeout in the b…
claude Oct 25, 2026
2316c10
Version 1.26.39: Go programs see Ubuntu, session tabs, seven more col…
claude Oct 25, 2026
cd303dd
Hook: Rust programs no longer crash in statx; reading /proc/self/fd o…
claude Oct 25, 2026
7d467e2
Merge the branch's merged history (PR #28 was squashed into main)
claude Oct 25, 2026
c39b5f5
Hook: shadow database and SELinux for Ubuntu's programs, the translat…
claude Oct 25, 2026
af04acc
Fix unclosed Kotlin comment in BaseUsers KDoc
claude Oct 25, 2026
4bc81d7
Version 1.26.40: faster program starts, sudo/su without root, Rust, r…
claude Oct 25, 2026
f98f6a1
Compat run: a MariaDB row; docs say MariaDB and redis run
claude Oct 25, 2026
958bd68
Emulator test: base users and groups, sudo
claude Oct 25, 2026
f1101fc
A color scheme chosen later reaches the cells of the sessions (#29)
claude Oct 25, 2026
29310aa
Review fixes: scheme remap by role, slots of vfork children given bac…
claude Oct 25, 2026
a56e0f4
Merge the branch's merged history (PR #31 was squashed into main)
claude Oct 26, 2026
5fedf73
No more ~/storage link to the shared storage (1.26.41)
claude Oct 26, 2026
bcb86ef
Fix the unclosed comment in the storage-link KDoc; test that no Kotli…
claude Oct 26, 2026
0803909
Test that a ~/storage link to another place is left alone
claude Oct 26, 2026
2474682
Start the next change from main
claude Oct 26, 2026
8cc804b
New app icon, held backspace keeps deleting, copy bar in the right pl…
claude Oct 26, 2026
5c9e1af
Chinese, Japanese, Korean and emoji take two cells instead of overlap…
claude Oct 27, 2026
0d6afc9
Start the next change from main
claude Oct 27, 2026
c5829ae
Fix a half left behind after inserting over a wide character; keep th…
claude Oct 27, 2026
f46bd24
Copy: a selection that ends on the first half of an emoji takes the w…
claude Oct 27, 2026
de3b864
Wide characters: mark the wrap padding, blank halves fully, cut copy;…
claude Oct 27, 2026
c9eb9df
Start the next change from main
claude Oct 27, 2026
4ae2b29
A cleaner drawer and tabs, and a white color scheme; 1.26.44
claude Oct 27, 2026
abc4169
Test the session tabs row by row, and their screen reader labels
claude Oct 27, 2026
515d3c7
Settings: the back arrow follows the color scheme
claude Oct 27, 2026
1d22b55
e2e: scroll the settings list all the way up before choosing the whit…
claude Oct 27, 2026
73f39f1
e2e: give the terminal the focus before typing in the second session
claude Oct 27, 2026
ed8ea5e
Hook: the real path of an open file keeps the rootfs in front of it
claude Oct 27, 2026
a72c5b1
Terminal: a character of the supplementary planes that wcwidth() give…
claude Oct 27, 2026
e6cfa61
Test: compare the colors with the ones the escape codes set
claude Oct 27, 2026
662a6ea
Review: fd links only for fd/N, the root tab says root, a plain curso…
claude Oct 27, 2026
07ce921
e2e: pictures for the README (neofetch, the end of the settings)
claude Oct 27, 2026
0d655de
Merge the earlier history of this branch (squash-merged as #37)
claude Oct 27, 2026
6157483
README and website: the session cards have no directory in their pict…
claude Oct 27, 2026
9b39eb7
e2e: neofetch for the README with the small logo and lines that fit
claude Oct 27, 2026
9d4fb47
tests/e2e: a script that makes the README's pictures from an emulator…
claude Oct 27, 2026
d085168
e2e: neofetch for the README without the emulator's model and archite…
claude Oct 27, 2026
dc33763
README and website pictures from the 1.26.44 emulator run
claude Oct 27, 2026
a342bd2
e2e: the neofetch picture only after a successful install and a finis…
claude Oct 27, 2026
bf700b5
The keyboard's Select all, Copy, Cut and Paste work in the terminal, …
claude Oct 28, 2026
94fc8d4
Merge the earlier history of this branch (squash-merged as #37 and #38)
claude Oct 28, 2026
c09f130
Review: Cut also does what delete does, the selection is the session'…
claude Oct 28, 2026
b3c825a
Review: the selected text is read under the emulator's lock
claude Oct 28, 2026
9855312
Go programs built for Android get Cortex's change, micro no longer ha…
ssfuisu Oct 30, 2026
4a0c199
Merge the earlier history of this branch (squash-merged as #37, #38, …
ssfuisu Oct 30, 2026
08c6397
Review: a clipboard note from the future is ignored, a timed-out clip…
ssfuisu Oct 30, 2026
372d6bc
The clipboard commands keep their deadline inside cortex-api (read -t…
ssfuisu Oct 30, 2026
49e591b
A directory that has lost its owner's rights is repaired when a name …
ssfuisu Oct 31, 2026
b0a4808
Merge the earlier history of this branch (squash-merged as #45)
ssfuisu Oct 31, 2026
2435128
The directory repair test also runs as a user that is not root but ha…
ssfuisu Oct 31, 2026
070181b
Review: the emulated hard link is looked up again after a directory w…
ssfuisu Oct 31, 2026
a108995
Hardware keyboard Ctrl, Alt and Shift reach the terminal; system() an…
ssfuisu Nov 2, 2026
54ad7bb
Review: system(NULL) checks for Ubuntu's shell, concurrent system() c…
ssfuisu Nov 2, 2026
536394a
Keep the branch on main after the 1.26.47 squash merge
claude Nov 2, 2026
b2e5618
Programs a shell starts take fewer page faults: the exec code is prep…
claude Nov 4, 2026
ee332e8
1.26.48: version and changelog
claude Nov 4, 2026
5789211
Review: the fork preparation does not wait for its lock (a signal han…
claude Nov 4, 2026
c134b52
Keep the branch on main after the 1.26.48 squash merge
claude Nov 4, 2026
71bd87b
Servers on a port the kernel picked are no longer connected to by the…
ssfuisu Nov 7, 2026
842ee05
Website: animated redesign
ssfuisu Nov 7, 2026
f4ffbfb
Review: a new socket starts without a chosen port, the qemu test find…
ssfuisu Nov 7, 2026
fdf9341
Pick the test port below the kernel's range, softer button shine in l…
ssfuisu Nov 7, 2026
76ced74
1.26.49
ssfuisu Nov 7, 2026
71394e0
Merge branch after squash
ssfuisu Nov 7, 2026
d549b0f
Website: drawn SVG icons instead of emoji
ssfuisu Nov 7, 2026
ac61da7
Merge branch after squash
ssfuisu Nov 7, 2026
76dade0
README: the phone's files are at /sdcard, near the top
ssfuisu Nov 7, 2026
9b3aa5d
Merge branch after README push
ssfuisu Nov 7, 2026
b9e3275
Programs find libraries through their RUNPATH (adb, fastboot); 1.26.50
ssfuisu Nov 7, 2026
f032f44
RUNPATH: search order as on Linux, any number of headers
ssfuisu Nov 7, 2026
7081ddd
Merge branch after squash
ssfuisu Nov 7, 2026
7bd9964
Desktop (X11) add-on, ld.so.conf.d/ldconfig/ldd, SysV shared memory; …
ssfuisu Nov 8, 2026
776bcb5
Review fixes: ld.so with its own --library-path, ldconfig marks ld.so…
ssfuisu Nov 8, 2026
5cfa504
Desktop and hook review fixes
ssfuisu Nov 8, 2026
e62136c
Merge branch state after #57 [skip ci]
ssfuisu Nov 8, 2026
efc45d6
Drawer: the desktop as a monitor icon next to the update check
ssfuisu Nov 8, 2026
a7e2789
Install: read the archive from the APK file with a CRC check, never m…
ssfuisu Nov 8, 2026
1883d92
installBootstrap: progress stays the last parameter (the trailing lam…
ssfuisu Nov 8, 2026
c6abc3f
Bootstrap reads: close the stream when the header read fails, copy fr…
ssfuisu Nov 8, 2026
c0cdec3
Merge branch state after #59 [skip ci]
ssfuisu Nov 8, 2026
b8d7e2d
SysV shm: a segment marked removed stays attachable for a while (GTK'…
ssfuisu Nov 9, 2026
4363f0b
shm: measure the IPC_RMID grace period with the boot clock
ssfuisu Nov 9, 2026
8d603dc
Merge branch state after 1.27.3
ssfuisu Nov 9, 2026
f7b7bbd
1.27.4: reload the custom terminal font when its file changes (#58)
ssfuisu Nov 9, 2026
42e651b
Merge branch state after 1.27.4
ssfuisu Nov 9, 2026
33acec7
Bug report: the desktop's state and its log
ssfuisu Nov 11, 2026
a4c9d50
1.27.5: XFCE runs (session bus, file type database, libpulse), PulseA…
ssfuisu Nov 11, 2026
4980a6e
Review: loader order and relative paths in dlopen, recvmmsg, desktop …
ssfuisu Nov 11, 2026
06734df
1.27.5.1: libraries' RUNPATH directories, locks on a memfd, fewer rea…
ssfuisu Nov 11, 2026
853aed6
Review: memfd locks by the memfd's inode, RTLD_DEEPBIND, 32-bit names…
ssfuisu Nov 11, 2026
1290602
32-bit: the headers already give these hooks their time64 names
ssfuisu Nov 11, 2026
83c894e
Merge branch state after 1.27.5.1
ssfuisu Nov 12, 2026
2c2e95c
1.27.6: selected text stays on the desktop, lstat keeps link sizes, r…
ssfuisu Nov 12, 2026
d056968
Review: /proc links are not kept, atomic cache fields, readlink of 0 …
ssfuisu Nov 12, 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
25 changes: 25 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,31 @@ What changed in each version of Cortex Terminal, newest first. The release workf
of a GitHub release from the section of its version here. Older versions are described on the
[GitHub releases](https://github.com/ssfuisu/cortex/releases) page.

## 1.27.6 (2026-11-12)

### Fixed

- **Selecting text on the desktop replaced what had been copied on the phone.** The X server sent
Android not only what a program copied (the CLIPBOARD) but also every piece of text that was only
selected (the PRIMARY), so a drag of the mouse over a word in a terminal or an editor wiped the
phone's clipboard. Only what is copied goes to Android now; text copied on the phone still pastes
on the desktop with Ctrl+V and with the middle button.
- **`readlink` printed long link targets cut off.** A target that Cortex rewrites is longer as the
phone stores it (it starts with the app's own directory), and a program that read it into a small
buffer got it cut off before it was rewritten, shorter than the buffer, so it took that for the
whole target: coreutils' `readlink`, which starts with 64 bytes, printed
`/usr/share/postgresql/16/man/man7/ALTER_TEXT_SEA`. Into a buffer smaller than that the phone's own
path could even come out. The target is now read whole and rewritten before it is cut to the
buffer, as Linux does.

### Changed

- **lstat() of a link is faster.** The size of a link that Cortex rewrites (those of
/etc/alternatives, long absolute ones) was found by reading the link again at every lstat() and
fstatat(): ls -l, cp -a, tar and find paid that for each of them. The size is now kept while the
link stays the same. Measured on an x86 machine, lstat() of /etc/alternatives/awk went from about
1,190 ns to 735 ns (530 ns without Cortex). The benchmark has a test for it: `lstat_alt_link`.

## 1.27.5.1 (2026-11-11)

### Fixed
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,7 @@ Measured on one 8-core Android phone (Linux 4.14, one big core, median of 9 inte
| `write()`, `dup()` + `close()`, `getcwd()`, `sigprocmask()` | up to +21 ns | up to +21 ns |
| Computation without system calls | 0 | 0 |

`lstat()` of a symbolic link stays expensive because the library reads the link to report its size the way Ubuntu does.
`lstat()` of a symbolic link stays the most expensive of these because the library reads the link to report its size the way Ubuntu does. Since 1.27.6 it reads each link once and keeps the size while the link stays the same (on an x86 machine, `lstat()` of `/etc/alternatives/awk` went from about 1,190 ns to 735 ns; 530 ns without the library).

**Starting a program** (what a shell does for every command): the library made 19 system calls before the program started and now makes 7, no longer changes the permissions of `ld.so` each time, and lists the library directories that hold nearly every library first, so a program no longer pays two failed lookups per library (`apt-cache` made 34 of them, `bash` 4, now none). Starting a program through the library takes 769 µs instead of 903 µs; starting `ld.so` by hand without the library takes 491 µs.

Expand Down
4 changes: 2 additions & 2 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@ android {
// from the app's writable data directory (filesDir). Android 10+ (targetSdk >= 29)
// enforces SELinux W^X restrictions preventing execution from writable app data storage.
targetSdk = 28
versionCode = 12705
versionName = "1.27.5.1"
versionCode = 12706
versionName = "1.27.6"
// UbuntuOnDeviceTest (app/src/androidTest), on Firebase Test Lab
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"

Expand Down
4 changes: 3 additions & 1 deletion app/src/main/assets/scripts/cortex-desktop.sh
Original file line number Diff line number Diff line change
Expand Up @@ -369,8 +369,10 @@ cmd_start() {
# -rfbport -1: no TCP port, the Unix socket only (mode 0600: this app's user alone). setsid: a
# session of its own, so that the end of the terminal that started it (the app runs this command
# on a pseudo-terminal of its own) sends it no SIGHUP, which makes an X server reset.
# -SendPrimary=0: only what is copied (the CLIPBOARD) goes to Android's clipboard. Text that is
# only selected (the PRIMARY) replaced what had been copied on the phone, at every selection.
setsid Xvnc ":$display" -geometry "$size" -depth 24 -rfbunixpath "$SOCKET" -rfbunixmode 0600 -rfbport -1 \
-SecurityTypes None -nolisten tcp -AlwaysShared -AcceptSetDesktopSize -dpi 120 \
-SecurityTypes None -nolisten tcp -AlwaysShared -AcceptSetDesktopSize -dpi 120 -SendPrimary=0 \
</dev/null >>"$LOG" 2>&1 9>&- &
pid=$!
echo "$pid" >"$STATE/xvnc.pid"
Expand Down
42 changes: 36 additions & 6 deletions app/src/main/cpp/hook/fileops.c
Original file line number Diff line number Diff line change
Expand Up @@ -298,8 +298,25 @@ int renameat2(int olddirfd, const char *oldpath, int newdirfd, const char *newpa
return ret;
}

// A target the hook rewrites is longer as the kernel stores it (it starts with the rootfs path). Read
// straight into a small buffer, it was cut off before it was rewritten, and the shorter result told
// the caller it had all of it: coreutils' readlink, which starts with 64 bytes, printed
// "/usr/share/postgresql/16/man/man7/ALTER_TEXT_SEA". A buffer smaller than any path is filled from
// one that is not, and cut off afterwards, as the kernel does it (the whole size: the caller asks again).
static ssize_t readlink_into(char *buf, size_t bufsiz, const char *full, ssize_t len) {
if (len < 0) return len;
size_t n = (size_t)len < bufsiz ? (size_t)len : bufsiz;
memcpy(buf, full, n);
return (ssize_t)n;
}

// Hook readlink
ssize_t readlink(const char *pathname, char *buf, size_t bufsiz) {
// as the kernel answers it (the target is read into a buffer of the hook's own below)
if (bufsiz == 0) {
errno = EINVAL;
return -1;
}
if (is_proc_self_exe(pathname)) {
ssize_t exe_len = handle_proc_self_exe(buf, bufsiz);
if (exe_len >= 0) return exe_len;
Expand All @@ -308,17 +325,26 @@ ssize_t readlink(const char *pathname, char *buf, size_t bufsiz) {
if (!orig_readlink) orig_readlink = (ssize_t (*)(const char *, char *, size_t))dlsym(RTLD_NEXT, "readlink");
char pbuf[PATH_MAX];
const char *target = rewrite_path(pathname, pbuf, sizeof(pbuf));
ssize_t ret = orig_readlink ? orig_readlink(target, buf, bufsiz) : -1;
char full[PATH_MAX];
char *into = bufsiz < sizeof(full) ? full : buf;
size_t into_size = bufsiz < sizeof(full) ? sizeof(full) : bufsiz;
Comment thread
ssfuisu marked this conversation as resolved.
Resolved
ssize_t ret = orig_readlink ? orig_readlink(target, into, into_size) : -1;
if (ret < 0 && errno == ENOENT) {
char alt[PATH_MAX];
const char *retry = usrmerge_retry_path(target, alt, sizeof(alt));
if (retry) ret = orig_readlink(retry, buf, bufsiz);
if (retry) ret = orig_readlink(retry, into, into_size);
}
return postprocess_readlink(pathname, buf, ret, bufsiz);
ret = postprocess_readlink(pathname, into, ret, into_size);
return into == buf ? ret : readlink_into(buf, bufsiz, full, ret);
}

// Hook readlinkat
ssize_t readlinkat(int dirfd, const char *pathname, char *buf, size_t bufsiz) {
// as the kernel answers it (the target is read into a buffer of the hook's own below)
if (bufsiz == 0) {
errno = EINVAL;
return -1;
}
if (is_proc_self_exe(pathname)) {
ssize_t exe_len = handle_proc_self_exe(buf, bufsiz);
if (exe_len >= 0) return exe_len;
Expand All @@ -327,13 +353,17 @@ ssize_t readlinkat(int dirfd, const char *pathname, char *buf, size_t bufsiz) {
if (!orig_readlinkat) orig_readlinkat = (ssize_t (*)(int, const char *, char *, size_t))dlsym(RTLD_NEXT, "readlinkat");
char pbuf[PATH_MAX];
const char *target = (pathname && pathname[0] == '/') ? rewrite_path(pathname, pbuf, sizeof(pbuf)) : pathname;
ssize_t ret = orig_readlinkat ? orig_readlinkat(dirfd, target, buf, bufsiz) : -1;
char full[PATH_MAX];
char *into = bufsiz < sizeof(full) ? full : buf;
size_t into_size = bufsiz < sizeof(full) ? sizeof(full) : bufsiz;
ssize_t ret = orig_readlinkat ? orig_readlinkat(dirfd, target, into, into_size) : -1;
if (ret < 0 && errno == ENOENT && pathname && pathname[0] == '/') {
char alt[PATH_MAX];
const char *retry = usrmerge_retry_path(target, alt, sizeof(alt));
if (retry) ret = orig_readlinkat(dirfd, retry, buf, bufsiz);
if (retry) ret = orig_readlinkat(dirfd, retry, into, into_size);
}
return postprocess_readlink(pathname, buf, ret, bufsiz);
ret = postprocess_readlink(pathname, into, ret, into_size);
return into == buf ? ret : readlink_into(buf, bufsiz, full, ret);
}

// Fortified readlink variants used by dpkg / update-alternatives
Expand Down
124 changes: 104 additions & 20 deletions app/src/main/cpp/hook/files.c
Original file line number Diff line number Diff line change
Expand Up @@ -493,6 +493,108 @@ int stat(const char *pathname, struct stat *statbuf) {
return ret;
}

// ---- the size lstat() reports for a link ---------------------------------------------------
// The size of a link is the length of its target, and the hook reports it as readlink() returns
// the target: rewritten. Finding that out read the link a second time, on every lstat() of a link
// that can be rewritten (those of /etc/alternatives, long absolute ones): ls -l, cp -a, tar and
// find did that for each of them. A link's target cannot change while the link exists (a new
// target is a new link), so the size is kept, by the link's inode, the time it was made, its own
// size and the path it was asked for by. No lock: each place has a sequence number that is odd
// while it is written, and a reader that sees it change takes nothing (a fork can happen at any
// moment, and a child must never wait for a lock that another thread held).
#define LINK_SIZE_SLOTS 128

static struct link_size_slot {
uint32_t seq;
uint64_t dev, ino, path_hash;
int64_t ctime_sec;
int64_t ctime_nsec;
long long raw, size;
} g_link_sizes[LINK_SIZE_SLOTS];

static uint64_t link_path_hash(int dirfd, const char *path) {
uint64_t h = 1469598103934665603ULL ^ (uint64_t)(int64_t)dirfd; // FNV-1a
for (const unsigned char *p = (const unsigned char *)path; p && *p; p++) h = (h ^ *p) * 1099511628211ULL;
return h;
}

static struct link_size_slot *link_size_slot_for(const struct stat *st, uint64_t path_hash) {
uint64_t mix = (uint64_t)st->st_ino * 0x9E3779B97F4A7C15ULL ^ path_hash;
return &g_link_sizes[(mix >> 32) % LINK_SIZE_SLOTS];
}

#define LS_LOAD(field) __atomic_load_n(&slot->field, __ATOMIC_RELAXED)
#define LS_STORE(field, value) __atomic_store_n(&slot->field, (value), __ATOMIC_RELAXED)

// The size kept for this link, or -1
static long long link_size_cached(const struct stat *st, uint64_t path_hash) {
struct link_size_slot *slot = link_size_slot_for(st, path_hash);
uint32_t before = __atomic_load_n(&slot->seq, __ATOMIC_ACQUIRE);
if (before == 0 || (before & 1)) return -1;
int match = LS_LOAD(dev) == (uint64_t)st->st_dev && LS_LOAD(ino) == (uint64_t)st->st_ino &&
LS_LOAD(path_hash) == path_hash && LS_LOAD(ctime_sec) == (int64_t)st->st_ctim.tv_sec &&
LS_LOAD(ctime_nsec) == (int64_t)st->st_ctim.tv_nsec && LS_LOAD(raw) == (long long)st->st_size;
long long size = LS_LOAD(size);
__atomic_thread_fence(__ATOMIC_ACQUIRE);
if (__atomic_load_n(&slot->seq, __ATOMIC_RELAXED) != before) return -1;
return match ? size : -1;
}

static void link_size_keep(const struct stat *st, uint64_t path_hash, long long size) {
struct link_size_slot *slot = link_size_slot_for(st, path_hash);
uint32_t seq = __atomic_load_n(&slot->seq, __ATOMIC_RELAXED);
// another thread writes it: this one is not kept
if ((seq & 1) || !__atomic_compare_exchange_n(&slot->seq, &seq, seq + 1, 0, __ATOMIC_ACQUIRE, __ATOMIC_RELAXED)) return;
__atomic_thread_fence(__ATOMIC_RELEASE);
LS_STORE(dev, (uint64_t)st->st_dev);
LS_STORE(ino, (uint64_t)st->st_ino);
LS_STORE(path_hash, path_hash);
LS_STORE(ctime_sec, (int64_t)st->st_ctim.tv_sec);
LS_STORE(ctime_nsec, (int64_t)st->st_ctim.tv_nsec);
LS_STORE(raw, (long long)st->st_size);
LS_STORE(size, size);
__atomic_store_n(&slot->seq, seq + 2, __ATOMIC_RELEASE);
}

// Whether the link is one of /proc's (fd/N, cwd, exe...): those point elsewhere while they stay the
// same inode (dup2() gives fd/N another file), so their size is never kept. Also when it was asked
// for by a path relative to a directory of /proc: by the device, which is /proc's.
static int link_is_procs(const struct stat *st, const char *target) {
if (target && strncmp(target, "/proc/", 6) == 0) return 1;
static uint64_t proc_dev; // device of /proc plus one, 0: not known yet
uint64_t dev = __atomic_load_n(&proc_dev, __ATOMIC_RELAXED);
if (dev == 0) {
static int (*orig_lstat)(const char *, struct stat *) = NULL;
if (!orig_lstat) orig_lstat = (int (*)(const char *, struct stat *))dlsym(RTLD_NEXT, "lstat");
struct stat p;
if (!orig_lstat || orig_lstat("/proc/self", &p) != 0) return 1; // not known: not kept
dev = (uint64_t)p.st_dev + 1;
__atomic_store_n(&proc_dev, dev, __ATOMIC_RELAXED);
}
return (uint64_t)st->st_dev + 1 == dev;
}

// statbuf (of a link at pathname, which dirfd and target reach) with the size of the rewritten target
static void link_size_as_read(int dirfd, const char *pathname, const char *target, struct stat *statbuf) {
int keep = !link_is_procs(statbuf, target);
uint64_t hash = link_path_hash(dirfd, pathname);
long long kept = keep ? link_size_cached(statbuf, hash) : -1;
if (kept >= 0) {
statbuf->st_size = (off_t)kept;
return;
}
static ssize_t (*orig_readlinkat)(int, const char *, char *, size_t) = NULL;
if (!orig_readlinkat) orig_readlinkat = (ssize_t (*)(int, const char *, char *, size_t))dlsym(RTLD_NEXT, "readlinkat");
char link_buf[PATH_MAX];
ssize_t llen = orig_readlinkat ? orig_readlinkat(dirfd, target, link_buf, sizeof(link_buf) - 1) : -1;
if (llen <= 0) return;
ssize_t post_len = postprocess_readlink(pathname, link_buf, llen, sizeof(link_buf));
if (post_len > 0) {
if (keep) link_size_keep(statbuf, hash, (long long)post_len);
statbuf->st_size = post_len;
}
}

// Hook lstat
int lstat(const char *pathname, struct stat *statbuf) {
static int (*orig_lstat)(const char *, struct stat *) = NULL;
Expand All @@ -510,16 +612,7 @@ int lstat(const char *pathname, struct stat *statbuf) {
}
if (ret == 0 && statbuf && S_ISLNK(statbuf->st_mode) && g_cortex_root[0] != '\0' && target &&
readlink_result_may_change(pathname, (long long)statbuf->st_size)) {
char link_buf[PATH_MAX];
static ssize_t (*orig_readlink)(const char *, char *, size_t) = NULL;
if (!orig_readlink) orig_readlink = (ssize_t (*)(const char *, char *, size_t))dlsym(RTLD_NEXT, "readlink");
ssize_t llen = orig_readlink ? orig_readlink(target, link_buf, sizeof(link_buf) - 1) : -1;
if (llen > 0) {
ssize_t post_len = postprocess_readlink(pathname, link_buf, llen, sizeof(link_buf));
if (post_len > 0) {
statbuf->st_size = post_len;
}
}
link_size_as_read(AT_FDCWD, pathname, target, statbuf);
}
if (ret == 0) apply_stat_emulation(statbuf);
return ret;
Expand All @@ -542,16 +635,7 @@ int fstatat(int dirfd, const char *pathname, struct stat *statbuf, int flags) {
}
if (ret == 0 && statbuf && (flags & AT_SYMLINK_NOFOLLOW) && S_ISLNK(statbuf->st_mode) && g_cortex_root[0] != '\0' && target &&
readlink_result_may_change(pathname, (long long)statbuf->st_size)) {
char link_buf[PATH_MAX];
static ssize_t (*orig_readlinkat)(int, const char *, char *, size_t) = NULL;
if (!orig_readlinkat) orig_readlinkat = (ssize_t (*)(int, const char *, char *, size_t))dlsym(RTLD_NEXT, "readlinkat");
ssize_t llen = orig_readlinkat ? orig_readlinkat(dirfd, target, link_buf, sizeof(link_buf) - 1) : -1;
if (llen > 0) {
ssize_t post_len = postprocess_readlink(pathname, link_buf, llen, sizeof(link_buf));
if (post_len > 0) {
statbuf->st_size = post_len;
}
}
link_size_as_read(dirfd, pathname, target, statbuf);
}
if (ret == 0) apply_stat_emulation(statbuf);
return ret;
Expand Down
11 changes: 10 additions & 1 deletion tests/bench/micro.c
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,15 @@ static void t_lstat_file(long n, const char *root) {
for (long i = 0; i < n; i++) { if (lstat(p, &st) != 0) die("lstat", p); sink += (unsigned long long)st.st_size; }
}

static void t_lstat_alt_link(long n, const char *root) {
char p[PATH_MAX]; path_of(p, root, "/etc/alternatives/awk"); /* -> /usr/bin/mawk: a link the hook rewrites */
struct stat st;
for (long i = 0; i < n; i++) {
if (lstat(p, &st) != 0 || !S_ISLNK(st.st_mode)) die("lstat(alternatives link)", p);
sink += (unsigned long long)st.st_size;
}
}

static void t_lstat_link(long n, const char *root) {
char p[PATH_MAX]; path_of(p, root, "/bin"); /* a symlink to usr/bin on Ubuntu 24.04 */
struct stat st;
Expand Down Expand Up @@ -275,7 +284,7 @@ static void t_spawn_host(long n, const char *root) {

static const struct { const char *name; test_fn fn; } TESTS[] = {
{ "compute", t_compute }, { "stat", t_stat }, { "stat_deep", t_stat_deep }, { "stat_miss", t_stat_miss },
{ "stat_miss_usrbin", t_stat_miss_usrbin }, { "lstat_file", t_lstat_file }, { "lstat_link", t_lstat_link },
{ "stat_miss_usrbin", t_stat_miss_usrbin }, { "lstat_file", t_lstat_file }, { "lstat_link", t_lstat_link }, { "lstat_alt_link", t_lstat_alt_link },
{ "access", t_access }, { "open", t_open }, { "openat_rel", t_openat_rel }, { "fopen", t_fopen },
{ "readlink", t_readlink }, { "readdir", t_readdir }, { "realpath", t_realpath }, { "getcwd", t_getcwd },
{ "write_null", t_write_null }, { "dup_close", t_dup_close }, { "sigprocmask", t_sigprocmask },
Expand Down
Loading
Loading