7 Commits

Author SHA1 Message Date
ab1e847d1c Fix exploit with trailing whitespace 2026-02-24 00:48:20 -06:00
62a03bfe6b Potential windows case insensitive filesystem issue fix 2026-02-24 00:34:59 -06:00
e77a8b3636 AsyncSyscall3 exploit fix 2026-02-24 00:01:39 -06:00
6bb7f03a3e file permissions fixes 2026-02-23 23:50:37 -06:00
8798a2f4fe 2 potential vulnerability fixes 2026-02-23 23:26:21 -06:00
a6d2f6dca7 /home/user owned by user, user starts in cwd /home/user 2026-02-23 23:05:13 -06:00
b015d5880a load vuln fixed, sudo fixed 2026-02-23 22:43:12 -06:00
34 changed files with 444 additions and 220 deletions

View File

@@ -64,12 +64,18 @@ local newGid = resolveGid(groupStr)
local function chgrpPath(path) local function chgrpPath(path)
local stat = syscall.stat(path) local stat = syscall.stat(path)
if not stat then if not stat then
print(name .. ": cannot stat '" .. path .. "': No such file or directory") print(name .. ": cannot stat '" .. path .. "': no such file or directory")
return false return false
end end
local ok, err = pcall(syscall.chown, path, stat.owner, newGid) local ok, err = pcall(syscall.chown, path, stat.owner, newGid)
if not ok then if not ok then
print(name .. ": cannot change group of '" .. path .. "': " .. tostring(err)) local msg = tostring(err)
if msg:find("EPERM") or msg:find("EACCES") then
msg = "operation not permitted (must be root)"
elseif msg:find("ENOENT") then
msg = "no such file or directory"
end
print(name .. ": cannot change group of '" .. path .. "': " .. msg)
return false return false
end end
return true return true

View File

@@ -220,7 +220,13 @@ local function chmodPath(path)
local ok, cerr = pcall(syscall.chmod, path, newPerms) local ok, cerr = pcall(syscall.chmod, path, newPerms)
if not ok then if not ok then
print(name .. ": cannot change permissions of '" .. path .. "': " .. tostring(cerr)) local msg = tostring(cerr)
if msg:find("EACCES") or msg:find("EPERM") then
msg = "permission denied"
elseif msg:find("ENOENT") then
msg = "no such file or directory"
end
print(name .. ": cannot change permissions of '" .. path .. "': " .. msg)
return false return false
end end
return true return true

View File

@@ -95,14 +95,20 @@ end
local function chownPath(path) local function chownPath(path)
local stat = syscall.stat(path) local stat = syscall.stat(path)
if not stat then if not stat then
print(name .. ": cannot stat '" .. path .. "': No such file or directory") print(name .. ": cannot stat '" .. path .. "': no such file or directory")
return false return false
end end
local uid = newUid ~= nil and newUid or stat.owner local uid = newUid ~= nil and newUid or stat.owner
local gid = newGid ~= nil and newGid or stat.group local gid = newGid ~= nil and newGid or stat.group
local ok, err = pcall(syscall.chown, path, uid, gid) local ok, err = pcall(syscall.chown, path, uid, gid)
if not ok then if not ok then
print(name .. ": cannot change owner of '" .. path .. "': " .. tostring(err)) local msg = tostring(err)
if msg:find("EPERM") or msg:find("EACCES") then
msg = "operation not permitted (must be root)"
elseif msg:find("ENOENT") then
msg = "no such file or directory"
end
print(name .. ": cannot change owner of '" .. path .. "': " .. msg)
return false return false
end end
return true return true

View File

@@ -33,7 +33,6 @@ local host_str = syscall.getHost() or "Unknown"
local cc_ver = host_str:match("ComputerCraft ([%d%.]+)") or host_str local cc_ver = host_str:match("ComputerCraft ([%d%.]+)") or host_str
local info = { local info = {
-- {label, value} label=nil means print value as-is (userhost / separator)
{nil, userhost}, {nil, userhost},
{nil, string.rep("-", #userhost)}, {nil, string.rep("-", #userhost)},
{"OS", syscall.version() or "Unknown"}, {"OS", syscall.version() or "Unknown"},
@@ -42,7 +41,7 @@ local info = {
{"Uptime", formatUptime(syscall.getUptime() or 0)}, {"Uptime", formatUptime(syscall.getUptime() or 0)},
{"Tasks", tostring(#(syscall.getTasks() or {}))}, {"Tasks", tostring(#(syscall.getTasks() or {}))},
{"Shell", syscall.getEnviron("SHELL") or "Unknown"}, {"Shell", syscall.getEnviron("SHELL") or "Unknown"},
{"Terminal", "TTY1"}, {"Terminal", "tty1"},
{"UID", tostring(syscall.getuid())}, {"UID", tostring(syscall.getuid())},
{"Packages", "n/a (spm)"}, {"Packages", "n/a (spm)"},
} }
@@ -71,15 +70,12 @@ local lines = math.max(#logo, #info)
for i = 1, lines do for i = 1, lines do
local logo_str = logo[i] or string.rep(" ", 36) local logo_str = logo[i] or string.rep(" ", 36)
-- print logo segment in cyan
c(C_LOGO) c(C_LOGO)
printInline(logo_str) printInline(logo_str)
-- print separator pipe
c(C_LABEL) c(C_LABEL)
printInline("| ") printInline("| ")
-- print info segment
local row = info[i] local row = info[i]
if row then if row then
if row[1] == nil and i == 1 then if row[1] == nil and i == 1 then

View File

@@ -1,6 +1,6 @@
--:Minify:-- --:Minify:--
syscall.open("/dev/tty/TTY1","r") --stdin (Device 0) syscall.open("/dev/tty/tty1","r") --stdin (Device 0)
syscall.open("/dev/tty/TTY1","w") --stdout (Device 1) syscall.open("/dev/tty/tty1","w") --stdout (Device 1)
syscall.open("/dev/null","w") --stderr (device 2) syscall.open("/dev/null","w") --stderr (device 2)
local success, errorMsg = xpcall(function() local success, errorMsg = xpcall(function()
@@ -17,7 +17,13 @@ local commandHistory = {}
local terminate = false local terminate = false
syscall.setEnviron("SHELL","rtbash") syscall.setEnviron("SHELL","rtbash")
syscall.setEnviron("PATH","/bin/") syscall.setEnviron("PATH","/bin/")
local _home = syscall.getEnviron("HOME")
if _home and _home ~= "" then
local ok = pcall(syscall.chdir, _home)
if not ok then syscall.chdir("/") end
else
syscall.chdir("/") syscall.chdir("/")
end
local oldWD = "" local oldWD = ""
for i = 1, 16 do for i = 1, 16 do
@@ -951,8 +957,8 @@ local function runCommand(command)
end end
local proc = syscall.spawn(function(...) local proc = syscall.spawn(function(...)
syscall.open("/dev/tty/TTY1","r") syscall.open("/dev/tty/tty1","r")
syscall.open("/dev/tty/TTY1","w") syscall.open("/dev/tty/tty1","w")
syscall.open("/dev/null","w") syscall.open("/dev/null","w")
local ok2, msg = pcall(program, ...) local ok2, msg = pcall(program, ...)
if not ok2 then printError(progName, msg) end if not ok2 then printError(progName, msg) end

View File

@@ -1,6 +1,6 @@
--:Minify:-- --:Minify:--
syscall.open("/dev/tty/TTY1","r") syscall.open("/dev/tty/tty1","r")
syscall.open("/dev/tty/TTY1","w") syscall.open("/dev/tty/tty1","w")
syscall.open("/dev/null","r") syscall.open("/dev/null","r")
syscall.devctl(1,"clear") syscall.devctl(1,"clear")
syscall.devctl(1,"sfgc",1) syscall.devctl(1,"sfgc",1)
@@ -57,8 +57,8 @@ while true do
printInline("> ") printInline("> ")
end end
proc = syscall.spawn(function(...) proc = syscall.spawn(function(...)
syscall.open("/dev/tty/TTY1","r") syscall.open("/dev/tty/tty1","r")
syscall.open("/dev/tty/TTY1","w") syscall.open("/dev/tty/tty1","w")
syscall.open("/dev/null","w") syscall.open("/dev/null","w")
program(...) program(...)
end, path, nil, {table.unpack(split, 2)}) end, path, nil, {table.unpack(split, 2)})

View File

@@ -1,6 +1,6 @@
--:Minify:-- --:Minify:--
syscall.open("/dev/tty/TTY1", "r") --stdin (fd 0) syscall.open("/dev/tty/tty1", "r") --stdin (fd 0)
syscall.open("/dev/tty/TTY1", "w") --stdout (fd 1) syscall.open("/dev/tty/tty1", "w") --stdout (fd 1)
syscall.open("/dev/null", "w") --stderr (fd 2) syscall.open("/dev/null", "w") --stderr (fd 2)
@@ -96,6 +96,9 @@ local function spawnShell(username, uid, shell, homedir)
end end
local chdirOk, chdirErr = pcall(syscall.chdir, homedir) local chdirOk, chdirErr = pcall(syscall.chdir, homedir)
if not chdirOk then
pcall(syscall.chdir, "/")
end
local ok, err = pcall(syscall.execspawn, shell, username .. ":shell") local ok, err = pcall(syscall.execspawn, shell, username .. ":shell")
if not ok then if not ok then

View File

@@ -31,7 +31,7 @@ for j = i + 1, #cmdArgs do restArgs[#restArgs + 1] = cmdArgs[j] end
local currentUid = syscall.getuid() local currentUid = syscall.getuid()
local currentUser = syscall.getUsername(currentUid) or tostring(currentUid) local currentUser = syscall.getUsername(currentUid) or tostring(currentUid)
local targetUid = syscall.getuid(targetUser) local targetUid = syscall.getuidbyname(targetUser)
if not targetUid then if not targetUid then
print("sudo: user '" .. targetUser .. "' does not exist") print("sudo: user '" .. targetUser .. "' does not exist")
syscall.exit(1) syscall.exit(1)
@@ -39,7 +39,7 @@ if not targetUid then
end end
if currentUid ~= 0 then if currentUid ~= 0 then
printInline("[sudo] password for " .. currentUser .. ": ") printInline("[sudo] password for root: ")
local pw = "" local pw = ""
while true do while true do
local ch = syscall.read(0) local ch = syscall.read(0)
@@ -55,7 +55,7 @@ if currentUid ~= 0 then
end end
end end
local ok, err = syscall.elevate(currentUser, pw) local ok, err = syscall.elevate("root", pw)
if not ok then if not ok then
sleep(1) sleep(1)
print("sudo: Authentication failure") print("sudo: Authentication failure")
@@ -63,7 +63,7 @@ if currentUid ~= 0 then
return return
end end
if targetUid ~= 0 then if targetUid ~= currentUid then
syscall.setuid(targetUid) syscall.setuid(targetUid)
end end
else else

View File

@@ -14,6 +14,9 @@ for i,v in pairs(kernel.processes) do
end, i) end, i)
end end
if not fs.exists("/bin/startup") then
fs.mkdir("/bin/startup")
end
local files = fs.list("/bin/startup") local files = fs.list("/bin/startup")
if not files then error("Failed to list /bin/startup") end if not files then error("Failed to list /bin/startup") end
for i,v in ipairs(files) do for i,v in ipairs(files) do

View File

@@ -37,7 +37,7 @@ function peripheral.isPresent(name)
end end
function peripheral.getType(peripheral) function peripheral.getType(peripheral)
if type(peripheral) == "string" then -- Peripheral name passed if type(peripheral) == "string" then
if native.isPresent(peripheral) then if native.isPresent(peripheral) then
return native.getType(peripheral) return native.getType(peripheral)
end end
@@ -58,7 +58,7 @@ function peripheral.getType(peripheral)
end end
function peripheral.hasType(peripheral, peripheral_type) function peripheral.hasType(peripheral, peripheral_type)
if type(peripheral) == "string" then -- Peripheral name passed if type(peripheral) == "string" then
if native.isPresent(peripheral) then if native.isPresent(peripheral) then
return native.hasType(peripheral, peripheral_type) return native.hasType(peripheral, peripheral_type)
end end

View File

@@ -184,11 +184,18 @@ end
kernel.log("Gathering modules") kernel.log("Gathering modules")
for _, i in ipairs(ifs.list("/lib/modules")) do for _, i in ipairs(ifs.list("/lib/modules")) do
for _,v in ipairs(ifs.list("/lib/modules/"..i)) do local modlist = ifs.list("/lib/modules/"..i)
if not modlist then
kernel.log("WARNING: could not list /lib/modules/"..i.." (skipping)", "WARN", 8)
else
for _,v in ipairs(modlist) do
local prior=tonumber(v:sub(1,2)) local prior=tonumber(v:sub(1,2))
if prior then
modules[prior+1][#modules[prior+1]+1]="/lib/modules/"..i.."/"..v modules[prior+1][#modules[prior+1]+1]="/lib/modules/"..i.."/"..v
end end
end end
end
end
kernel.ifs=ifs kernel.ifs=ifs
kernel.apis=apis kernel.apis=apis

View File

@@ -1,142 +0,0 @@
-- :Minify:--
local kernel = ...
local P = kernel.vfs.P
local PERM = kernel.vfs.PERM
local RW_R_R = P.OWNER_R + P.OWNER_W + P.GROUP_R + P.WORLD_R
local RWX_RX_RX = P.OWNER_R + P.OWNER_W + P.OWNER_X
+ P.GROUP_R + P.GROUP_X
+ P.WORLD_R + P.WORLD_X
local RW_R__ = P.OWNER_R + P.OWNER_W + P.GROUP_R
local RW____ = P.OWNER_R + P.OWNER_W
local RWXRWXRWX = PERM.RWXRWXRWX
local SUID_755 = PERM.SUID_755
local META_VERSION = 0x01
local rootDisk = kernel.disks["$"]
local function makeEntry(name, etype, owner, group, perms, cmeta)
cmeta = cmeta or ""
local plo = perms % 256
local phi = math.floor(perms / 256) % 256
return string.char(#name) .. name
.. string.char(etype, owner, group, plo, phi)
.. string.char(#cmeta) .. cmeta
end
local function writeMeta(dir, entries)
local diskDir = dir
if diskDir:sub(1,1) == "/" then diskDir = diskDir:sub(2) end
local metaPath = (diskDir == "" and ".meta" or diskDir .. "/.meta")
local data = string.char(META_VERSION)
for _, e in ipairs(entries) do
data = data .. makeEntry(e[1], e[2] or 0x00, e[3], e[4], e[5], e[6])
end
local ok, err = pcall(function()
local f = rootDisk:open(metaPath, "w")
f.write(data)
f.close()
end)
if not ok then
kernel.log("permissions: failed to write " .. metaPath .. ": " .. tostring(err), "WARN", 8)
end
end
local REG = 0x00
if rootDisk:fileExists(".meta") then
kernel.log("Permissions already seeded, skipping.", "INFO")
else
kernel.log("Seeding filesystem permissions...", "INFO")
-- /
writeMeta("/", {
{"bin", REG, 0, 0, RWX_RX_RX},
{"boot", REG, 0, 0, RWX_RX_RX},
{"dev", REG, 0, 0, RWX_RX_RX},
{"etc", REG, 0, 0, RWX_RX_RX},
{"home", REG, 0, 0, RWX_RX_RX},
{"lib", REG, 0, 0, RWX_RX_RX},
{"root", REG, 0, 0, RW____ },
{"sbin", REG, 0, 0, RWX_RX_RX},
{"tmp", REG, 0, 0, RWXRWXRWX},
{"usr", REG, 0, 0, RWX_RX_RX},
{"var", REG, 0, 0, RWX_RX_RX},
})
-- /bin
writeMeta("/bin", {
{"cat", REG, 0, 0, RWX_RX_RX},
{"clear", REG, 0, 0, RWX_RX_RX},
{"echo", REG, 0, 0, RWX_RX_RX},
{"hfetch", REG, 0, 0, RWX_RX_RX},
{"hysh", REG, 0, 0, RWX_RX_RX},
{"hyshex", REG, 0, 0, RWX_RX_RX},
{"install", REG, 0, 0, RWX_RX_RX},
{"login", REG, 0, 0, SUID_755 },
{"ls", REG, 0, 0, RWX_RX_RX},
{"lua", REG, 0, 0, RWX_RX_RX},
{"luaold", REG, 0, 0, RWX_RX_RX},
{"mkdir", REG, 0, 0, RWX_RX_RX},
{"ps", REG, 0, 0, RWX_RX_RX},
{"pwd", REG, 0, 0, RWX_RX_RX},
{"spm", REG, 0, 0, RWX_RX_RX},
{"su", REG, 0, 0, SUID_755 },
{"sudo", REG, 0, 0, SUID_755 },
{"sysdump", REG, 0, 0, RWX_RX_RX},
{"whoami", REG, 0, 0, RWX_RX_RX},
{"yes", REG, 0, 0, RWX_RX_RX},
{"startup", REG, 0, 0, RWX_RX_RX},
{"ln", REG, 0, 0, RWX_RX_RX},
{"readlink", REG, 0, 0, RWX_RX_RX},
})
writeMeta("/bin/startup", {
{"test.lua", REG, 0, 0, RWX_RX_RX},
})
-- /etc
writeMeta("/etc", {
{"passwd", REG, 0, 0, RW_R_R},
{"shadow", REG, 0, 0, RW____},
{"pam.d", REG, 0, 0, RWX_RX_RX},
})
writeMeta("/etc/pam.d", {
{"secret", REG, 0, 0, RW____},
})
-- /sbin
writeMeta("/sbin", {
{"init.lua", REG, 0, 0, RWX_RX_RX},
})
-- /boot
writeMeta("/boot", {
{"kernel.lua", REG, 0, 0, RW_R_R },
{"boot.cfg", REG, 0, 0, RW_R_R },
{"safeboot.cfg", REG, 0, 0, RW_R_R },
{"fstab", REG, 0, 0, RW_R_R },
{"initfs", REG, 0, 0, RW_R_R },
{"cct", REG, 0, 0, RWX_RX_RX},
{"oc", REG, 0, 0, RWX_RX_RX},
})
-- /lib
writeMeta("/lib", {
{"sys", REG, 0, 0, RWX_RX_RX},
{"modules", REG, 0, 0, RWX_RX_RX},
{"crypto", REG, 0, 0, RWX_RX_RX},
{"store", REG, 0, 0, RWX_RX_RX},
{"snip", REG, 0, 0, RW_R_R },
{"io", REG, 0, 0, RW_R_R },
{"bit32", REG, 0, 0, RW_R_R },
})
kernel.log("Filesystem permissions seeded.", "INFO")
end
kernel.log("Permission module loaded.", "INFO")

View File

@@ -1,4 +1,7 @@
-- :Minify:-- -- :Minify:--
local kernel = ...
kernel.allowGlobalOverwrites = true
function string.hasSuffix(str, suffix) function string.hasSuffix(str, suffix)
return string.sub(str, #suffix + 1) == suffix return string.sub(str, #suffix + 1) == suffix
end end
@@ -68,30 +71,24 @@ end
local function serialize(tbl, seen) local function serialize(tbl, seen)
seen = seen or {} seen = seen or {}
-- If we've seen this table before, return a placeholder to prevent infinite loops
if seen[tbl] then return '"[Circular Reference]"' end if seen[tbl] then return '"[Circular Reference]"' end
-- Mark this table as seen
seen[tbl] = true seen[tbl] = true
local output = "{" local output = "{"
local first = true local first = true
for i, v in pairs(tbl) do for i, v in pairs(tbl) do
-- Handle comma placement more cleanly
if not first then output = output .. "," end if not first then output = output .. "," end
first = false first = false
-- Serialize Key
if type(i) == "string" then if type(i) == "string" then
output = output .. "[\"" .. i .. "\"]=" output = output .. "[\"" .. i .. "\"]="
elseif type(i) == "number" then elseif type(i) == "number" then
output = output .. "[" .. tostring(i) .. "]=" output = output .. "[" .. tostring(i) .. "]="
end end
-- Serialize Value
if type(v) == "table" then if type(v) == "table" then
-- Pass the 'seen' table down to the recursive call
output = output .. serialize(v, seen) output = output .. serialize(v, seen)
elseif type(v) == "string" then elseif type(v) == "string" then
output = output .. "[=[" .. v .. "]=]" output = output .. "[=[" .. v .. "]=]"

View File

@@ -5,14 +5,14 @@ kernel.vfs = vfs
vfs.mounts = {["$"] = "/"} vfs.mounts = {["$"] = "/"}
vfs.disks = kernel.disks vfs.disks = kernel.disks
-- Metafile format (version 1) -- Metafile format (version 2)
-- File header: 1 byte = version (0x01) -- File header: 1 byte = version (0x02)
-- Per-entry: -- Per-entry:
-- 1 byte = name length -- 1 byte = name length
-- N bytes = name -- N bytes = name
-- 1 byte = entry type (0x00 = regular, 0x01 = symlink) -- 1 byte = entry type (0x00 = regular, 0x01 = symlink)
-- 1 byte = owner uid -- 2 bytes = owner uid (little-endian uint16)
-- 1 byte = group gid -- 2 bytes = group gid (little-endian uint16)
-- 2 bytes = perms (little-endian uint16) -- 2 bytes = perms (little-endian uint16)
-- bit 0 = world-write bit 1 = world-read -- bit 0 = world-write bit 1 = world-read
-- bit 2 = group-write bit 3 = group-read -- bit 2 = group-write bit 3 = group-read
@@ -24,12 +24,16 @@ vfs.disks = kernel.disks
-- 1 byte = cmeta length -- 1 byte = cmeta length
-- N bytes = cmeta (for symlinks: the link target path) -- N bytes = cmeta (for symlinks: the link target path)
-- --
-- Version 1:
-- 1 byte name len, N bytes name, 1 byte etype, 1 byte owner,
-- 1 byte group, 2 bytes perms (little-endian), 1 byte cmeta len, N bytes cmeta
--
-- Version 0: -- Version 0:
-- No file header. Per-entry: -- No file header. Per-entry:
-- 1 byte name len, N bytes name, 1 byte owner, 1 byte group, -- 1 byte name len, N bytes name, 1 byte owner, 1 byte group,
-- 1 byte perms (low 7 bits only), 1 byte cmeta len, N bytes cmeta -- 1 byte perms (low 7 bits only), 1 byte cmeta len, N bytes cmeta
local META_VERSION = 0x01 local META_VERSION = 0x02
local function bit_is_set(num, bit) local function bit_is_set(num, bit)
return math.floor(num / (2 ^ bit)) % 2 == 1 return math.floor(num / (2 ^ bit)) % 2 == 1
@@ -41,8 +45,9 @@ local function parseMetafile(raw)
local p = 1 local p = 1
local version = 0 local version = 0
if raw:byte(1) == META_VERSION then local firstByte = raw:byte(1)
version = META_VERSION if firstByte == 0x02 or firstByte == 0x01 then
version = firstByte
p = 2 p = 2
end end
@@ -54,7 +59,13 @@ local function parseMetafile(raw)
local etype, owner, group, perms, cmeta local etype, owner, group, perms, cmeta
if version == META_VERSION then if version == 0x02 then
if p + 6 > #raw then break end
etype = raw:byte(p); p = p + 1
owner = raw:byte(p) + raw:byte(p+1) * 256; p = p + 2
group = raw:byte(p) + raw:byte(p+1) * 256; p = p + 2
perms = raw:byte(p) + raw:byte(p+1) * 256; p = p + 2
elseif version == 0x01 then
if p + 4 > #raw then break end if p + 4 > #raw then break end
etype = raw:byte(p); p = p + 1 etype = raw:byte(p); p = p + 1
owner = raw:byte(p); p = p + 1 owner = raw:byte(p); p = p + 1
@@ -87,34 +98,71 @@ local function makeMetafile(meta)
for name, m in pairs(meta) do for name, m in pairs(meta) do
local plo = m.perms % 256 local plo = m.perms % 256
local phi = math.floor(m.perms / 256) % 256 local phi = math.floor(m.perms / 256) % 256
local olo = (m.owner or 0) % 256
local ohi = math.floor((m.owner or 0) / 256) % 256
local glo = (m.group or 0) % 256
local ghi = math.floor((m.group or 0) / 256) % 256
out = out out = out
.. string.char(#name) .. name .. string.char(#name) .. name
.. string.char(m.etype or 0x00) .. string.char(m.etype or 0x00)
.. string.char(m.owner, m.group, plo, phi) .. string.char(olo, ohi, glo, ghi, plo, phi)
.. string.char(#m.cmeta) .. m.cmeta .. string.char(#m.cmeta) .. m.cmeta
end end
return out return out
end end
local SAFE_COMPONENT_PATTERN = "^[A-Za-z0-9_.+%-%@%(%)%[%]]+$"
local function normalizePath(path) local function normalizePath(path)
local task = kernel.currentTask local task = kernel.currentTask
local cwd = task.cwd or "/" local cwd = task.cwd or "/"
if path:sub(1,1) ~= "/" then path = cwd .. "/" .. path end
local parts = {} if path:sub(1, 1) ~= "/" then
for part in path:gmatch("[^/]+") do path = cwd .. "/" .. path
if part == ".." then end
if #parts > 0 then table.remove(parts) end
elseif part ~= "." and part ~= "" then local stack = {}
table.insert(parts, part) local i = 1
local len = #path
while i <= len do
local j = path:find("/", i, true)
local comp
if j then
comp = path:sub(i, j - 1)
i = j + 1
else
comp = path:sub(i)
i = len + 1
end
comp = comp:match("^%s*(.-)%s*$")
if comp == "" or comp == "." then
elseif comp == ".." then
if #stack > 0 then
table.remove(stack)
end
else
comp = comp:lower()
if not comp:match(SAFE_COMPONENT_PATTERN) then
error("EINVAL: illegal characters in path component: " .. comp, 2)
end
if comp == ".meta" then
error("EINVAL: reserved path component: " .. comp, 2)
end
table.insert(stack, comp)
end end
end end
local result = "/" .. table.concat(parts, "/")
local result = "/" .. table.concat(stack, "/")
local root = task and task.root local root = task and task.root
if root and root ~= "/" then if root and root ~= "/" then
if result ~= root and result:sub(1, #root + 1) ~= root .. "/" then if result ~= root and result:sub(1, #root + 1) ~= root .. "/" then
result = root result = root
end end
end end
return result return result
end end
@@ -146,8 +194,10 @@ local function resolveMount(normalPath)
return vfs.disks[mountId], diskPath return vfs.disks[mountId], diskPath
end end
vfs._parseMetafile = parseMetafile
local function readMetaEntry(disk, parentDiskPath, filename) local function readMetaEntry(disk, parentDiskPath, filename)
if filename == ".meta" then error("Cannot open metafile") end if filename == ".meta" then error("EACCES: Cannot open metafile") end
local mp local mp
if parentDiskPath == "/" then if parentDiskPath == "/" then
mp = ".meta" mp = ".meta"
@@ -159,6 +209,14 @@ local function readMetaEntry(disk, parentDiskPath, filename)
if not ok or not f then return nil end if not ok or not f then return nil end
local raw = f.read(65535) local raw = f.read(65535)
if f.close then f.close() end if f.close then f.close() end
if raw and #raw > 0 and raw:byte(1) ~= META_VERSION then
local upgraded = makeMetafile(parseMetafile(raw))
local wok, wf = pcall(function() return disk:open(mp, "w") end)
if wok and wf then wf.write(upgraded); if wf.close then wf.close() end end
raw = upgraded
end
local parsed = parseMetafile(raw) local parsed = parseMetafile(raw)
return parsed[filename] return parsed[filename]
end end
@@ -400,6 +458,8 @@ function vfs.open(path, mode)
if not disk then error("NODISK") end if not disk then error("NODISK") end
local meta = getFileMeta(path) local meta = getFileMeta(path)
local isNew = (mode == "w" or mode == "a") and not disk:fileExists(diskPath)
checkperms(meta, mode == "r" and "r" or "w") checkperms(meta, mode == "r" and "r" or "w")
local handle local handle
@@ -408,6 +468,21 @@ function vfs.open(path, mode)
if type(handle) ~= "table" then error("ENFILE") end if type(handle) ~= "table" then error("ENFILE") end
end end
if isNew then
local euid = (task and (task.euid or task.uid)) or kernel.uid
local egid = (task and task.gid) or 0
local norm = normalizePath(path)
local parent = norm:match("^(.*)/[^/]+$") or "/"
if parent == "" then parent = "/" end
local name = norm:match("[^/]+$")
if name then
local entry = { etype=0x00, owner=euid, group=egid,
perms=vfs.PERM.RW_R_R, cmeta="" }
pcall(writeMetaEntry, parent, name, entry, false)
meta = entry
end
end
local fobj = newFileObj(handle, mode, path, meta, disk:type(diskPath)) local fobj = newFileObj(handle, mode, path, meta, disk:type(diskPath))
if mode == "r" and bit_is_set(meta.perms, 6) then if mode == "r" and bit_is_set(meta.perms, 6) then
fobj.suid_owner = meta.owner fobj.suid_owner = meta.owner
@@ -574,15 +649,32 @@ function vfs.listdir(path)
end end
function vfs.mkdir(path) function vfs.mkdir(path)
local norm = normalizePath(path)
local parent = norm:match("^(.*)/[^/]+$") or "/"
if parent == "" then parent = "/" end
local parentMeta = getFileMeta(parent)
checkperms(parentMeta, "w")
local disk, diskPath = resolvePath(path) local disk, diskPath = resolvePath(path)
local meta = getFileMeta(path)
checkperms(meta, "w")
disk:makeDirectory(diskPath) disk:makeDirectory(diskPath)
local task = kernel.currentTask
local euid = (task and (task.euid or task.uid)) or kernel.uid
local egid = (task and task.gid) or 0
local name = norm:match("[^/]+$")
if name then
local entry = { etype=0x00, owner=euid, group=egid,
perms=vfs.PERM.RWX_RX, cmeta="" }
pcall(writeMetaEntry, parent, name, entry, false)
end
end end
function vfs.remove(path) function vfs.remove(path)
local norm = resolveSymlinks(path, true)
local parent = norm:match("^(.*)/[^/]+$") or "/"
if parent == "" then parent = "/" end
local parentMeta = getFileMeta(parent)
checkperms(parentMeta, "w")
local meta = getFileMeta(path, true) local meta = getFileMeta(path, true)
checkperms(meta, "w")
if kernel.unixSockets and kernel.unixSockets[path] then if kernel.unixSockets and kernel.unixSockets[path] then
kernel.unixSockets[path] = nil kernel.unixSockets[path] = nil

View File

@@ -385,11 +385,11 @@ kernel.processes.cctmond = function()
end end
end end
newtty(apis.term, "TTY1", fifo.pop) newtty(apis.term, "tty1", fifo.pop)
for i,v in ipairs({peripheral.find("monitor")}) do for i,v in ipairs({peripheral.find("monitor")}) do
v.setTextScale(.5) v.setTextScale(.5)
v.write("Initializing...") v.write("Initializing...")
newtty(v,"TTY"..tostring(i+1),function () end) newtty(v,"tty"..tostring(i+1),function () end)
end end

View File

@@ -3,6 +3,7 @@ local args = {...}
local kernel = args[1] local kernel = args[1]
kernel._G = _G kernel._G = _G
local function readonly(tbl) local function readonly(tbl)
return setmetatable({}, { return setmetatable({}, {
__index = function(_, key) __index = function(_, key)
@@ -20,7 +21,7 @@ local function readonly(tbl)
error("Attempt to modify global variable '" .. k .. "'", 2) error("Attempt to modify global variable '" .. k .. "'", 2)
end, end,
__pairs = function() __pairs = function(self)
local function iter(_, key) local function iter(_, key)
local nextKey, value = next(tbl, key) local nextKey, value = next(tbl, key)
if type(value) == "table" then if type(value) == "table" then
@@ -28,7 +29,7 @@ local function readonly(tbl)
end end
return nextKey, value return nextKey, value
end end
return iter, tbl, nil return iter, self, nil
end, end,
__ipairs = function() __ipairs = function()
@@ -49,8 +50,8 @@ local function readonly(tbl)
__metatable = false __metatable = false
}) })
end end
local origLoad = load
kernel._U = readonly(kernel._G) kernel._U = readonly(kernel._G)
kernel.allowGlobalOverwrites = true
kernel._U._G = kernel._U kernel._U._G = kernel._U
kernel.allowGlobalOverwrites = false kernel._U.load = function(a,b,c,d) return origLoad(a,b,c,d or kernel._U) end

View File

@@ -394,6 +394,8 @@ function auth.newUser(username, password, gid, homedir, shell)
if kernel.vfs.mkdir and not kernel.vfs.exists(homedir) then if kernel.vfs.mkdir and not kernel.vfs.exists(homedir) then
kernel.vfs.mkdir(homedir) kernel.vfs.mkdir(homedir)
-- Homedir must be owned by the new user, not root
pcall(kernel.vfs.chown, homedir, uid, uid)
end end
kernel.log("AUTH: new user '" .. username .. "' uid=" .. tostring(uid)) kernel.log("AUTH: new user '" .. username .. "' uid=" .. tostring(uid))
@@ -591,13 +593,13 @@ function auth.elevate(targetUsername, password)
local task = kernel.currentTask local task = kernel.currentTask
local prevUid = task.uid local prevUid = task.uid
task.uid = uid task.uid = 0
task.euid = uid task.euid = 0
task.gid = tonumber(entry[2]) or uid task.gid = 0
task.egid = tonumber(entry[2]) or uid task.egid = 0
kernel.uid = uid kernel.uid = 0
kernel.log("AUTH: elevate uid=" .. tostring(prevUid) .. " -> " .. tostring(uid) .. " (" .. targetUsername .. ")") kernel.log("AUTH: elevate uid=" .. tostring(prevUid) .. " -> 0 (via " .. targetUsername .. ")")
return true, uid return true, uid
end end

View File

@@ -5,6 +5,8 @@ local sys = {}
local nextpid = 2 local nextpid = 2
kernel.exitMain = false kernel.exitMain = false
local resumeWithTimeout = coroutine.resumeWithTimeout
local function bit_is_set(num, bit) local function bit_is_set(num, bit)
return math.floor(num / (2 ^ bit)) % 2 == 1 return math.floor(num / (2 ^ bit)) % 2 == 1
end end
@@ -206,11 +208,15 @@ function sys.kill(pid)
return false, "Task does not exist" return false, "Task does not exist"
elseif task.status == "Z" then elseif task.status == "Z" then
return false, "Task is already dead" return false, "Task is already dead"
else end
local caller = kernel.currentTask
local ceuid = caller and (caller.euid or caller.uid) or kernel.uid
if ceuid ~= 0 and task.uid ~= (caller and caller.uid or kernel.uid) then
return false, "EPERM"
end
task.status = "Z" task.status = "Z"
return true return true
end end
end
function sys.stop(pid) function sys.stop(pid)
local task = tasks[tostring(pid)] local task = tasks[tostring(pid)]
@@ -352,7 +358,7 @@ function kernel.main()
if task.sigq and #task.sigq ~= 0 and task.sigh then if task.sigq and #task.sigq ~= 0 and task.sigh then
local coro = coroutine.create(task.sigh) local coro = coroutine.create(task.sigh)
if kernel.config.preempt then if kernel.config.preempt then
coroutine.resumeWithTimeout(coro, task.timeSlice, table.remove(task.sigq, 1)) resumeWithTimeout(coro, task.timeSlice, table.remove(task.sigq, 1))
else else
coroutine.resume(coro, table.remove(task.sigq, 1)) coroutine.resume(coro, table.remove(task.sigq, 1))
end end
@@ -363,7 +369,7 @@ function kernel.main()
local ret local ret
if kernel.config.preempt then if kernel.config.preempt then
ret = { coroutine.resumeWithTimeout(task.coro, task.timeSlice, table.unpack(task.syscallReturn)) } ret = { resumeWithTimeout(task.coro, task.timeSlice, table.unpack(task.syscallReturn)) }
else else
ret = { coroutine.resume(task.coro, table.unpack(task.syscallReturn)) } ret = { coroutine.resume(task.coro, table.unpack(task.syscallReturn)) }
end end

View File

@@ -0,0 +1,233 @@
-- :Minify:--
local kernel = ...
local P = kernel.vfs.P
local PERM = kernel.vfs.PERM
local RW_R_R = P.OWNER_R + P.OWNER_W + P.GROUP_R + P.WORLD_R
local RWX_RX_RX = P.OWNER_R + P.OWNER_W + P.OWNER_X
+ P.GROUP_R + P.GROUP_X
+ P.WORLD_R + P.WORLD_X
local RW_R__ = P.OWNER_R + P.OWNER_W + P.GROUP_R
local RW____ = P.OWNER_R + P.OWNER_W
local RWXRWXRWX = PERM.RWXRWXRWX
local SUID_755 = PERM.SUID_755
local META_VERSION = 0x02
local rootDisk = kernel.disks["$"]
local function makeEntry(name, etype, owner, group, perms, cmeta)
cmeta = cmeta or ""
local plo = perms % 256
local phi = math.floor(perms / 256) % 256
local olo = (owner or 0) % 256
local ohi = math.floor((owner or 0) / 256) % 256
local glo = (group or 0) % 256
local ghi = math.floor((group or 0) / 256) % 256
return string.char(#name) .. name
.. string.char(etype, olo, ohi, glo, ghi, plo, phi)
.. string.char(#cmeta) .. cmeta
end
local REG = 0x00
local function mergeMeta(dir, entries)
local diskDir = dir
if diskDir:sub(1,1) == "/" then diskDir = diskDir:sub(2) end
local metaPath = (diskDir == "" and ".meta" or diskDir .. "/.meta")
local existing = {}
local rok, rf = pcall(function() return rootDisk:open(metaPath, "r") end)
if rok and rf then
local raw = rf.read(65535)
if rf.close then rf.close() end
existing = (kernel.vfs._parseMetafile and kernel.vfs._parseMetafile(raw)) or {}
end
for _, e in ipairs(entries) do
local name = e[1]
local etype = e[2] or REG
local owner = e[3] or 0
local group = e[4] or 0
local perms = e[5] or RWX_RX_RX
local cmeta = e[6] or ""
existing[name] = {
etype = etype,
owner = owner,
group = group,
perms = perms,
cmeta = cmeta,
}
end
local data = string.char(META_VERSION)
for name, m in pairs(existing) do
data = data .. makeEntry(
name,
m.etype or REG,
m.owner or 0,
m.group or 0,
m.perms or RWX_RX_RX,
m.cmeta or ""
)
end
local ok, err = pcall(function()
local f = rootDisk:open(metaPath, "w")
f.write(data)
f.close()
end)
if not ok then
kernel.log("permissions: failed to write " .. metaPath .. ": " .. tostring(err), "WARN", 8)
end
end
kernel.log("Seeding filesystem permissions...", "INFO")
mergeMeta("/", {
{"bin", REG, 0, 0, RWX_RX_RX},
{"boot", REG, 0, 0, RWX_RX_RX},
{"dev", REG, 0, 0, RWX_RX_RX},
{"etc", REG, 0, 0, RWX_RX_RX},
{"home", REG, 0, 0, RWX_RX_RX},
{"lib", REG, 0, 0, RWX_RX_RX},
{"root", REG, 0, 0, RW____ },
{"sbin", REG, 0, 0, RWX_RX_RX},
{"tmp", REG, 0, 0, RWXRWXRWX},
{"usr", REG, 0, 0, RWX_RX_RX},
{"var", REG, 0, 0, RWX_RX_RX},
})
mergeMeta("/boot", {
{"kernel.lua", REG, 0, 0, RW_R_R },
{"boot.cfg", REG, 0, 0, RW_R_R },
{"safeboot.cfg", REG, 0, 0, RW_R_R },
{"fstab", REG, 0, 0, RW_R_R },
{"initfs", REG, 0, 0, RW_R_R },
{"cct", REG, 0, 0, RWX_RX_RX},
{"oc", REG, 0, 0, RWX_RX_RX},
})
mergeMeta("/boot/cct", {
{"boot.lua", REG, 0, 0, RW_R_R},
{"initdisks", REG, 0, 0, RW_R_R},
{"eeprom", REG, 0, 0, RW_R_R},
})
mergeMeta("/boot/oc", {
{"boot.lua", REG, 0, 0, RW_R_R},
{"initfs.lua",REG, 0, 0, RW_R_R},
{"eeprom", REG, 0, 0, RW_R_R},
})
mergeMeta("/sbin", {
{"init.lua", REG, 0, 0, RWX_RX_RX},
})
mergeMeta("/bin", {
{"cat", REG, 0, 0, RWX_RX_RX},
{"chattr", REG, 0, 0, RWX_RX_RX},
{"chgrp", REG, 0, 0, RWX_RX_RX},
{"chmod", REG, 0, 0, RWX_RX_RX},
{"chown", REG, 0, 0, RWX_RX_RX},
{"chroot", REG, 0, 0, RWX_RX_RX},
{"clear", REG, 0, 0, RWX_RX_RX},
{"echo", REG, 0, 0, RWX_RX_RX},
{"hfetch", REG, 0, 0, RWX_RX_RX},
{"help", REG, 0, 0, RWX_RX_RX},
{"hysh", REG, 0, 0, RWX_RX_RX},
{"hyshex", REG, 0, 0, RWX_RX_RX},
{"id", REG, 0, 0, RWX_RX_RX},
{"install", REG, 0, 0, RWX_RX_RX},
{"ln", REG, 0, 0, RWX_RX_RX},
{"login", REG, 0, 0, SUID_755 },
{"loimgcreate", REG, 0, 0, RWX_RX_RX},
{"looptest", REG, 0, 0, RWX_RX_RX},
{"losetup", REG, 0, 0, RWX_RX_RX},
{"ls", REG, 0, 0, RWX_RX_RX},
{"lsusers", REG, 0, 0, RWX_RX_RX},
{"lua", REG, 0, 0, RWX_RX_RX},
{"luaold", REG, 0, 0, RWX_RX_RX},
{"micro", REG, 0, 0, RWX_RX_RX},
{"mkdir", REG, 0, 0, RWX_RX_RX},
{"mount", REG, 0, 0, RWX_RX_RX},
{"passwd", REG, 0, 0, RWX_RX_RX},
{"ps", REG, 0, 0, RWX_RX_RX},
{"pwd", REG, 0, 0, RWX_RX_RX},
{"readlink", REG, 0, 0, RWX_RX_RX},
{"sed", REG, 0, 0, RWX_RX_RX},
{"socktest", REG, 0, 0, RWX_RX_RX},
{"spm", REG, 0, 0, RWX_RX_RX},
{"startup", REG, 0, 0, RWX_RX_RX},
{"su", REG, 0, 0, SUID_755 },
{"sudo", REG, 0, 0, SUID_755 },
{"sysdump", REG, 0, 0, RWX_RX_RX},
{"umount", REG, 0, 0, RWX_RX_RX},
{"useradd", REG, 0, 0, RWX_RX_RX},
{"userdel", REG, 0, 0, RWX_RX_RX},
{"usermod", REG, 0, 0, RWX_RX_RX},
{"whoami", REG, 0, 0, RWX_RX_RX},
{"yes", REG, 0, 0, RWX_RX_RX},
})
mergeMeta("/bin/startup", {
{"test.lua", REG, 0, 0, RWX_RX_RX},
})
mergeMeta("/lib", {
{"sys", REG, 0, 0, RWX_RX_RX},
{"modules", REG, 0, 0, RWX_RX_RX},
{"crypto", REG, 0, 0, RWX_RX_RX},
{"store", REG, 0, 0, RWX_RX_RX},
{"snip", REG, 0, 0, RW_R_R },
{"io", REG, 0, 0, RW_R_R },
{"bit32", REG, 0, 0, RW_R_R },
})
mergeMeta("/lib/sys", {
{"fs", REG, 0, 0, RW_R_R},
{"hpv", REG, 0, 0, RW_R_R},
{"ipc", REG, 0, 0, RW_R_R},
{"term", REG, 0, 0, RW_R_R},
{"init", REG, 0, 0, RW_R_R},
})
mergeMeta("/lib/modules", {
{"hyperion", REG, 0, 0, RWX_RX_RX},
})
mergeMeta("/lib/modules/hyperion", {
{"01_stdlib.kmod", REG, 0, 0, RW_R_R},
{"10_vfs.kmod", REG, 0, 0, RW_R_R},
{"11_require.kmod", REG, 0, 0, RW_R_R},
{"12_devfs.kmod", REG, 0, 0, RW_R_R},
{"12_tmpfs.kmod", REG, 0, 0, RW_R_R},
{"13_loopdev.kmod", REG, 0, 0, RW_R_R},
{"14_keventd.kmod", REG, 0, 0, RW_R_R},
{"19_fstab.kmod", REG, 0, 0, RW_R_R},
{"20_signals.kmod", REG, 0, 0, RW_R_R},
{"20_socket.kmod", REG, 0, 0, RW_R_R},
{"26_tty.kmod", REG, 0, 0, RW_R_R},
{"30_userspace.kmod", REG, 0, 0, RW_R_R},
{"40_auth.kmod", REG, 0, 0, RW_R_R},
{"45_hypervisor.kmod", REG, 0, 0, RW_R_R},
{"47_dbg.kmod", REG, 0, 0, RW_R_R},
{"50_gpio.kmod", REG, 0, 0, RW_R_R},
{"70_stdlibadv.kmod", REG, 0, 0, RW_R_R},
{"90_init.kmod", REG, 0, 0, RW_R_R},
{"91_login.kmod", REG, 0, 0, RW_R_R},
{"92_permissions.kmod", REG, 0, 0, RW_R_R},
{"99_final.kmod", REG, 0, 0, RW_R_R},
})
mergeMeta("/etc", {
{"passwd", REG, 0, 0, RW_R_R },
{"shadow", REG, 0, 0, RW____ },
{"pam.d", REG, 0, 0, RWX_RX_RX},
})
mergeMeta("/etc/pam.d", {
{"secret", REG, 0, 0, RW____},
})
kernel.log("Filesystem permissions seeded.", "INFO")

View File

@@ -0,0 +1,2 @@
local kernel = ...
kernel.allowGlobalOverwrites = false

View File

@@ -203,7 +203,7 @@ def _make_firstboot_kmod(users):
lines.append("do") lines.append("do")
lines.append(" local ok, err = pcall(function()") lines.append(" local ok, err = pcall(function()")
lines.append(" kernel.vfs.remove('/lib/modules/Hyperion/50_firstboot_users.kmod')") lines.append(" kernel.vfs.remove('/lib/modules/hyperion/50_firstboot_users.kmod')")
lines.append(" end)") lines.append(" end)")
lines.append(" if not ok then") lines.append(" if not ok then")
lines.append(" kernel.log('FIRSTBOOT: could not self-delete: ' .. tostring(err), 'WARN')") lines.append(" kernel.log('FIRSTBOOT: could not self-delete: ' .. tostring(err), 'WARN')")
@@ -215,7 +215,7 @@ def _make_firstboot_kmod(users):
def inject_makeusers(users, arch): def inject_makeusers(users, arch):
base = BUILD_ROOT / "$" if arch else BUILD_ROOT base = BUILD_ROOT / "$" if arch else BUILD_ROOT
kmod_path = base / "lib" / "modules" / "Hyperion" / "50_firstboot_users.kmod" kmod_path = base / "lib" / "modules" / "hyperion" / "50_firstboot_users.kmod"
kmod_path.parent.mkdir(parents=True, exist_ok=True) kmod_path.parent.mkdir(parents=True, exist_ok=True)
kmod_path.write_text(_make_firstboot_kmod(users), encoding="utf-8") kmod_path.write_text(_make_firstboot_kmod(users), encoding="utf-8")
print(" Wrote first-boot user setup -> " + str(kmod_path.relative_to(BUILD_ROOT))) print(" Wrote first-boot user setup -> " + str(kmod_path.relative_to(BUILD_ROOT)))