Skip to content

sai: give rs274 its own tool mmap instead of truncating $HOME/.tool.mmap - #4385

Open
tzuohann wants to merge 1 commit into
LinuxCNC:2.9from
tzuohann:rs274-private-toolmmap
Open

sai: give rs274 its own tool mmap instead of truncating $HOME/.tool.mmap#4385
tzuohann wants to merge 1 commit into
LinuxCNC:2.9from
tzuohann:rs274-private-toolmmap

Conversation

@tzuohann

Copy link
Copy Markdown
Contributor

Running rs274 while a LinuxCNC session is up replaces that session's tool
table with the sample one (shipped with lcnc), until restart.

A lucky user will find that conflicting MDI or gcode stops running. A less
lucky user will find code running with offsets from sample tools - potentially
disastrous.

tool_mmap_creator() opens $HOME/.tool.mmap O_RDWR|O_CREAT|O_TRUNC
(tooldata_mmap.cc:33, :135) and sai calls it at driver.cc:570 — before
getopt() at :578, so it happens on every invocation, --help included, and
-t (read at :583) cannot prevent it.

io, milltask, halui and the Python bindings all map that same inode
MAP_SHARED. O_TRUNC keeps the inode, so nothing re-maps and nothing
errors — the running session simply starts using the sample table.

Reproduce:

rm -rf /tmp/rsx && mkdir -p /tmp/rsx
HOME=/tmp/rsx rs274 -g /dev/null
# /tmp/rsx/.tool.mmap, last_index=4, sample table

Patch:

Gives sai its own pid-qualified file under /tmp, unlinked on exit, so an
offline check can never touch a running session's table.
tool_mmap_close() already unlinks and tool_mmap_fname() already honours a
preset name; this only adds the setter to reach it. io and milltask are
unchanged.

Seen on 2.9.8, identical at 2.9.10.

Comment thread src/emc/sai/driver.cc
Comment thread src/emc/sai/driver.cc
@tzuohann
tzuohann force-pushed the rs274-private-toolmmap branch from 3c37f31 to 74522c6 Compare August 16, 2026 14:44
Comment thread src/emc/tooldata/tooldata_mmap.cc
tool_mmap_creator() opens the file O_RDWR|O_CREAT|O_TRUNC
(tooldata_mmap.cc:33, used at :135) and sai calls it at driver.cc:570 --
before getopt() at :578. Every rs274 invocation therefore empties
$HOME/.tool.mmap, including `rs274 --help` and including one that supplies
-t, since -t is not read until :583.

That file is not scratch space: tool_mmap_fname() builds it from
secure_getenv("HOME") with a fixed name, and io, milltask, halui and the
Python bindings all map that same inode MAP_SHARED. An offline parse run
beside a live session therefore replaces the running machine's tool table
with the compiled-in sample table. O_TRUNC preserves the inode, so nothing
re-maps and nothing is notified: the session simply observes its tools
change. Observed on a machine using [EMCIO]DB_PROGRAM -- 15 tools became the
4 sample entries mid-session, G43 applied 0.0000 for a tool that was no
longer in the table, and the tool-number/drawbar guard inhibited jog and
feed. DB_PROGRAM neither prevents nor repairs it: ioControl.cc creates the
mmap before the DB_ACTIVE branch, and io does not re-read afterwards.

Give sai its own file and unlink it on exit. tool_mmap_close() already
unlinks and tool_mmap_fname() already honours a preset filename -- this only
adds the setter to reach it. io and milltask are untouched and remain the
only creators of the shared file.

Two points from review, both addressed here:

mkstemp(), not a name built from the pid (grandixximo). TMPDIR is
world-writable and a pid is guessable, and the creator opens without O_EXCL
or O_NOFOLLOW, so a predictable name can be pre-created as a symlink and the
victim's rs274 then truncates the attacker's chosen file -- and an attacker
can blanket a pid range in advance. mkstemp() creates it atomically with
O_EXCL and mode 0600, and TOOL_MMAP_CREATOR_OPEN_FLAGS gains O_NOFOLLOW so
the creator refuses a symlink at that path even if one appears in the gap.

tool_mmap_close() is now safe as an atexit handler (BsAtHome). It called
exit(EXIT_FAILURE) when munmap failed, and calling exit() from within an
atexit handler is undefined behaviour; _exit would skip the remaining
handlers, so that is not the answer either. It now reports the failure,
closes the fd and returns.

Reproduce before the change:

    rm -rf /tmp/rsx && mkdir -p /tmp/rsx
    HOME=/tmp/rsx rs274 -g /dev/null
    # /tmp/rsx/.tool.mmap, last_index=4, holding
    # T1 z0.511 d0.125 / T2 z0.100 d0.0625 / T3 z1.273 d0.201 / T99999 P123

After: rs274 writes $TMPDIR/rs274.tool.mmap.XXXXXX, removes it on exit, and
does not open $HOME/.tool.mmap. Verified on a live machine: an rs274 run with
the real $HOME left last_index=15 and every tool untouched.
@tzuohann
tzuohann force-pushed the rs274-private-toolmmap branch from 74522c6 to 8c8e048 Compare August 17, 2026 02:31
Comment on lines +235 to 243
if (mmap_fd >= 0) {
close(mmap_fd);
mmap_fd = -1;
}
// Idempotent from here: a second call, or an atexit after an explicit
// close, returns at the tool_mmap_base guard above.
tool_mmap_base = (char*)0;
mmap_is_creator = false;
} //tool_mmap_close()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I should have checked this better when I made my other comment. But, now you also removed the close at the bottom of the function. That means you now will leak the descriptor.

The original code would leak a file on the filesystem, now it is the file descriptor. It can be fixed much easier by making it look like:

void tool_mmap_close()
{
    if (!tool_mmap_base) { return; }
    // flush mmapped file to filesystem
    if (msync(tool_mmap_base, TOOL_MMAP_SIZE, MS_SYNC) == -1) {
        perror("tool_mmap_close(): msync fail");
    }
    if (munmap(tool_mmap_base, TOOL_MMAP_SIZE) < 0) {
        perror("tool_mmap_close(): munmapfail");
    }
    if( unlink(tool_mmap_fname() )) {
        perror("tool_mmap_close(): unlink fail");
    }
    close(creator_fd);
} //tool_mmap_close()

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The close is still there, just not on creator_fd:

if (mmap_fd >= 0) {
close(mmap_fd);
mmap_fd = -1;
}

mmap_fd is set by both the creator and user paths after a successful mmap; both close exactly one descriptor. creator_fd is set by creator; user process leaves it 0, so close(creator_fd) there closes stdin. Same reason unlink is guarded by mmap_is_creator.

Full disclosure if not obvious, I have no training with software architecture. Have a background in scientific programming, but that's completely different stuff. So, if I don't understand basic things, just take it as a report instead of a pull request. That may make things go faster. I use claude for all this stuff, but I don't have enough time to properly learn software, not at 42. Just relaying what claude said after quizzing it repeatedly about making sure your comments are addressed.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No problem, most are not programmers by profession here. We're glad to have people participate and contribute!
Let me have a wider look at this code. Maybe I am barking up the wrong tree. However, there are many oddities with the tool database code, so a closer look may be warranted.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

cool lmk if this works

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants